]> git.basschouten.com Git - openhab-addons.git/blob
e4756630e7f16dc0f65936a039095ab3ef95b17f
[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 <List_Control> and <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 Exception
92      */
93     @Override
94     public void goBack() throws IOException, ReceivedMessageParseException {
95         navigateCursor("Back");
96     }
97
98     /**
99      * Navigate up
100      *
101      * @throws Exception
102      */
103     @Override
104     public void goUp() throws IOException, ReceivedMessageParseException {
105         navigateCursor("Up");
106     }
107
108     /**
109      * Navigate down
110      *
111      * @throws Exception
112      */
113     @Override
114     public void goDown() throws IOException, ReceivedMessageParseException {
115         navigateCursor("Down");
116     }
117
118     /**
119      * Navigate left. Not for all zones or functions available.
120      *
121      * @throws Exception
122      */
123     @Override
124     public void goLeft() throws IOException, ReceivedMessageParseException {
125         navigateCursor("Left");
126     }
127
128     /**
129      * Navigate right. Not for all zones or functions available.
130      *
131      * @throws Exception
132      */
133     @Override
134     public void goRight() throws IOException, ReceivedMessageParseException {
135         navigateCursor("Right");
136     }
137
138     /**
139      * Select current item. Not for all zones or functions available.
140      *
141      * @throws Exception
142      */
143     @Override
144     public void selectCurrentItem() throws IOException, ReceivedMessageParseException {
145         navigateCursor("Select");
146     }
147
148     /**
149      * Navigate to root menu
150      *
151      * @throws Exception
152      */
153     @Override
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!");
159                 return false;
160             }
161         } else {
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;
167                 return goToRoot();
168             }
169         }
170         return true;
171     }
172
173     @Override
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>"));
177         update();
178     }
179
180     @Override
181     public void selectItemFullPath(String fullPath) throws IOException, ReceivedMessageParseException {
182         update();
183
184         if (state.menuName == null) {
185             return;
186         }
187
188         String[] pathArr = fullPath.split("/");
189
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);
194             }
195             return;
196         }
197
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;
202
203         boolean sameMenu = state.menuName.equals(selectMenuName) && state.menuLayer == selectMenuLevel;
204
205         if (sameMenu) {
206             if (!selectItem(selectItemName)) {
207                 observer.navigationError("Item '" + selectItemName + "' doesn't exist in menu " + state.menuName
208                         + " at level " + String.valueOf(state.menuLayer) + ". Available options are: "
209                         + state.getAllItemLabels());
210             }
211             return;
212         }
213
214         if (state.menuLayer > 0) {
215             if (!goToRoot()) {
216                 return;
217             }
218         }
219
220         for (String pathElement : pathArr) {
221             if (!selectItem(pathElement)) {
222                 observer.navigationError("Item '" + pathElement + "' doesn't exist in menu " + state.menuName
223                         + " at level " + String.valueOf(state.menuLayer) + ". Available options are: "
224                         + state.getAllItemLabels());
225                 return;
226             }
227         }
228     }
229
230     /**
231      * Finds an item on the current page. A page contains up to 8 items.
232      * Operates on a cached XML node! Call refreshMenuState for up-to-date information.
233      *
234      * @return Return the item index [1,8] or -1 if not found.
235      */
236     private int findItemOnCurrentPage(String itemName) {
237         for (int i = 0; i < MAX_PER_PAGE; i++) {
238             if (itemName.equals(state.items[i])) {
239                 return i + 1;
240             }
241         }
242
243         return -1;
244     }
245
246     private boolean selectItem(String name) throws IOException, ReceivedMessageParseException {
247         final int pageCount = (int) Math.ceil(state.maxLine / (double) MAX_PER_PAGE);
248         final int currentPage = (int) Math.floor((state.currentLine - 1) / (double) MAX_PER_PAGE);
249
250         AbstractConnection com = comReference.get();
251         for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
252             // Start with the current page and then go to the end and start at page 1 again
253             int realPage = (currentPage + pageIndex) % pageCount;
254
255             if (currentPage != realPage) {
256                 goToPage(pageIndex);
257             }
258
259             int index = findItemOnCurrentPage(name);
260             if (index > 0) {
261                 com.send(wrInput(
262                         "<List_Control><Direct_Sel>Line_" + String.valueOf(index) + "</Direct_Sel></List_Control>"));
263                 update();
264                 return true;
265             }
266         }
267
268         return false;
269     }
270
271     /**
272      * Refreshes the menu state and caches the List_Info node from the response. This method may take
273      * some time because it retries the request for up to MENU_MAX_WAITING_TIME or the menu state reports
274      * "Ready", whatever comes first.
275      *
276      * @throws Exception
277      */
278     @Override
279     public void update() throws IOException, ReceivedMessageParseException {
280         int totalWaitingTime = 0;
281
282         Document doc;
283         Node currentMenu;
284
285         AbstractConnection com = comReference.get();
286         while (true) {
287             String response = com.sendReceive(wrInput("<List_Info>GetParam</List_Info>"));
288             doc = XMLUtils.xml(response);
289             if (doc.getFirstChild() == null) {
290                 throw new ReceivedMessageParseException("<List_Info>GetParam failed: " + response);
291             }
292
293             currentMenu = XMLUtils.getNodeOrFail(doc.getFirstChild(), "List_Info");
294
295             Node nodeMenuState = XMLUtils.getNode(currentMenu, "Menu_Status");
296             if (nodeMenuState == null || "Ready".equals(nodeMenuState.getTextContent())) {
297                 break;
298             }
299
300             totalWaitingTime += YamahaReceiverBindingConstants.MENU_RETRY_DELAY;
301             if (totalWaitingTime > YamahaReceiverBindingConstants.MENU_MAX_WAITING_TIME) {
302                 logger.info("Menu still not ready after " + YamahaReceiverBindingConstants.MENU_MAX_WAITING_TIME
303                         + "ms. The menu state will be out of sync.");
304                 // ToDo: this needs to redesigned to allow for some sort of async update
305                 // Note: there is not really that much we can do here.
306                 return;
307             }
308
309             try {
310                 Thread.sleep(YamahaReceiverBindingConstants.MENU_RETRY_DELAY);
311             } catch (InterruptedException e) {
312                 Thread.currentThread().interrupt();
313                 throw new RuntimeException(e);
314             }
315         }
316
317         state.clearItems();
318
319         Node node = XMLUtils.getNodeOrFail(currentMenu, "Menu_Name");
320         state.menuName = node.getTextContent();
321
322         node = XMLUtils.getNodeOrFail(currentMenu, "Menu_Layer");
323         state.menuLayer = Integer.parseInt(node.getTextContent()) - 1;
324
325         node = XMLUtils.getNodeOrFail(currentMenu, "Cursor_Position/Current_Line");
326         int currentLine = Integer.parseInt(node.getTextContent());
327         state.currentLine = currentLine;
328
329         node = XMLUtils.getNodeOrFail(currentMenu, "Cursor_Position/Max_Line");
330         int maxLines = Integer.parseInt(node.getTextContent());
331         state.maxLine = maxLines;
332
333         for (int i = 1; i < 8; ++i) {
334             state.items[i - 1] = XMLUtils.getNodeContentOrDefault(currentMenu, "Current_List/Line_" + i + "/Txt",
335                     (String) null);
336         }
337
338         if (observer != null) {
339             observer.navigationUpdated(state);
340         }
341     }
342 }