2 * Copyright (c) 2010-2021 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 static java.util.stream.Collectors.*;
16 import static org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLUtils.getChildElements;
18 import java.io.IOException;
19 import java.util.Arrays;
20 import java.util.HashMap;
21 import java.util.HashSet;
24 import java.util.function.BiFunction;
25 import java.util.function.Function;
26 import java.util.function.Predicate;
27 import java.util.stream.Stream;
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;
42 * Represents device descriptor for XML protocol
44 * @author Tomasz Maruszak - Initial contribution
46 public class DeviceDescriptorXML {
48 private final Logger logger = LoggerFactory.getLogger(DeviceDescriptorXML.class);
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<>();
55 public void attach(DeviceInformationState state) {
56 state.properties.put("desc", this);
59 public static DeviceDescriptorXML getAttached(DeviceInformationState state) {
60 return (DeviceDescriptorXML) state.properties.getOrDefault("desc", null);
63 public String getUnitName() {
68 * Checks if the condition is met, on false result calls the runnable.
74 public boolean hasFeature(Predicate<DeviceDescriptorXML> predicate, Runnable falseAction) {
75 boolean result = predicate.test(this);
82 public abstract static class HasCommands {
84 public final Set<String> commands;
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())
92 commands = new HashSet<>();
96 public boolean hasCommandEnding(String command) {
97 return commands.stream().anyMatch(x -> x.endsWith(command));
100 public boolean hasAnyCommandEnding(String... anyCommand) {
101 return Arrays.stream(anyCommand).anyMatch(x -> hasCommandEnding(x));
105 * Checks if the command is available, on false result calls the runnable.
111 public boolean hasCommandEnding(String command, Runnable falseAction) {
112 boolean result = hasCommandEnding(command);
120 public String toString() {
121 return commands.stream().collect(joining(";"));
125 public class SystemDescriptor extends HasCommands {
127 public SystemDescriptor(Element element) {
132 public class ZoneDescriptor extends HasCommands {
134 public final Zone zone;
136 public ZoneDescriptor(Zone zone, Element element) {
139 logger.trace("Zone {} has commands: {}", zone, super.toString());
143 public class FeatureDescriptor extends HasCommands {
145 public final Feature feature;
147 public FeatureDescriptor(Feature feature, Element element) {
149 this.feature = feature;
150 logger.trace("Feature {} has commands: {}", feature, super.toString());
155 * Get the descriptor XML from the AVR and parse
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);
163 unitName = descNode.getAttributes().getNamedItem("Unit_Name").getTextContent();
165 system = buildFeatureLookup(descNode, "Unit", tag -> tag, (tag, e) -> new SystemDescriptor(e))
166 .getOrDefault("System", this.system); // there will be only one System entry
168 zones = buildFeatureLookup(descNode, "Subunit", tag -> YamahaUtils.tryParseEnum(Zone.class, tag),
169 (zone, e) -> new ZoneDescriptor(zone, e));
171 features = buildFeatureLookup(descNode, "Source_Device",
172 tag -> XMLConstants.FEATURE_BY_YNC_TAG.getOrDefault(tag, null),
173 (feature, e) -> new FeatureDescriptor(feature, e));
175 logger.debug("Found system {}, zones {}, features {}", system != null ? 1 : 0, zones.size(), features.size());
179 * Tires to get the XML descriptor for the AVR
184 private Node tryGetDescriptor(XMLConnection con) {
185 for (String path : Arrays.asList("/YamahaRemoteControl/desc.xml", "/YamahaRemoteControl/UnitDesc.xml")) {
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);
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);
204 logger.warn("Could not retrieve descriptor");
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<>();
212 if (descNode != null) {
213 Stream<Element> elements = getChildElements(descNode)
214 .filter(x -> "Menu".equals(x.getTagName()) && funcValue.equals(x.getAttribute("Func")));
216 elements.forEach(e -> {
217 String tag = e.getAttribute("YNC_Tag");
219 if (StringUtils.isNotEmpty(tag)) {
220 T key = converter.apply(tag);
222 V value = factory.apply(key, e);
224 // a VNC_Tag value might appear more than once (e.g. Zone B has Main_Zone tag)
225 groupedElements.putIfAbsent(key, value);
231 return groupedElements;