2 * Copyright (c) 2010-2023 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.yamahareceiver.internal.protocol.xml;
15 import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLConstants.GET_PARAM;
16 import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLProtocolService.getResponse;
17 import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLUtils.*;
19 import java.io.IOException;
21 import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
22 import org.openhab.binding.yamahareceiver.internal.protocol.InputWithPresetControl;
23 import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
24 import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
25 import org.openhab.binding.yamahareceiver.internal.state.PlayInfoState;
26 import org.openhab.binding.yamahareceiver.internal.state.PresetInfoState;
27 import org.openhab.binding.yamahareceiver.internal.state.PresetInfoStateListener;
28 import org.slf4j.LoggerFactory;
29 import org.w3c.dom.Node;
32 * This class implements the Yamaha Receiver protocol related to navigation functionally. USB, NET_RADIO, IPOD and
33 * other inputs are using the same way of playback control.
35 * The XML nodes {@code <Play_Info>} and {@code <Play_Control>} are used.
39 * InputWithPlayControl menu = new InputWithPlayControl("NET_RADIO", comObject);
40 * menu.goToPath(menuDir);
41 * menu.selectItem(stationName);
43 * No state will be saved in here, but in {@link PlayInfoState} and
44 * {@link PresetInfoState} instead.
46 * @author David Graeff - Initial contribution
47 * @author Tomasz Maruszak - Compatibility fixes
49 public class InputWithPresetControlXML extends AbstractInputControlXML implements InputWithPresetControl {
51 private static final String PRESET_LETTERS = "ABCD";
53 protected CommandTemplate preset = new CommandTemplate(
54 "<Play_Control><Preset><Preset_Sel>%s</Preset_Sel></Preset></Play_Control>",
55 "Play_Control/Preset/Preset_Sel");
57 private final PresetInfoStateListener observer;
60 * Create an InputWithPlayControl object for altering menu positions and requesting current menu information as well
61 * as controlling the playback and choosing a preset item.
63 * @param inputID The input ID like USB or NET_RADIO.
64 * @param con The Yamaha communication object to send http requests.
66 public InputWithPresetControlXML(String inputID, AbstractConnection con, PresetInfoStateListener observer,
67 DeviceInformationState deviceInformationState) {
68 super(LoggerFactory.getLogger(InputWithPresetControlXML.class), inputID, con, deviceInformationState);
70 this.observer = observer;
72 this.applyModelVariations();
76 * Apply command changes to ensure compatibility with all supported models
78 protected void applyModelVariations() {
79 if (deviceDescriptor == null) {
80 logger.trace("Descriptor not available");
84 // add compatibility adjustments here (if any)
88 * Updates the preset information
91 * @throws ReceivedMessageParseException
94 public void update() throws IOException, ReceivedMessageParseException {
95 if (observer == null) {
99 AbstractConnection con = comReference.get();
100 Node response = getResponse(con,
101 wrInput("<Play_Control><Preset><Preset_Sel_Item>GetParam</Preset_Sel_Item></Preset></Play_Control>"),
104 PresetInfoState msg = new PresetInfoState();
106 // Set preset channel names, obtained from this xpath:
107 // NET_RADIO/Play_Control/Preset/Preset_Sel_Item/Item_1/Title
108 Node presetNode = getNode(response, "Play_Control/Preset/Preset_Sel_Item");
109 if (presetNode != null) {
110 for (int i = 1; i <= PRESET_CHANNELS; i++) {
111 Node itemNode = getNode(presetNode, "Item_" + i);
112 if (itemNode == null) {
116 String title = getNodeContentOrDefault(itemNode, "Title", "Item_" + i);
117 String value = getNodeContentOrDefault(itemNode, "Param", String.valueOf(i));
119 // For RX-V3900 when a preset slot is not used, this is how it looks
120 if (title.isEmpty() && "Not Used".equalsIgnoreCase(value)) {
124 int presetChannel = convertToPresetNumber(value);
125 PresetInfoState.Preset preset = new PresetInfoState.Preset(title, presetChannel);
126 msg.presetChannelNames.add(preset);
129 msg.presetChannelNamesChanged = true;
131 String presetValue = getNodeContentOrEmpty(response, preset.getPath());
133 // fall back to second method of obtaining current preset (works for Tuner on RX-V3900)
134 if (presetValue.isEmpty()) {
136 Node presetResponse = getResponse(con, wrInput(preset.apply(GET_PARAM)), inputElement);
137 presetValue = getNodeContentOrEmpty(presetResponse, preset.getPath());
138 } catch (IOException | ReceivedMessageParseException e) {
139 // this is on purpose, in case the AVR does not support this request and responds with error or nonsense
143 // For Tuner input on RX-V3900 this is not a number (e.g. "A1" or "B1").
144 msg.presetChannel = convertToPresetNumber(presetValue);
146 observer.presetInfoUpdated(msg);
149 private int convertToPresetNumber(String presetValue) {
150 if (!presetValue.isEmpty()) {
151 if (presetValue.chars().allMatch(Character::isDigit)) {
152 return Integer.parseInt(presetValue);
154 // special handling for RX-V3900, where 'A1' becomes 101 and 'B2' becomes 202 preset
155 if (presetValue.length() >= 2) {
156 Character presetAlpha = presetValue.charAt(0);
157 if (Character.isLetter(presetAlpha) && Character.isUpperCase(presetAlpha)
158 && Character.isDigit(presetValue.charAt(1))) {
159 int presetNumber = Integer.parseInt(presetValue.substring(1));
160 return (PRESET_LETTERS.indexOf(presetAlpha) + 1) * 100 + presetNumber;
169 * Select a preset channel.
171 * @param presetChannel The preset position [1,40]
172 * @throws IOException
173 * @throws ReceivedMessageParseException
176 public void selectItemByPresetNumber(int presetChannel) throws IOException, ReceivedMessageParseException {
179 // special handling for RX-V3900, where 'A1' becomes 101 and 'B2' becomes 202 preset
180 if (presetChannel > 100) {
181 int presetNumber = presetChannel % 100;
182 char presetAlpha = PRESET_LETTERS.charAt(presetChannel / 100 - 1);
183 presetValue = Character.toString(presetAlpha) + presetNumber;
185 presetValue = Integer.toString(presetChannel);
188 String cmd = wrInput(preset.apply(presetValue));
189 comReference.get().send(cmd);