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