]> git.basschouten.com Git - openhab-addons.git/blob
83ec3fc6d5dc73cb04cf489b874fff1b4fcef25b
[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 java.util.stream.Collectors.*;
16 import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLUtils.getChildElements;
17
18 import java.io.IOException;
19 import java.util.Arrays;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.Map;
23 import java.util.Set;
24 import java.util.function.BiFunction;
25 import java.util.function.Function;
26 import java.util.function.Predicate;
27 import java.util.stream.Stream;
28
29 import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Feature;
30 import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
31 import org.openhab.binding.yamahareceiver.internal.config.YamahaUtils;
32 import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35 import org.w3c.dom.Document;
36 import org.w3c.dom.Element;
37 import org.w3c.dom.Node;
38
39 /**
40  *
41  * Represents device descriptor for XML protocol
42  *
43  * @author Tomasz Maruszak - Initial contribution
44  */
45 public class DeviceDescriptorXML {
46
47     private final Logger logger = LoggerFactory.getLogger(DeviceDescriptorXML.class);
48
49     private String unitName;
50     public SystemDescriptor system = new SystemDescriptor(null);
51     public Map<Zone, ZoneDescriptor> zones = new HashMap<>();
52     public Map<Feature, FeatureDescriptor> features = new HashMap<>();
53
54     public void attach(DeviceInformationState state) {
55         state.properties.put("desc", this);
56     }
57
58     public static DeviceDescriptorXML getAttached(DeviceInformationState state) {
59         return (DeviceDescriptorXML) state.properties.getOrDefault("desc", null);
60     }
61
62     public String getUnitName() {
63         return unitName;
64     }
65
66     /**
67      * Checks if the condition is met, on false result calls the runnable.
68      *
69      * @param predicate
70      * @param falseAction
71      * @return
72      */
73     public boolean hasFeature(Predicate<DeviceDescriptorXML> predicate, Runnable falseAction) {
74         boolean result = predicate.test(this);
75         if (!result) {
76             falseAction.run();
77         }
78         return result;
79     }
80
81     public abstract static class HasCommands {
82
83         public final Set<String> commands;
84
85         public HasCommands(Element element) {
86             Element cmdList = (Element) XMLUtils.getNode(element, "Cmd_List");
87             if (cmdList != null) {
88                 commands = XMLUtils.toStream(cmdList.getElementsByTagName("Define")).map(x -> x.getTextContent())
89                         .collect(toSet());
90             } else {
91                 commands = new HashSet<>();
92             }
93         }
94
95         public boolean hasCommandEnding(String command) {
96             return commands.stream().anyMatch(x -> x.endsWith(command));
97         }
98
99         public boolean hasAnyCommandEnding(String... anyCommand) {
100             return Arrays.stream(anyCommand).anyMatch(x -> hasCommandEnding(x));
101         }
102
103         /**
104          * Checks if the command is available, on false result calls the runnable.
105          *
106          * @param command
107          * @param falseAction
108          * @return
109          */
110         public boolean hasCommandEnding(String command, Runnable falseAction) {
111             boolean result = hasCommandEnding(command);
112             if (!result) {
113                 falseAction.run();
114             }
115             return result;
116         }
117
118         @Override
119         public String toString() {
120             return commands.stream().collect(joining(";"));
121         }
122     }
123
124     public class SystemDescriptor extends HasCommands {
125
126         public SystemDescriptor(Element element) {
127             super(element);
128         }
129     }
130
131     public class ZoneDescriptor extends HasCommands {
132
133         public final Zone zone;
134
135         public ZoneDescriptor(Zone zone, Element element) {
136             super(element);
137             this.zone = zone;
138             logger.trace("Zone {} has commands: {}", zone, super.toString());
139         }
140     }
141
142     public class FeatureDescriptor extends HasCommands {
143
144         public final Feature feature;
145
146         public FeatureDescriptor(Feature feature, Element element) {
147             super(element);
148             this.feature = feature;
149             logger.trace("Feature {} has commands: {}", feature, super.toString());
150         }
151     }
152
153     /**
154      * Get the descriptor XML from the AVR and parse
155      *
156      * @param con
157      */
158     public void load(XMLConnection con) {
159         // Get and store the Yamaha Description XML. This will be used to detect proper element naming in other areas.
160         Node descNode = tryGetDescriptor(con);
161
162         unitName = descNode.getAttributes().getNamedItem("Unit_Name").getTextContent();
163
164         system = buildFeatureLookup(descNode, "Unit", tag -> tag, (tag, e) -> new SystemDescriptor(e))
165                 .getOrDefault("System", this.system); // there will be only one System entry
166
167         zones = buildFeatureLookup(descNode, "Subunit", tag -> YamahaUtils.tryParseEnum(Zone.class, tag),
168                 (zone, e) -> new ZoneDescriptor(zone, e));
169
170         features = buildFeatureLookup(descNode, "Source_Device",
171                 tag -> XMLConstants.FEATURE_BY_YNC_TAG.getOrDefault(tag, null),
172                 (feature, e) -> new FeatureDescriptor(feature, e));
173
174         logger.debug("Found system {}, zones {}, features {}", system != null ? 1 : 0, zones.size(), features.size());
175     }
176
177     /**
178      * Tires to get the XML descriptor for the AVR
179      *
180      * @param con
181      * @return
182      */
183     private Node tryGetDescriptor(XMLConnection con) {
184         for (String path : Arrays.asList("/YamahaRemoteControl/desc.xml", "/YamahaRemoteControl/UnitDesc.xml")) {
185             try {
186                 String descXml = con.getResponse(path);
187                 Document doc = XMLUtils.xml(descXml);
188                 Node root = doc.getFirstChild();
189                 if (root != null && "Unit_Description".equals(root.getNodeName())) {
190                     logger.debug("Retrieved descriptor from {}", path);
191                     return root;
192                 }
193                 logger.debug("The {} response was invalid: {}", path, descXml);
194             } catch (IOException e) {
195                 // The XML document under specified path does not exist for this model
196                 logger.debug("No descriptor at path {}", path);
197             } catch (Exception e) {
198                 // Note: We were able to get the XML, but likely cannot parse it properly
199                 logger.warn("Could not parse descriptor at path {}", path, e);
200                 break;
201             }
202         }
203         logger.warn("Could not retrieve descriptor");
204         return null;
205     }
206
207     private <T, V> Map<T, V> buildFeatureLookup(Node descNode, String funcValue, Function<String, T> converter,
208             BiFunction<T, Element, V> factory) {
209         Map<T, V> groupedElements = new HashMap<>();
210
211         if (descNode != null) {
212             Stream<Element> elements = getChildElements(descNode)
213                     .filter(x -> "Menu".equals(x.getTagName()) && funcValue.equals(x.getAttribute("Func")));
214
215             elements.forEach(e -> {
216                 String tag = e.getAttribute("YNC_Tag");
217
218                 if (!tag.isEmpty()) {
219                     T key = converter.apply(tag);
220                     if (key != null) {
221                         V value = factory.apply(key, e);
222
223                         // a VNC_Tag value might appear more than once (e.g. Zone B has Main_Zone tag)
224                         groupedElements.putIfAbsent(key, value);
225                     }
226                 }
227             });
228         }
229
230         return groupedElements;
231     }
232 }