]> git.basschouten.com Git - openhab-addons.git/blob
dec885e734dbad7986938e2cdf3a8bd96c78bed2
[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.toSet;
16 import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.Inputs.*;
17
18 import java.io.IOException;
19 import java.lang.ref.WeakReference;
20 import java.util.HashMap;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.stream.Stream;
24
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;
32
33 /**
34  * XML implementation of {@link InputConverter}.
35  *
36  * @author Tomasz Maruszak - Initial contribution.
37  *
38  */
39 public class InputConverterXML implements InputConverter {
40
41     private final Logger logger = LoggerFactory.getLogger(InputConverterXML.class);
42
43     private final WeakReference<AbstractConnection> comReference;
44
45     /**
46      * User defined mapping for state to input name.
47      */
48     private final Map<String, String> inputMap;
49     /**
50      * Holds all the inputs names that should NOT be transformed by the {@link #convertNameToID(String)} method.
51      */
52     private final Set<String> inputsWithoutMapping;
53
54     public InputConverterXML(AbstractConnection con, String inputMapConfig) {
55         this.comReference = new WeakReference<>(con);
56
57         logger.trace("User defined mapping: {}", inputMapConfig);
58         this.inputMap = createMapFromSetting(inputMapConfig);
59
60         try {
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);
65         }
66     }
67
68     /**
69      * Creates a map from a string representation: "KEY1=VALUE1,KEY2=VALUE2"
70      *
71      * @param setting
72      * @return
73      */
74     private Map<String, String> createMapFromSetting(String setting) {
75         Map<String, String> map = new HashMap<>();
76
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);
83                 } else {
84                     String key = keyValue[0];
85                     String value = keyValue[1];
86
87                     if (map.putIfAbsent(key, value) != null) {
88                         logger.warn("Invalid setting: {} entry: {} - key: {} was already provided before", setting,
89                                 entry, key);
90                     }
91                 }
92             }
93         }
94         return map;
95     }
96
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());
100
101         Set<String> nativeInputNames = XMLProtocolService.getInputs(comReference.get(), Zone.Main_Zone).stream()
102                 .filter(x -> x.isWritable()).map(x -> x.getParam()).collect(toSet());
103
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));
110
111         return inputsWithoutMapping;
112     }
113
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);
117     }
118
119     @Override
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);
123         return name;
124     }
125
126     @Override
127     public String fromStateName(String name) {
128         String convertedName;
129         String method;
130
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";
139         } else {
140             // Step 3: Fallback to legacy logic
141             convertedName = convertNameToID(name);
142             method = "legacy mapping";
143         }
144         logger.trace("Converting from state name {} to {} - as per {}", name, convertedName, method);
145         return convertedName;
146     }
147
148     /**
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.
151      **
152      * @param name The inputID like "Net Radio".
153      * @return An xml node / xml protocol compatible name like NET_RADIO.
154      */
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_");
164         }
165         return name;
166     }
167 }