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.YamahaReceiverBindingConstants.Inputs.INPUT_SPOTIFY;
16 import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLConstants.Commands.PLAYBACK_STATUS_CMD;
17 import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLProtocolService.getResponse;
18 import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLUtils.*;
20 import java.io.IOException;
22 import org.openhab.binding.yamahareceiver.internal.config.YamahaBridgeConfig;
23 import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
24 import org.openhab.binding.yamahareceiver.internal.protocol.InputWithPlayControl;
25 import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
26 import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
27 import org.openhab.binding.yamahareceiver.internal.state.PlayInfoState;
28 import org.openhab.binding.yamahareceiver.internal.state.PlayInfoStateListener;
29 import org.openhab.binding.yamahareceiver.internal.state.PresetInfoState;
30 import org.slf4j.LoggerFactory;
31 import org.w3c.dom.Node;
34 * This class implements the Yamaha Receiver protocol related to navigation functionally. USB, NET_RADIO, IPOD and
35 * other inputs are using the same way of playback control.
37 * The XML nodes {@code <Play_Info>} and {@code <Play_Control>} are used.
41 * InputWithPlayControl menu = new InputWithPlayControl("NET_RADIO", comObject);
42 * menu.goToPath(menuDir);
43 * menu.selectItem(stationName);
45 * No state will be saved in here, but in {@link PlayInfoState} and
46 * {@link PresetInfoState} instead.
48 * @author David Graeff - Initial contribution
49 * @author Tomasz Maruszak - Spotify support, refactoring
51 public class InputWithPlayControlXML extends AbstractInputControlXML implements InputWithPlayControl {
53 private final PlayInfoStateListener observer;
54 private final YamahaBridgeConfig bridgeConfig;
56 protected CommandTemplate playCmd = new CommandTemplate("<Play_Control><Playback>%s</Playback></Play_Control>",
57 "Play_Info/Playback_Info");
58 protected CommandTemplate skipCmd = new CommandTemplate("<Play_Control><Playback>%s</Playback></Play_Control>");
59 protected String skipForwardValue = "Skip Fwd";
60 protected String skipBackwardValue = "Skip Rev";
63 * Create an InputWithPlayControl object for altering menu positions and requesting current menu information as well
64 * as controlling the playback and choosing a preset item.
66 * @param inputID The input ID like USB or NET_RADIO.
67 * @param com The Yamaha communication object to send http requests.
69 public InputWithPlayControlXML(String inputID, AbstractConnection com, PlayInfoStateListener observer,
70 YamahaBridgeConfig bridgeConfig, DeviceInformationState deviceInformationState) {
71 super(LoggerFactory.getLogger(InputWithPlayControlXML.class), inputID, com, deviceInformationState);
73 this.observer = observer;
74 this.bridgeConfig = bridgeConfig;
76 this.applyModelVariations();
80 * Apply command changes to ensure compatibility with all supported models
82 protected void applyModelVariations() {
83 if (inputFeatureDescriptor != null) {
85 if (inputFeatureDescriptor.hasCommandEnding("Play_Control,Play")) {
86 playCmd = new CommandTemplate("<Play_Control><Play>%s</Play></Play_Control>", "Play_Info/Status");
87 logger.debug("Input {} - adjusting command to: {}", inputElement, playCmd);
90 if (inputFeatureDescriptor.hasCommandEnding("Play_Control,Skip")) {
91 // For RX-V3900 the command value is also different
92 skipForwardValue = "Fwd";
93 skipBackwardValue = "Rev";
95 skipCmd = new CommandTemplate("<Play_Control><Skip>%s</Skip></Play_Control>");
96 logger.debug("Input {} - adjusting command to: {}", inputElement, skipCmd);
102 * Start the playback of the content which is usually selected by the means of the Navigation control class or
103 * which has been stopped by stop().
105 * @throws IOException
106 * @throws ReceivedMessageParseException
109 public void play() throws IOException, ReceivedMessageParseException {
110 sendCommand(playCmd.apply("Play"));
114 * Stop the currently playing content. Use start() to start again.
116 * @throws IOException
117 * @throws ReceivedMessageParseException
120 public void stop() throws IOException, ReceivedMessageParseException {
121 sendCommand(playCmd.apply("Stop"));
125 * Pause the currently playing content. This is not available for streaming content like on NET_RADIO.
127 * @throws IOException
128 * @throws ReceivedMessageParseException
131 public void pause() throws IOException, ReceivedMessageParseException {
132 sendCommand(playCmd.apply("Pause"));
136 * Skip forward. This is not available for streaming content like on NET_RADIO.
138 * @throws IOException
139 * @throws ReceivedMessageParseException
142 public void skipFF() throws IOException, ReceivedMessageParseException {
143 if (INPUT_SPOTIFY.equals(inputID)) {
144 logger.warn("Command skip forward is not supported for input {}", inputID);
147 sendCommand(skipCmd.apply(">>|"));
151 * Skip reverse. This is not available for streaming content like on NET_RADIO.
153 * @throws IOException
154 * @throws ReceivedMessageParseException
157 public void skipREV() throws IOException, ReceivedMessageParseException {
158 if (INPUT_SPOTIFY.equals(inputID)) {
159 logger.warn("Command skip reverse is not supported for input {}", inputID);
162 sendCommand(skipCmd.apply("|<<"));
166 * Next track. This is not available for streaming content like on NET_RADIO.
168 * @throws IOException
169 * @throws ReceivedMessageParseException
172 public void nextTrack() throws IOException, ReceivedMessageParseException {
173 sendCommand(skipCmd.apply(skipForwardValue));
177 * Previous track. This is not available for streaming content like on NET_RADIO.
179 * @throws IOException
180 * @throws ReceivedMessageParseException
183 public void previousTrack() throws IOException, ReceivedMessageParseException {
184 sendCommand(skipCmd.apply(skipBackwardValue));
188 * Sends a playback command to the AVR. After command is invoked, the state is also being refreshed.
190 * @param command - the protocol level command name
191 * @throws IOException
192 * @throws ReceivedMessageParseException
194 private void sendCommand(String command) throws IOException, ReceivedMessageParseException {
195 comReference.get().send(wrInput(command));
200 * Updates the playback information
202 * @throws IOException
203 * @throws ReceivedMessageParseException
206 public void update() throws IOException, ReceivedMessageParseException {
207 if (observer == null) {
211 // <YAMAHA_AV rsp="GET" RC="0">
214 // <Feature_Availability>Ready</Feature_Availability>
215 // <Playback_Info>Play</Playback_Info>
217 // <Artist>Way Out West</Artist>
218 // <Album>Tuesday Maybe</Album>
219 // <Track>Tuesday Maybe</Track>
222 // <URL>/YamahaRemoteControl/AlbumART/AlbumART3929.jpg</URL>
224 // <Format>JPEG</Format>
227 // <URL_S>/YamahaRemoteControl/Logos/logo005.png</URL_S>
235 AbstractConnection con = comReference.get();
236 Node node = getResponse(con, wrInput(PLAYBACK_STATUS_CMD), inputElement);
238 PlayInfoState msg = new PlayInfoState();
240 msg.playbackMode = getNodeContentOrDefault(node, playCmd.getPath(), msg.playbackMode);
242 // elements for these are named differently per model and per input, so we try to match any known element
243 msg.station = getAnyNodeContentOrDefault(node, msg.station, "Play_Info/Meta_Info/Radio_Text_A",
244 "Play_Info/Meta_Info/Station", "Play_Info/RDS/Program_Service");
245 msg.artist = getAnyNodeContentOrDefault(node, msg.artist, "Play_Info/Meta_Info/Artist",
246 "Play_Info/Title/Artist", "Play_Info/RDS/Radio_Text_A");
247 msg.album = getAnyNodeContentOrDefault(node, msg.album, "Play_Info/Meta_Info/Album", "Play_Info/Title/Album",
248 "Play_Info/RDS/Program_Type");
249 msg.song = getAnyNodeContentOrDefault(node, msg.song, "Play_Info/Meta_Info/Track", "Play_Info/Meta_Info/Song",
250 "Play_Info/Title/Song", "Play_Info/RDS/Radio_Text_B");
252 // Spotify and NET RADIO input supports song cover image (at least on RX-S601D)
253 String songImageUrl = getNodeContentOrEmpty(node, "Play_Info/Album_ART/URL");
254 msg.songImageUrl = !songImageUrl.isEmpty() ? String.format("http://%s%s", con.getHost(), songImageUrl)
255 : bridgeConfig.getAlbumUrl();
257 logger.trace("Playback: {}, Station: {}, Artist: {}, Album: {}, Song: {}, SongImageUrl: {}", msg.playbackMode,
258 msg.station, msg.artist, msg.album, msg.song, msg.songImageUrl);
260 observer.playInfoUpdated(msg);