]> git.basschouten.com Git - openhab-addons.git/blob
47742c1796eed1c577003785133527e877fff9c7
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.pulseaudio.internal.cli;
14
15 import java.util.ArrayList;
16 import java.util.Collection;
17 import java.util.Hashtable;
18 import java.util.List;
19 import java.util.regex.Matcher;
20 import java.util.regex.Pattern;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.pulseaudio.internal.PulseaudioClient;
25 import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
26 import org.openhab.binding.pulseaudio.internal.items.Module;
27 import org.openhab.binding.pulseaudio.internal.items.Sink;
28 import org.openhab.binding.pulseaudio.internal.items.SinkInput;
29 import org.openhab.binding.pulseaudio.internal.items.Source;
30 import org.openhab.binding.pulseaudio.internal.items.SourceOutput;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 /**
35  * Parsers for the pulseaudio return strings
36  *
37  * @author Tobias Bräutigam - Initial contribution
38  */
39 @NonNullByDefault
40 public class Parser {
41     private static final Logger LOGGER = LoggerFactory.getLogger(Parser.class);
42
43     private static final Pattern PATTERN = Pattern.compile("^\\s*([a-z\\s._]+)[:=]\\s*<?\\\"?([^>\\\"]+)\\\"?>?$");
44     private static final Pattern VOLUME_PATTERN = Pattern
45             .compile("^([\\w\\-]+):( *[\\d]+ \\/)? *([\\d]+)% *\\/? *([\\d\\-., dB]+)?$");
46     private static final Pattern FALL_BACK_PATTERN = Pattern
47             .compile("^([0-9]+)([a-z\\s._]+)[:=]\\s*<?\"?([^>\"]+)\"?>?$");
48     private static final Pattern NUMBER_VALUE_PATTERN = Pattern.compile("^([0-9]+).*$");
49
50     /**
51      * parses the pulseaudio servers answer to the list-modules command and returns a list of
52      * {@link Module} objects
53      *
54      * @param raw the given string from the pulseaudio server
55      * @return list of modules
56      */
57     public static List<Module> parseModules(String raw) {
58         List<Module> modules = new ArrayList<>();
59         String[] parts = raw.split("index: ");
60         if (parts.length <= 1) {
61             return modules;
62         }
63         // skip first part
64         for (int i = 1; i < parts.length; i++) {
65             String[] lines = parts[i].split("\n");
66             Hashtable<String, String> properties = new Hashtable<>();
67             int id = 0;
68             try {
69                 id = Integer.valueOf(lines[0].trim());
70             } catch (NumberFormatException e) {
71                 // sometime the line feed is missing here
72                 Matcher matcher = FALL_BACK_PATTERN.matcher(lines[0].trim());
73                 if (matcher.find()) {
74                     id = Integer.valueOf(matcher.group(1));
75                     properties.put(matcher.group(2).trim(), matcher.group(3).trim());
76                 }
77             }
78             for (int j = 1; j < lines.length; j++) {
79                 Matcher matcher = PATTERN.matcher(lines[j]);
80                 if (matcher.find()) {
81                     properties.put(matcher.group(1).trim(), matcher.group(2).trim());
82                 }
83             }
84             if (properties.containsKey("name")) {
85                 Module module = new Module(id, properties.get("name"));
86                 if (properties.containsKey("argument")) {
87                     module.setArgument(properties.get("argument"));
88                 }
89                 modules.add(module);
90             }
91         }
92         return modules;
93     }
94
95     /**
96      * parses the pulseaudio servers answer to the list-sinks command and returns a list of
97      * {@link Sink} objects
98      *
99      * @param raw the given string from the pulseaudio server
100      * @return list of sinks
101      */
102     public static Collection<Sink> parseSinks(String raw, PulseaudioClient client) {
103         Hashtable<String, Sink> sinks = new Hashtable<>();
104         String[] parts = raw.split("index: ");
105         if (parts.length <= 1) {
106             return sinks.values();
107         }
108         // skip first part
109         List<Sink> combinedSinks = new ArrayList<>();
110         for (int i = 1; i < parts.length; i++) {
111             String[] lines = parts[i].split("\n");
112             Hashtable<String, String> properties = new Hashtable<>();
113             int id = 0;
114             try {
115                 id = Integer.valueOf(lines[0].trim());
116             } catch (NumberFormatException e) {
117                 // sometime the line feed is missing here
118                 Matcher matcher = FALL_BACK_PATTERN.matcher(lines[0].trim());
119                 if (matcher.find()) {
120                     id = Integer.valueOf(matcher.group(1));
121                     properties.put(matcher.group(2).trim(), matcher.group(3).trim());
122                 }
123             }
124             for (int j = 1; j < lines.length; j++) {
125                 Matcher matcher = PATTERN.matcher(lines[j]);
126                 if (matcher.find()) {
127                     properties.put(matcher.group(1).trim(), matcher.group(2).trim());
128                 }
129             }
130             if (properties.containsKey("name")) {
131                 Sink sink = new Sink(id, properties.get("name"),
132                         client.getModule(getNumberValue(properties.get("module"))));
133                 if (properties.containsKey("state")) {
134                     try {
135                         sink.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
136                     } catch (IllegalArgumentException e) {
137                         LOGGER.error("unhandled state {} in sink item #{}", properties.get("state"), id);
138                     }
139                 }
140                 if (properties.containsKey("muted")) {
141                     sink.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
142                 }
143                 if (properties.containsKey("volume")) {
144                     sink.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
145                 }
146                 if (properties.containsKey("combine.slaves")) {
147                     // this is a combined sink, the combined sink object should be
148                     String sinkNames = properties.get("combine.slaves");
149                     for (String sinkName : sinkNames.replace("\"", "").split(",")) {
150                         sink.addCombinedSinkName(sinkName);
151                     }
152                     combinedSinks.add(sink);
153                 }
154                 sinks.put(sink.getUIDName(), sink);
155             }
156         }
157         for (Sink combinedSink : combinedSinks) {
158             for (String sinkName : combinedSink.getCombinedSinkNames()) {
159                 combinedSink.addCombinedSink(sinks.get(sinkName));
160             }
161         }
162         return sinks.values();
163     }
164
165     /**
166      * parses the pulseaudio servers answer to the list-sink-inputs command and returns a list of
167      * {@link SinkInput} objects
168      *
169      * @param raw the given string from the pulseaudio server
170      * @return list of sink-inputs
171      */
172     public static List<SinkInput> parseSinkInputs(String raw, PulseaudioClient client) {
173         List<SinkInput> items = new ArrayList<>();
174         String[] parts = raw.split("index: ");
175         if (parts.length <= 1) {
176             return items;
177         }
178         for (int i = 1; i < parts.length; i++) {
179             String[] lines = parts[i].split("\n");
180             Hashtable<String, String> properties = new Hashtable<>();
181             int id = 0;
182             try {
183                 id = Integer.valueOf(lines[0].trim());
184             } catch (NumberFormatException e) {
185                 // sometime the line feed is missing here
186                 Matcher matcher = FALL_BACK_PATTERN.matcher(lines[0].trim());
187                 if (matcher.find()) {
188                     id = Integer.valueOf(matcher.group(1));
189                     properties.put(matcher.group(2).trim(), matcher.group(3).trim());
190                 }
191             }
192             for (int j = 1; j < lines.length; j++) {
193                 Matcher matcher = PATTERN.matcher(lines[j]);
194                 if (matcher.find()) {
195                     properties.put(matcher.group(1).trim(), matcher.group(2).trim());
196                 }
197             }
198             if (properties.containsKey("sink")) {
199                 String name = properties.containsKey("media.name") ? properties.get("media.name")
200                         : properties.get("sink");
201                 SinkInput item = new SinkInput(id, name, client.getModule(getNumberValue(properties.get("module"))));
202                 if (properties.containsKey("state")) {
203                     try {
204                         item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
205                     } catch (IllegalArgumentException e) {
206                         LOGGER.error("unhandled state {} in sink-input item #{}", properties.get("state"), id);
207                     }
208                 }
209                 if (properties.containsKey("muted")) {
210                     item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
211                 }
212                 if (properties.containsKey("volume")) {
213                     item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
214                 }
215                 if (properties.containsKey("sink")) {
216                     item.setSink(client.getSink(Integer.valueOf(getNumberValue(properties.get("sink")))));
217                 }
218                 items.add(item);
219             }
220         }
221         return items;
222     }
223
224     /**
225      * parses the pulseaudio servers answer to the list-sources command and returns a list of
226      * {@link Source} objects
227      *
228      * @param raw the given string from the pulseaudio server
229      * @return list of sources
230      */
231     public static List<Source> parseSources(String raw, PulseaudioClient client) {
232         List<Source> sources = new ArrayList<>();
233         String[] parts = raw.split("index: ");
234         if (parts.length <= 1) {
235             return sources;
236         }
237         // skip first part
238         for (int i = 1; i < parts.length; i++) {
239             String[] lines = parts[i].split("\n");
240             Hashtable<String, String> properties = new Hashtable<>();
241             int id = 0;
242             try {
243                 id = Integer.valueOf(lines[0].trim());
244             } catch (NumberFormatException e) {
245                 // sometime the line feed is missing here
246                 Matcher matcher = FALL_BACK_PATTERN.matcher(lines[0].trim());
247                 if (matcher.find()) {
248                     id = Integer.valueOf(matcher.group(1));
249                     properties.put(matcher.group(2).trim(), matcher.group(3).trim());
250                 }
251             }
252             for (int j = 1; j < lines.length; j++) {
253                 Matcher matcher = PATTERN.matcher(lines[j]);
254                 if (matcher.find()) {
255                     properties.put(matcher.group(1).trim(), matcher.group(2).trim());
256                 }
257             }
258             if (properties.containsKey("name")) {
259                 Source source = new Source(id, properties.get("name"),
260                         client.getModule(getNumberValue(properties.get("module"))));
261                 if (properties.containsKey("state")) {
262                     try {
263                         source.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
264                     } catch (IllegalArgumentException e) {
265                         LOGGER.error("unhandled state {} in source item #{}", properties.get("state"), id);
266                     }
267                 }
268                 if (properties.containsKey("muted")) {
269                     source.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
270                 }
271                 if (properties.containsKey("volume")) {
272                     source.setVolume(parseVolume(properties.get("volume")));
273                 }
274                 if (properties.containsKey("monitor_of")) {
275                     String monitorOf = properties.get("monitor_of");
276                     source.setMonitorOf(client.getSink(Integer.valueOf(monitorOf)));
277                 }
278                 sources.add(source);
279             }
280         }
281         return sources;
282     }
283
284     /**
285      * parses the pulseaudio servers answer to the list-source-outputs command and returns a list of
286      * {@link SourceOutput} objects
287      *
288      * @param raw the given string from the pulseaudio server
289      * @return list of source-outputs
290      */
291     public static List<SourceOutput> parseSourceOutputs(String raw, PulseaudioClient client) {
292         List<SourceOutput> items = new ArrayList<>();
293         String[] parts = raw.split("index: ");
294         if (parts.length <= 1) {
295             return items;
296         }
297         // skip first part
298         for (int i = 1; i < parts.length; i++) {
299             String[] lines = parts[i].split("\n");
300             Hashtable<String, String> properties = new Hashtable<>();
301             int id = 0;
302             try {
303                 id = Integer.valueOf(lines[0].trim());
304             } catch (NumberFormatException e) {
305                 // sometime the line feed is missing here
306                 Matcher matcher = FALL_BACK_PATTERN.matcher(lines[0].trim());
307                 if (matcher.find()) {
308                     id = Integer.valueOf(matcher.group(1));
309                     properties.put(matcher.group(2).trim(), matcher.group(3).trim());
310                 }
311             }
312             for (int j = 1; j < lines.length; j++) {
313                 Matcher matcher = PATTERN.matcher(lines[j]);
314                 if (matcher.find()) {
315                     properties.put(matcher.group(1).trim(), matcher.group(2).trim());
316                 }
317             }
318             if (properties.containsKey("source")) {
319                 SourceOutput item = new SourceOutput(id, properties.get("source"),
320                         client.getModule(getNumberValue(properties.get("module"))));
321                 if (properties.containsKey("state")) {
322                     try {
323                         item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
324                     } catch (IllegalArgumentException e) {
325                         LOGGER.error("unhandled state {} in source-output item #{}", properties.get("state"), id);
326                     }
327                 }
328                 if (properties.containsKey("muted")) {
329                     item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
330                 }
331                 if (properties.containsKey("volume")) {
332                     item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
333                 }
334                 if (properties.containsKey("source")) {
335                     item.setSource(client.getSource(Integer.valueOf(getNumberValue(properties.get("source")))));
336                 }
337                 items.add(item);
338             }
339         }
340         return items;
341     }
342
343     /**
344      * converts the volume value given by the pulseaudio server
345      * to a percentage value. The pulseaudio server sends 2 values for left and right channel volume
346      * e.g. 0: 80% 1: 80% which would be converted to 80
347      *
348      * @param vol
349      * @return
350      */
351     private static int parseVolume(String vol) {
352         int volumeTotal = 0;
353         int nChannels = 0;
354         for (String channel : vol.split(", ")) {
355             Matcher matcher = VOLUME_PATTERN.matcher(channel.trim());
356             if (matcher.find()) {
357                 volumeTotal += Integer.valueOf(matcher.group(3));
358                 nChannels++;
359             } else {
360                 LOGGER.debug("Unable to parse channel volume '{}'", channel);
361             }
362         }
363         if (nChannels > 0) {
364             return Math.round(volumeTotal / nChannels);
365         }
366         return 0;
367     }
368
369     /**
370      * sometimes the pulseaudio server "forgets" some line feeds which leeds to unparsable number values
371      * like 80NextProperty:
372      * this is a workaround to get the correct number value in these cases
373      *
374      * @param raw
375      * @return
376      */
377     private static int getNumberValue(@Nullable String raw) {
378         int id = -1;
379         if (raw == null) {
380             return 0;
381         }
382         try {
383             id = Integer.valueOf(raw.trim());
384         } catch (NumberFormatException e) {
385             Matcher matcher = NUMBER_VALUE_PATTERN.matcher(raw.trim());
386             if (matcher.find()) {
387                 id = Integer.valueOf(matcher.group(1));
388             }
389         }
390         return id;
391     }
392 }