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