]> git.basschouten.com Git - openhab-addons.git/blob
29510a3bf8313bfc8a04cbfbf86992621b592252
[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 java.io.IOException;
16
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;
28
29 /**
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.
33  *
34  * The XML nodes {@code <List_Control>} and {@code <List_Info>} are used.
35  *
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
38  * in the future.
39  *
40  * Example:
41  *
42  * NavigationControl menu = new NavigationControl("NET_RADIO", comObject);
43  * menu.goToPath(menuDir);
44  * menu.selectItem(stationName);
45  *
46  * @author Dennis Frommknecht - Initial contribution
47  * @author David Graeff - Completely refactored class
48  * @author Tomasz Maruszak - Refactor
49  */
50 public class InputWithNavigationControlXML extends AbstractInputControlXML implements InputWithNavigationControl {
51
52     private final Logger logger = LoggerFactory.getLogger(InputWithNavigationControlXML.class);
53
54     public static final int MAX_PER_PAGE = 8;
55     private boolean useAlternativeBackToHomeCmd = false;
56
57     private NavigationControlState state;
58     private NavigationControlStateListener observer;
59
60     /**
61      * Create a NavigationControl object for altering menu positions and requesting current menu information.
62      *
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.
67      */
68     public InputWithNavigationControlXML(NavigationControlState state, String inputID, AbstractConnection con,
69             NavigationControlStateListener observer, DeviceInformationState deviceInformationState) {
70         super(LoggerFactory.getLogger(InputWithNavigationControlXML.class), inputID, con, deviceInformationState);
71
72         this.state = state;
73         this.observer = observer;
74     }
75
76     /**
77      * Sends a cursor command to Yamaha.
78      *
79      * @param command
80      * @throws IOException
81      * @throws ReceivedMessageParseException
82      */
83     private void navigateCursor(String command) throws IOException, ReceivedMessageParseException {
84         comReference.get().send(wrInput("<List_Control><Cursor>" + command + "</Cursor></List_Control>"));
85         update();
86     }
87
88     /**
89      * Navigate back
90      *
91      * @throws IOException
92      * @throws ReceivedMessageParseException
93      */
94     @Override
95     public void goBack() throws IOException, ReceivedMessageParseException {
96         navigateCursor("Back");
97     }
98
99     /**
100      * Navigate up
101      *
102      * @throws IOException
103      * @throws ReceivedMessageParseException
104      */
105     @Override
106     public void goUp() throws IOException, ReceivedMessageParseException {
107         navigateCursor("Up");
108     }
109
110     /**
111      * Navigate down
112      *
113      * @throws IOException
114      * @throws ReceivedMessageParseException
115      */
116     @Override
117     public void goDown() throws IOException, ReceivedMessageParseException {
118         navigateCursor("Down");
119     }
120
121     /**
122      * Navigate left. Not for all zones or functions available.
123      *
124      * @throws IOException
125      * @throws ReceivedMessageParseException
126      */
127     @Override
128     public void goLeft() throws IOException, ReceivedMessageParseException {
129         navigateCursor("Left");
130     }
131
132     /**
133      * Navigate right. Not for all zones or functions available.
134      *
135      * @throws IOException
136      * @throws ReceivedMessageParseException
137      */
138     @Override
139     public void goRight() throws IOException, ReceivedMessageParseException {
140         navigateCursor("Right");
141     }
142
143     /**
144      * Select current item. Not for all zones or functions available.
145      *
146      * @throws IOException
147      * @throws ReceivedMessageParseException
148      */
149     @Override
150     public void selectCurrentItem() throws IOException, ReceivedMessageParseException {
151         navigateCursor("Select");
152     }
153
154     /**
155      * Navigate to root menu
156      *
157      * @throws IOException
158      * @throws ReceivedMessageParseException
159      */
160     @Override
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!");
166                 return false;
167             }
168         } else {
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;
174                 return goToRoot();
175             }
176         }
177         return true;
178     }
179
180     @Override
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>"));
184         update();
185     }
186
187     @Override
188     public void selectItemFullPath(String fullPath) throws IOException, ReceivedMessageParseException {
189         update();
190
191         if (state.menuName == null) {
192             return;
193         }
194
195         String[] pathArr = fullPath.split("/");
196
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);
201             }
202             return;
203         }
204
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;
209
210         boolean sameMenu = state.menuName.equals(selectMenuName) && state.menuLayer == selectMenuLevel;
211
212         if (sameMenu) {
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());
216             }
217             return;
218         }
219
220         if (state.menuLayer > 0) {
221             if (!goToRoot()) {
222                 return;
223             }
224         }
225
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());
230                 return;
231             }
232         }
233     }
234
235     /**
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.
238      *
239      * @return Return the item index [1,8] or -1 if not found.
240      */
241     private int findItemOnCurrentPage(String itemName) {
242         for (int i = 0; i < MAX_PER_PAGE; i++) {
243             if (itemName.equals(state.items[i])) {
244                 return i + 1;
245             }
246         }
247
248         return -1;
249     }
250
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);
254
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;
259
260             if (currentPage != realPage) {
261                 goToPage(pageIndex);
262             }
263
264             int index = findItemOnCurrentPage(name);
265             if (index > 0) {
266                 com.send(wrInput("<List_Control><Direct_Sel>Line_" + index + "</Direct_Sel></List_Control>"));
267                 update();
268                 return true;
269             }
270         }
271
272         return false;
273     }
274
275     /**
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.
279      *
280      * @throws IOException
281      * @throws ReceivedMessageParseException
282      */
283     @Override
284     public void update() throws IOException, ReceivedMessageParseException {
285         int totalWaitingTime = 0;
286
287         Document doc;
288         Node currentMenu;
289
290         AbstractConnection com = comReference.get();
291         while (true) {
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);
296             }
297
298             currentMenu = XMLUtils.getNodeOrFail(doc.getFirstChild(), "List_Info");
299
300             Node nodeMenuState = XMLUtils.getNode(currentMenu, "Menu_Status");
301             if (nodeMenuState == null || "Ready".equals(nodeMenuState.getTextContent())) {
302                 break;
303             }
304
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.
311                 return;
312             }
313
314             try {
315                 Thread.sleep(YamahaReceiverBindingConstants.MENU_RETRY_DELAY);
316             } catch (InterruptedException e) {
317                 Thread.currentThread().interrupt();
318                 throw new RuntimeException(e);
319             }
320         }
321
322         state.clearItems();
323
324         Node node = XMLUtils.getNodeOrFail(currentMenu, "Menu_Name");
325         state.menuName = node.getTextContent();
326
327         node = XMLUtils.getNodeOrFail(currentMenu, "Menu_Layer");
328         state.menuLayer = Integer.parseInt(node.getTextContent()) - 1;
329
330         node = XMLUtils.getNodeOrFail(currentMenu, "Cursor_Position/Current_Line");
331         int currentLine = Integer.parseInt(node.getTextContent());
332         state.currentLine = currentLine;
333
334         node = XMLUtils.getNodeOrFail(currentMenu, "Cursor_Position/Max_Line");
335         int maxLines = Integer.parseInt(node.getTextContent());
336         state.maxLine = maxLines;
337
338         for (int i = 1; i < 8; ++i) {
339             state.items[i - 1] = XMLUtils.getNodeContentOrDefault(currentMenu, "Current_List/Line_" + i + "/Txt",
340                     (String) null);
341         }
342
343         if (observer != null) {
344             observer.navigationUpdated(state);
345         }
346     }
347 }