2 * Copyright (c) 2010-2023 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.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;
41 * Represents device descriptor for XML protocol
43 * @author Tomasz Maruszak - Initial contribution
45 public class DeviceDescriptorXML {
47 private final Logger logger = LoggerFactory.getLogger(DeviceDescriptorXML.class);
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<>();
54 public void attach(DeviceInformationState state) {
55 state.properties.put("desc", this);
58 public static DeviceDescriptorXML getAttached(DeviceInformationState state) {
59 return (DeviceDescriptorXML) state.properties.getOrDefault("desc", null);
62 public String getUnitName() {
67 * Checks if the condition is met, on false result calls the runnable.
73 public boolean hasFeature(Predicate<DeviceDescriptorXML> predicate, Runnable falseAction) {
74 boolean result = predicate.test(this);
81 public abstract static class HasCommands {
83 public final Set<String> commands;
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())
91 commands = new HashSet<>();
95 public boolean hasCommandEnding(String command) {
96 return commands.stream().anyMatch(x -> x.endsWith(command));
99 public boolean hasAnyCommandEnding(String... anyCommand) {
100 return Arrays.stream(anyCommand).anyMatch(x -> hasCommandEnding(x));
104 * Checks if the command is available, on false result calls the runnable.
110 public boolean hasCommandEnding(String command, Runnable falseAction) {
111 boolean result = hasCommandEnding(command);
119 public String toString() {
120 return commands.stream().collect(joining(";"));
124 public class SystemDescriptor extends HasCommands {
126 public SystemDescriptor(Element element) {
131 public class ZoneDescriptor extends HasCommands {
133 public final Zone zone;
135 public ZoneDescriptor(Zone zone, Element element) {
138 logger.trace("Zone {} has commands: {}", zone, super.toString());
142 public class FeatureDescriptor extends HasCommands {
144 public final Feature feature;
146 public FeatureDescriptor(Feature feature, Element element) {
148 this.feature = feature;
149 logger.trace("Feature {} has commands: {}", feature, super.toString());
154 * Get the descriptor XML from the AVR and parse
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);
162 unitName = descNode.getAttributes().getNamedItem("Unit_Name").getTextContent();
164 system = buildFeatureLookup(descNode, "Unit", tag -> tag, (tag, e) -> new SystemDescriptor(e))
165 .getOrDefault("System", this.system); // there will be only one System entry
167 zones = buildFeatureLookup(descNode, "Subunit", tag -> YamahaUtils.tryParseEnum(Zone.class, tag),
168 (zone, e) -> new ZoneDescriptor(zone, e));
170 features = buildFeatureLookup(descNode, "Source_Device",
171 tag -> XMLConstants.FEATURE_BY_YNC_TAG.getOrDefault(tag, null),
172 (feature, e) -> new FeatureDescriptor(feature, e));
174 logger.debug("Found system {}, zones {}, features {}", system != null ? 1 : 0, zones.size(), features.size());
178 * Tires to get the XML descriptor for the AVR
183 private Node tryGetDescriptor(XMLConnection con) {
184 for (String path : Arrays.asList("/YamahaRemoteControl/desc.xml", "/YamahaRemoteControl/UnitDesc.xml")) {
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);
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);
203 logger.warn("Could not retrieve descriptor");
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<>();
211 if (descNode != null) {
212 Stream<Element> elements = getChildElements(descNode)
213 .filter(x -> "Menu".equals(x.getTagName()) && funcValue.equals(x.getAttribute("Func")));
215 elements.forEach(e -> {
216 String tag = e.getAttribute("YNC_Tag");
218 if (!tag.isEmpty()) {
219 T key = converter.apply(tag);
221 V value = factory.apply(key, e);
223 // a VNC_Tag value might appear more than once (e.g. Zone B has Main_Zone tag)
224 groupedElements.putIfAbsent(key, value);
230 return groupedElements;