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