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 java.io.IOException;
17 import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants;
18 import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
19 import org.openhab.binding.yamahareceiver.internal.protocol.InputWithNavigationControl;
20 import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
21 import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
22 import org.openhab.binding.yamahareceiver.internal.state.NavigationControlState;
23 import org.openhab.binding.yamahareceiver.internal.state.NavigationControlStateListener;
24 import org.slf4j.Logger;
25 import org.slf4j.LoggerFactory;
26 import org.w3c.dom.Document;
27 import org.w3c.dom.Node;
30 * This class implements the Yamaha Receiver protocol related to navigation functionally. USB, NET_RADIO, IPOD and
31 * other inputs are using the same way of navigating through menus. A menu on Yamaha AVRs
32 * is hierarchically organised. Entries are divided into pages with 8 elements per page.
34 * The XML nodes {@code <List_Control>} and {@code <List_Info>} are used.
36 * In contrast to other protocol classes an object of this type will store state information,
37 * because it caches the received XML information of the updateNavigationState(). This may change
42 * NavigationControl menu = new NavigationControl("NET_RADIO", comObject);
43 * menu.goToPath(menuDir);
44 * menu.selectItem(stationName);
46 * @author Dennis Frommknecht - Initial contribution
47 * @author David Graeff - Completely refactored class
48 * @author Tomasz Maruszak - Refactor
50 public class InputWithNavigationControlXML extends AbstractInputControlXML implements InputWithNavigationControl {
52 private final Logger logger = LoggerFactory.getLogger(InputWithNavigationControlXML.class);
54 public static final int MAX_PER_PAGE = 8;
55 private boolean useAlternativeBackToHomeCmd = false;
57 private NavigationControlState state;
58 private NavigationControlStateListener observer;
61 * Create a NavigationControl object for altering menu positions and requesting current menu information.
63 * @param state We need the current navigation state, because most navigation commands are relative commands and we
64 * offer API with absolute values.
65 * @param inputID The input ID like USB or NET_RADIO.
66 * @param con The Yamaha communication object to send http requests.
68 public InputWithNavigationControlXML(NavigationControlState state, String inputID, AbstractConnection con,
69 NavigationControlStateListener observer, DeviceInformationState deviceInformationState) {
70 super(LoggerFactory.getLogger(InputWithNavigationControlXML.class), inputID, con, deviceInformationState);
73 this.observer = observer;
77 * Sends a cursor command to Yamaha.
81 * @throws ReceivedMessageParseException
83 private void navigateCursor(String command) throws IOException, ReceivedMessageParseException {
84 comReference.get().send(wrInput("<List_Control><Cursor>" + command + "</Cursor></List_Control>"));
92 * @throws ReceivedMessageParseException
95 public void goBack() throws IOException, ReceivedMessageParseException {
96 navigateCursor("Back");
102 * @throws IOException
103 * @throws ReceivedMessageParseException
106 public void goUp() throws IOException, ReceivedMessageParseException {
107 navigateCursor("Up");
113 * @throws IOException
114 * @throws ReceivedMessageParseException
117 public void goDown() throws IOException, ReceivedMessageParseException {
118 navigateCursor("Down");
122 * Navigate left. Not for all zones or functions available.
124 * @throws IOException
125 * @throws ReceivedMessageParseException
128 public void goLeft() throws IOException, ReceivedMessageParseException {
129 navigateCursor("Left");
133 * Navigate right. Not for all zones or functions available.
135 * @throws IOException
136 * @throws ReceivedMessageParseException
139 public void goRight() throws IOException, ReceivedMessageParseException {
140 navigateCursor("Right");
144 * Select current item. Not for all zones or functions available.
146 * @throws IOException
147 * @throws ReceivedMessageParseException
150 public void selectCurrentItem() throws IOException, ReceivedMessageParseException {
151 navigateCursor("Select");
155 * Navigate to root menu
157 * @throws IOException
158 * @throws ReceivedMessageParseException
161 public boolean goToRoot() throws IOException, ReceivedMessageParseException {
162 if (useAlternativeBackToHomeCmd) {
163 navigateCursor("Return to Home");
164 if (state.menuLayer > 0) {
165 observer.navigationError("Both going back to root commands failed for your receiver!");
169 navigateCursor("Back to Home");
170 if (state.menuLayer > 0) {
171 observer.navigationError(
172 "The going back to root command failed for your receiver. Trying to use a different command.");
173 useAlternativeBackToHomeCmd = true;
181 public void goToPage(int page) throws IOException, ReceivedMessageParseException {
182 int line = (page - 1) * 8 + 1;
183 comReference.get().send(wrInput("<List_Control><Jump_Line>" + line + "</Jump_Line></List_Control>"));
188 public void selectItemFullPath(String fullPath) throws IOException, ReceivedMessageParseException {
191 if (state.menuName == null) {
195 String[] pathArr = fullPath.split("/");
197 // Just a relative menu item.
198 if (pathArr.length < 2) {
199 if (!selectItem(pathArr[0])) {
200 observer.navigationError("Item '" + pathArr[0] + "' doesn't exist in menu " + state.menuName);
205 // Full path info not available, so guess from last path element and number of path elements
206 String selectMenuName = pathArr[pathArr.length - 2];
207 String selectItemName = pathArr[pathArr.length - 1];
208 int selectMenuLevel = pathArr.length - 1;
210 boolean sameMenu = state.menuName.equals(selectMenuName) && state.menuLayer == selectMenuLevel;
213 if (!selectItem(selectItemName)) {
214 observer.navigationError("Item '" + selectItemName + "' doesn't exist in menu " + state.menuName
215 + " at level " + state.menuLayer + ". Available options are: " + state.getAllItemLabels());
220 if (state.menuLayer > 0) {
226 for (String pathElement : pathArr) {
227 if (!selectItem(pathElement)) {
228 observer.navigationError("Item '" + pathElement + "' doesn't exist in menu " + state.menuName
229 + " at level " + state.menuLayer + ". Available options are: " + state.getAllItemLabels());
236 * Finds an item on the current page. A page contains up to 8 items.
237 * Operates on a cached XML node! Call refreshMenuState for up-to-date information.
239 * @return Return the item index [1,8] or -1 if not found.
241 private int findItemOnCurrentPage(String itemName) {
242 for (int i = 0; i < MAX_PER_PAGE; i++) {
243 if (itemName.equals(state.items[i])) {
251 private boolean selectItem(String name) throws IOException, ReceivedMessageParseException {
252 final int pageCount = (int) Math.ceil(state.maxLine / (double) MAX_PER_PAGE);
253 final int currentPage = (int) Math.floor((state.currentLine - 1) / (double) MAX_PER_PAGE);
255 AbstractConnection com = comReference.get();
256 for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
257 // Start with the current page and then go to the end and start at page 1 again
258 int realPage = (currentPage + pageIndex) % pageCount;
260 if (currentPage != realPage) {
264 int index = findItemOnCurrentPage(name);
266 com.send(wrInput("<List_Control><Direct_Sel>Line_" + index + "</Direct_Sel></List_Control>"));
276 * Refreshes the menu state and caches the List_Info node from the response. This method may take
277 * some time because it retries the request for up to MENU_MAX_WAITING_TIME or the menu state reports
278 * "Ready", whatever comes first.
280 * @throws IOException
281 * @throws ReceivedMessageParseException
284 public void update() throws IOException, ReceivedMessageParseException {
285 int totalWaitingTime = 0;
290 AbstractConnection com = comReference.get();
292 String response = com.sendReceive(wrInput("<List_Info>GetParam</List_Info>"));
293 doc = XMLUtils.xml(response);
294 if (doc.getFirstChild() == null) {
295 throw new ReceivedMessageParseException("<List_Info>GetParam failed: " + response);
298 currentMenu = XMLUtils.getNodeOrFail(doc.getFirstChild(), "List_Info");
300 Node nodeMenuState = XMLUtils.getNode(currentMenu, "Menu_Status");
301 if (nodeMenuState == null || "Ready".equals(nodeMenuState.getTextContent())) {
305 totalWaitingTime += YamahaReceiverBindingConstants.MENU_RETRY_DELAY;
306 if (totalWaitingTime > YamahaReceiverBindingConstants.MENU_MAX_WAITING_TIME) {
307 logger.info("Menu still not ready after " + YamahaReceiverBindingConstants.MENU_MAX_WAITING_TIME
308 + "ms. The menu state will be out of sync.");
309 // ToDo: this needs to redesigned to allow for some sort of async update
310 // Note: there is not really that much we can do here.
315 Thread.sleep(YamahaReceiverBindingConstants.MENU_RETRY_DELAY);
316 } catch (InterruptedException e) {
317 Thread.currentThread().interrupt();
318 throw new RuntimeException(e);
324 Node node = XMLUtils.getNodeOrFail(currentMenu, "Menu_Name");
325 state.menuName = node.getTextContent();
327 node = XMLUtils.getNodeOrFail(currentMenu, "Menu_Layer");
328 state.menuLayer = Integer.parseInt(node.getTextContent()) - 1;
330 node = XMLUtils.getNodeOrFail(currentMenu, "Cursor_Position/Current_Line");
331 int currentLine = Integer.parseInt(node.getTextContent());
332 state.currentLine = currentLine;
334 node = XMLUtils.getNodeOrFail(currentMenu, "Cursor_Position/Max_Line");
335 int maxLines = Integer.parseInt(node.getTextContent());
336 state.maxLine = maxLines;
338 for (int i = 1; i < 8; ++i) {
339 state.items[i - 1] = XMLUtils.getNodeContentOrDefault(currentMenu, "Current_List/Line_" + i + "/Txt",
343 if (observer != null) {
344 observer.navigationUpdated(state);