]> git.basschouten.com Git - openhab-addons.git/blob
5cbf39e9e4c680b5a3558fe349e7d277cdb835e0
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.yamahareceiver.internal.protocol.xml;
14
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.*;
19
20 import java.io.IOException;
21
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;
32
33 /**
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.
36  * <p>
37  * The XML nodes {@code <Play_Info>} and {@code <Play_Control>} are used.
38  * <p>
39  * Example:
40  * <p>
41  * InputWithPlayControl menu = new InputWithPlayControl("NET_RADIO", comObject);
42  * menu.goToPath(menuDir);
43  * menu.selectItem(stationName);
44  * <p>
45  * No state will be saved in here, but in {@link PlayInfoState} and
46  * {@link PresetInfoState} instead.
47  *
48  * @author David Graeff - Initial contribution
49  * @author Tomasz Maruszak - Spotify support, refactoring
50  */
51 public class InputWithPlayControlXML extends AbstractInputControlXML implements InputWithPlayControl {
52
53     private final PlayInfoStateListener observer;
54     private final YamahaBridgeConfig bridgeConfig;
55
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";
61
62     /**
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.
65      *
66      * @param inputID The input ID like USB or NET_RADIO.
67      * @param com The Yamaha communication object to send http requests.
68      */
69     public InputWithPlayControlXML(String inputID, AbstractConnection com, PlayInfoStateListener observer,
70             YamahaBridgeConfig bridgeConfig, DeviceInformationState deviceInformationState) {
71         super(LoggerFactory.getLogger(InputWithPlayControlXML.class), inputID, com, deviceInformationState);
72
73         this.observer = observer;
74         this.bridgeConfig = bridgeConfig;
75
76         this.applyModelVariations();
77     }
78
79     /**
80      * Apply command changes to ensure compatibility with all supported models
81      */
82     protected void applyModelVariations() {
83         if (inputFeatureDescriptor != null) {
84             // For RX-V3900
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);
88             }
89             // For RX-V3900
90             if (inputFeatureDescriptor.hasCommandEnding("Play_Control,Skip")) {
91                 // For RX-V3900 the command value is also different
92                 skipForwardValue = "Fwd";
93                 skipBackwardValue = "Rev";
94
95                 skipCmd = new CommandTemplate("<Play_Control><Skip>%s</Skip></Play_Control>");
96                 logger.debug("Input {} - adjusting command to: {}", inputElement, skipCmd);
97             }
98         }
99     }
100
101     /**
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().
104      *
105      * @throws IOException
106      * @throws ReceivedMessageParseException
107      */
108     @Override
109     public void play() throws IOException, ReceivedMessageParseException {
110         sendCommand(playCmd.apply("Play"));
111     }
112
113     /**
114      * Stop the currently playing content. Use start() to start again.
115      *
116      * @throws IOException
117      * @throws ReceivedMessageParseException
118      */
119     @Override
120     public void stop() throws IOException, ReceivedMessageParseException {
121         sendCommand(playCmd.apply("Stop"));
122     }
123
124     /**
125      * Pause the currently playing content. This is not available for streaming content like on NET_RADIO.
126      *
127      * @throws IOException
128      * @throws ReceivedMessageParseException
129      */
130     @Override
131     public void pause() throws IOException, ReceivedMessageParseException {
132         sendCommand(playCmd.apply("Pause"));
133     }
134
135     /**
136      * Skip forward. This is not available for streaming content like on NET_RADIO.
137      *
138      * @throws IOException
139      * @throws ReceivedMessageParseException
140      */
141     @Override
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);
145             return;
146         }
147         sendCommand(skipCmd.apply(">>|"));
148     }
149
150     /**
151      * Skip reverse. This is not available for streaming content like on NET_RADIO.
152      *
153      * @throws IOException
154      * @throws ReceivedMessageParseException
155      */
156     @Override
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);
160             return;
161         }
162         sendCommand(skipCmd.apply("|<<"));
163     }
164
165     /**
166      * Next track. This is not available for streaming content like on NET_RADIO.
167      *
168      * @throws IOException
169      * @throws ReceivedMessageParseException
170      */
171     @Override
172     public void nextTrack() throws IOException, ReceivedMessageParseException {
173         sendCommand(skipCmd.apply(skipForwardValue));
174     }
175
176     /**
177      * Previous track. This is not available for streaming content like on NET_RADIO.
178      *
179      * @throws IOException
180      * @throws ReceivedMessageParseException
181      */
182     @Override
183     public void previousTrack() throws IOException, ReceivedMessageParseException {
184         sendCommand(skipCmd.apply(skipBackwardValue));
185     }
186
187     /**
188      * Sends a playback command to the AVR. After command is invoked, the state is also being refreshed.
189      *
190      * @param command - the protocol level command name
191      * @throws IOException
192      * @throws ReceivedMessageParseException
193      */
194     private void sendCommand(String command) throws IOException, ReceivedMessageParseException {
195         comReference.get().send(wrInput(command));
196         update();
197     }
198
199     /**
200      * Updates the playback information
201      *
202      * @throws IOException
203      * @throws ReceivedMessageParseException
204      */
205     @Override
206     public void update() throws IOException, ReceivedMessageParseException {
207         if (observer == null) {
208             return;
209         }
210
211         // <YAMAHA_AV rsp="GET" RC="0">
212         // <Spotify>
213         // <Play_Info>
214         // <Feature_Availability>Ready</Feature_Availability>
215         // <Playback_Info>Play</Playback_Info>
216         // <Meta_Info>
217         // <Artist>Way Out West</Artist>
218         // <Album>Tuesday Maybe</Album>
219         // <Track>Tuesday Maybe</Track>
220         // </Meta_Info>
221         // <Album_ART>
222         // <URL>/YamahaRemoteControl/AlbumART/AlbumART3929.jpg</URL>
223         // <ID>39290</ID>
224         // <Format>JPEG</Format>
225         // </Album_ART>
226         // <Input_Logo>
227         // <URL_S>/YamahaRemoteControl/Logos/logo005.png</URL_S>
228         // <URL_M></URL_M>
229         // <URL_L></URL_L>
230         // </Input_Logo>
231         // </Play_Info>
232         // </Spotify>
233         // </YAMAHA_AV>
234
235         AbstractConnection con = comReference.get();
236         Node node = getResponse(con, wrInput(PLAYBACK_STATUS_CMD), inputElement);
237
238         PlayInfoState msg = new PlayInfoState();
239
240         msg.playbackMode = getNodeContentOrDefault(node, playCmd.getPath(), msg.playbackMode);
241
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");
251
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();
256
257         logger.trace("Playback: {}, Station: {}, Artist: {}, Album: {}, Song: {}, SongImageUrl: {}", msg.playbackMode,
258                 msg.station, msg.artist, msg.album, msg.song, msg.songImageUrl);
259
260         observer.playInfoUpdated(msg);
261     }
262 }