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 <List_Control> and <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>"));
94 public void goBack() throws IOException, ReceivedMessageParseException {
95 navigateCursor("Back");
104 public void goUp() throws IOException, ReceivedMessageParseException {
105 navigateCursor("Up");
114 public void goDown() throws IOException, ReceivedMessageParseException {
115 navigateCursor("Down");
119 * Navigate left. Not for all zones or functions available.
124 public void goLeft() throws IOException, ReceivedMessageParseException {
125 navigateCursor("Left");
129 * Navigate right. Not for all zones or functions available.
134 public void goRight() throws IOException, ReceivedMessageParseException {
135 navigateCursor("Right");
139 * Select current item. Not for all zones or functions available.
144 public void selectCurrentItem() throws IOException, ReceivedMessageParseException {
145 navigateCursor("Select");
149 * Navigate to root menu
154 public boolean goToRoot() throws IOException, ReceivedMessageParseException {
155 if (useAlternativeBackToHomeCmd) {
156 navigateCursor("Return to Home");
157 if (state.menuLayer > 0) {
158 observer.navigationError("Both going back to root commands failed for your receiver!");
162 navigateCursor("Back to Home");
163 if (state.menuLayer > 0) {
164 observer.navigationError(
165 "The going back to root command failed for your receiver. Trying to use a different command.");
166 useAlternativeBackToHomeCmd = true;
174 public void goToPage(int page) throws IOException, ReceivedMessageParseException {
175 int line = (page - 1) * 8 + 1;
176 comReference.get().send(wrInput("<List_Control><Jump_Line>" + line + "</Jump_Line></List_Control>"));
181 public void selectItemFullPath(String fullPath) throws IOException, ReceivedMessageParseException {
184 if (state.menuName == null) {
188 String[] pathArr = fullPath.split("/");
190 // Just a relative menu item.
191 if (pathArr.length < 2) {
192 if (!selectItem(pathArr[0])) {
193 observer.navigationError("Item '" + pathArr[0] + "' doesn't exist in menu " + state.menuName);
198 // Full path info not available, so guess from last path element and number of path elements
199 String selectMenuName = pathArr[pathArr.length - 2];
200 String selectItemName = pathArr[pathArr.length - 1];
201 int selectMenuLevel = pathArr.length - 1;
203 boolean sameMenu = state.menuName.equals(selectMenuName) && state.menuLayer == selectMenuLevel;
206 if (!selectItem(selectItemName)) {
207 observer.navigationError("Item '" + selectItemName + "' doesn't exist in menu " + state.menuName
208 + " at level " + state.menuLayer + ". Available options are: " + state.getAllItemLabels());
213 if (state.menuLayer > 0) {
219 for (String pathElement : pathArr) {
220 if (!selectItem(pathElement)) {
221 observer.navigationError("Item '" + pathElement + "' doesn't exist in menu " + state.menuName
222 + " at level " + state.menuLayer + ". Available options are: " + state.getAllItemLabels());
229 * Finds an item on the current page. A page contains up to 8 items.
230 * Operates on a cached XML node! Call refreshMenuState for up-to-date information.
232 * @return Return the item index [1,8] or -1 if not found.
234 private int findItemOnCurrentPage(String itemName) {
235 for (int i = 0; i < MAX_PER_PAGE; i++) {
236 if (itemName.equals(state.items[i])) {
244 private boolean selectItem(String name) throws IOException, ReceivedMessageParseException {
245 final int pageCount = (int) Math.ceil(state.maxLine / (double) MAX_PER_PAGE);
246 final int currentPage = (int) Math.floor((state.currentLine - 1) / (double) MAX_PER_PAGE);
248 AbstractConnection com = comReference.get();
249 for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
250 // Start with the current page and then go to the end and start at page 1 again
251 int realPage = (currentPage + pageIndex) % pageCount;
253 if (currentPage != realPage) {
257 int index = findItemOnCurrentPage(name);
259 com.send(wrInput("<List_Control><Direct_Sel>Line_" + index + "</Direct_Sel></List_Control>"));
269 * Refreshes the menu state and caches the List_Info node from the response. This method may take
270 * some time because it retries the request for up to MENU_MAX_WAITING_TIME or the menu state reports
271 * "Ready", whatever comes first.
276 public void update() throws IOException, ReceivedMessageParseException {
277 int totalWaitingTime = 0;
282 AbstractConnection com = comReference.get();
284 String response = com.sendReceive(wrInput("<List_Info>GetParam</List_Info>"));
285 doc = XMLUtils.xml(response);
286 if (doc.getFirstChild() == null) {
287 throw new ReceivedMessageParseException("<List_Info>GetParam failed: " + response);
290 currentMenu = XMLUtils.getNodeOrFail(doc.getFirstChild(), "List_Info");
292 Node nodeMenuState = XMLUtils.getNode(currentMenu, "Menu_Status");
293 if (nodeMenuState == null || "Ready".equals(nodeMenuState.getTextContent())) {
297 totalWaitingTime += YamahaReceiverBindingConstants.MENU_RETRY_DELAY;
298 if (totalWaitingTime > YamahaReceiverBindingConstants.MENU_MAX_WAITING_TIME) {
299 logger.info("Menu still not ready after " + YamahaReceiverBindingConstants.MENU_MAX_WAITING_TIME
300 + "ms. The menu state will be out of sync.");
301 // ToDo: this needs to redesigned to allow for some sort of async update
302 // Note: there is not really that much we can do here.
307 Thread.sleep(YamahaReceiverBindingConstants.MENU_RETRY_DELAY);
308 } catch (InterruptedException e) {
309 Thread.currentThread().interrupt();
310 throw new RuntimeException(e);
316 Node node = XMLUtils.getNodeOrFail(currentMenu, "Menu_Name");
317 state.menuName = node.getTextContent();
319 node = XMLUtils.getNodeOrFail(currentMenu, "Menu_Layer");
320 state.menuLayer = Integer.parseInt(node.getTextContent()) - 1;
322 node = XMLUtils.getNodeOrFail(currentMenu, "Cursor_Position/Current_Line");
323 int currentLine = Integer.parseInt(node.getTextContent());
324 state.currentLine = currentLine;
326 node = XMLUtils.getNodeOrFail(currentMenu, "Cursor_Position/Max_Line");
327 int maxLines = Integer.parseInt(node.getTextContent());
328 state.maxLine = maxLines;
330 for (int i = 1; i < 8; ++i) {
331 state.items[i - 1] = XMLUtils.getNodeContentOrDefault(currentMenu, "Current_List/Line_" + i + "/Txt",
335 if (observer != null) {
336 observer.navigationUpdated(state);