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.toSet;
16 import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Inputs.*;
18 import java.io.IOException;
19 import java.lang.ref.WeakReference;
20 import java.util.HashMap;
23 import java.util.stream.Stream;
25 import org.apache.commons.lang.StringUtils;
26 import org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Zone;
27 import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
28 import org.openhab.binding.yamahareceiver.internal.protocol.InputConverter;
29 import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
34 * XML implementation of {@link InputConverter}.
36 * @author Tomasz Maruszak - Initial contribution.
39 public class InputConverterXML implements InputConverter {
41 private final Logger logger = LoggerFactory.getLogger(InputConverterXML.class);
43 private final WeakReference<AbstractConnection> comReference;
46 * User defined mapping for state to input name.
48 private final Map<String, String> inputMap;
50 * Holds all the inputs names that should NOT be transformed by the {@link #convertNameToID(String)} method.
52 private final Set<String> inputsWithoutMapping;
54 public InputConverterXML(AbstractConnection con, String inputMapConfig) {
55 this.comReference = new WeakReference<>(con);
57 logger.trace("User defined mapping: {}", inputMapConfig);
58 this.inputMap = createMapFromSetting(inputMapConfig);
61 this.inputsWithoutMapping = createInputsWithoutMapping();
62 logger.trace("These inputs will not be mapped: {}", inputsWithoutMapping);
63 } catch (IOException | ReceivedMessageParseException e) {
64 throw new RuntimeException("Could not communicate with the device", e);
69 * Creates a map from a string representation: "KEY1=VALUE1,KEY2=VALUE2"
74 private Map<String, String> createMapFromSetting(String setting) {
75 Map<String, String> map = new HashMap<>();
77 if (!StringUtils.isEmpty(setting)) {
78 String[] entries = setting.split(","); // will contain KEY=VALUE entires
79 for (String entry : entries) {
80 String[] keyValue = entry.split("="); // split the KEY=VALUE string
81 if (keyValue.length != 2) {
82 logger.warn("Invalid setting: {} entry: {} - KEY=VALUE format was expected", setting, entry);
84 String key = keyValue[0];
85 String value = keyValue[1];
87 if (map.putIfAbsent(key, value) != null) {
88 logger.warn("Invalid setting: {} entry: {} - key: {} was already provided before", setting,
97 private Set<String> createInputsWithoutMapping() throws IOException, ReceivedMessageParseException {
98 // Tested on RX-S601D, RX-V479
99 Set<String> inputsWithoutMapping = Stream.of(INPUT_SPOTIFY, INPUT_BLUETOOTH).collect(toSet());
101 Set<String> nativeInputNames = XMLProtocolService.getInputs(comReference.get(), Zone.Main_Zone).stream()
102 .filter(x -> x.isWritable()).map(x -> x.getParam()).collect(toSet());
104 // When native input returned matches any of 'HDMIx', 'AUDIOx' or 'NET RADIO', ensure no conversion happens.
105 // Tested on RX-S601D, RX-V479
106 nativeInputNames.stream()
107 .filter(x -> startsWithAndLength(x, "HDMI", 1) || startsWithAndLength(x, "AUDIO", 1)
108 || x.equals(INPUT_NET_RADIO) || x.equals(INPUT_MUSIC_CAST_LINK))
109 .forEach(x -> inputsWithoutMapping.add(x));
111 return inputsWithoutMapping;
114 private static boolean startsWithAndLength(String str, String prefix, int extraLength) {
115 // Should be faster then regex
116 return str != null && str.length() == prefix.length() + extraLength && str.startsWith(prefix);
120 public String toCommandName(String name) {
121 // Note: conversation of outgoing command might be needed in the future
122 logger.trace("Converting from {} to command name {}", name, name);
127 public String fromStateName(String name) {
128 String convertedName;
131 if (inputMap.containsKey(name)) {
132 // Step 1: Check if the user defined custom mapping for their AVR
133 convertedName = inputMap.get(name);
134 method = "user defined mapping";
135 } else if (inputsWithoutMapping.contains(name)) {
136 // Step 2: Check if input should not be mapped at all
137 convertedName = name;
138 method = "no conversion rule";
140 // Step 3: Fallback to legacy logic
141 convertedName = convertNameToID(name);
142 method = "legacy mapping";
144 logger.trace("Converting from state name {} to {} - as per {}", name, convertedName, method);
145 return convertedName;
149 * The xml protocol expects HDMI_1, NET_RADIO as xml nodes, while the actual input IDs are
150 * HDMI 1, Net Radio. We offer this conversion method therefore.
152 * @param name The inputID like "Net Radio".
153 * @return An xml node / xml protocol compatible name like NET_RADIO.
155 public String convertNameToID(String name) {
156 // Replace whitespace with an underscore. The ID is what is used for xml tags and the AVR doesn't like
157 // whitespace in xml tags.
158 name = name.replace(" ", "_").toUpperCase();
159 // Workaround if the receiver returns "HDMI2" instead of "HDMI_2". We can't really change the input IDs in the
160 // thing type description, because we still need to send "HDMI_2" for an input change to the receiver.
161 if (name.length() >= 5 && name.startsWith("HDMI") && name.charAt(4) != '_') {
162 // Adds the missing underscore.
163 name = name.replace("HDMI", "HDMI_");