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