]> git.basschouten.com Git - openhab-addons.git/blob
9675d4400f790b139809b6f742bf317743a82591
[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.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("yes".equalsIgnoreCase(properties.get("muted")));
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                     String sinkNames = properties.get("combine.slaves");
146                     if (sinkNames != null) {
147                         for (String sinkName : sinkNames.replace("\"", "").split(",")) {
148                             sink.addCombinedSinkName(sinkName);
149                         }
150                     }
151                     combinedSinks.add(sink);
152                 }
153                 sinks.put(sink.getUIDName(), sink);
154             }
155         }
156         for (Sink combinedSink : combinedSinks) {
157             for (String sinkName : combinedSink.getCombinedSinkNames()) {
158                 combinedSink.addCombinedSink(sinks.get(sinkName));
159             }
160         }
161         return sinks.values();
162     }
163
164     /**
165      * parses the pulseaudio servers answer to the list-sink-inputs command and returns a list of
166      * {@link SinkInput} objects
167      *
168      * @param raw the given string from the pulseaudio server
169      * @return list of sink-inputs
170      */
171     public static List<SinkInput> parseSinkInputs(String raw, PulseaudioClient client) {
172         List<SinkInput> items = new ArrayList<>();
173         String[] parts = raw.split("index: ");
174         if (parts.length <= 1) {
175             return items;
176         }
177         for (int i = 1; i < parts.length; i++) {
178             String[] lines = parts[i].split("\n");
179             Hashtable<String, String> properties = new Hashtable<>();
180             int id = 0;
181             try {
182                 id = Integer.valueOf(lines[0].trim());
183             } catch (NumberFormatException e) {
184                 // sometime the line feed is missing here
185                 Matcher matcher = FALL_BACK_PATTERN.matcher(lines[0].trim());
186                 if (matcher.find()) {
187                     id = Integer.valueOf(matcher.group(1));
188                     properties.put(matcher.group(2).trim(), matcher.group(3).trim());
189                 }
190             }
191             for (int j = 1; j < lines.length; j++) {
192                 Matcher matcher = PATTERN.matcher(lines[j]);
193                 if (matcher.find()) {
194                     properties.put(matcher.group(1).trim(), matcher.group(2).trim());
195                 }
196             }
197             if (properties.containsKey("sink")) {
198                 String name = properties.containsKey("media.name") ? properties.get("media.name")
199                         : properties.get("sink");
200                 SinkInput item = new SinkInput(id, name, client.getModule(getNumberValue(properties.get("module"))));
201                 if (properties.containsKey("state")) {
202                     try {
203                         item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
204                     } catch (IllegalArgumentException e) {
205                         LOGGER.error("unhandled state {} in sink-input item #{}", properties.get("state"), id);
206                     }
207                 }
208                 if (properties.containsKey("muted")) {
209                     item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
210                 }
211                 if (properties.containsKey("volume")) {
212                     item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
213                 }
214                 if (properties.containsKey("sink")) {
215                     item.setSink(client.getSink(Integer.valueOf(getNumberValue(properties.get("sink")))));
216                 }
217                 items.add(item);
218             }
219         }
220         return items;
221     }
222
223     /**
224      * parses the pulseaudio servers answer to the list-sources command and returns a list of
225      * {@link Source} objects
226      *
227      * @param raw the given string from the pulseaudio server
228      * @return list of sources
229      */
230     public static List<Source> parseSources(String raw, PulseaudioClient client) {
231         List<Source> sources = new ArrayList<>();
232         String[] parts = raw.split("index: ");
233         if (parts.length <= 1) {
234             return sources;
235         }
236         // skip first part
237         for (int i = 1; i < parts.length; i++) {
238             String[] lines = parts[i].split("\n");
239             Hashtable<String, String> properties = new Hashtable<>();
240             int id = 0;
241             try {
242                 id = Integer.valueOf(lines[0].trim());
243             } catch (NumberFormatException e) {
244                 // sometime the line feed is missing here
245                 Matcher matcher = FALL_BACK_PATTERN.matcher(lines[0].trim());
246                 if (matcher.find()) {
247                     id = Integer.valueOf(matcher.group(1));
248                     properties.put(matcher.group(2).trim(), matcher.group(3).trim());
249                 }
250             }
251             for (int j = 1; j < lines.length; j++) {
252                 Matcher matcher = PATTERN.matcher(lines[j]);
253                 if (matcher.find()) {
254                     properties.put(matcher.group(1).trim(), matcher.group(2).trim());
255                 }
256             }
257             if (properties.containsKey("name")) {
258                 Source source = new Source(id, properties.get("name"),
259                         client.getModule(getNumberValue(properties.get("module"))));
260                 if (properties.containsKey("state")) {
261                     try {
262                         source.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
263                     } catch (IllegalArgumentException e) {
264                         LOGGER.error("unhandled state {} in source item #{}", properties.get("state"), id);
265                     }
266                 }
267                 if (properties.containsKey("muted")) {
268                     source.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
269                 }
270                 if (properties.containsKey("volume")) {
271                     source.setVolume(parseVolume(properties.get("volume")));
272                 }
273                 String monitorOf = properties.get("monitor_of");
274                 if (monitorOf != null) {
275                     source.setMonitorOf(client.getSink(Integer.valueOf(monitorOf)));
276                 }
277                 sources.add(source);
278             }
279         }
280         return sources;
281     }
282
283     /**
284      * parses the pulseaudio servers answer to the list-source-outputs command and returns a list of
285      * {@link SourceOutput} objects
286      *
287      * @param raw the given string from the pulseaudio server
288      * @return list of source-outputs
289      */
290     public static List<SourceOutput> parseSourceOutputs(String raw, PulseaudioClient client) {
291         List<SourceOutput> items = new ArrayList<>();
292         String[] parts = raw.split("index: ");
293         if (parts.length <= 1) {
294             return items;
295         }
296         // skip first part
297         for (int i = 1; i < parts.length; i++) {
298             String[] lines = parts[i].split("\n");
299             Hashtable<String, String> properties = new Hashtable<>();
300             int id = 0;
301             try {
302                 id = Integer.valueOf(lines[0].trim());
303             } catch (NumberFormatException e) {
304                 // sometime the line feed is missing here
305                 Matcher matcher = FALL_BACK_PATTERN.matcher(lines[0].trim());
306                 if (matcher.find()) {
307                     id = Integer.valueOf(matcher.group(1));
308                     properties.put(matcher.group(2).trim(), matcher.group(3).trim());
309                 }
310             }
311             for (int j = 1; j < lines.length; j++) {
312                 Matcher matcher = PATTERN.matcher(lines[j]);
313                 if (matcher.find()) {
314                     properties.put(matcher.group(1).trim(), matcher.group(2).trim());
315                 }
316             }
317             if (properties.containsKey("source")) {
318                 SourceOutput item = new SourceOutput(id, properties.get("source"),
319                         client.getModule(getNumberValue(properties.get("module"))));
320                 if (properties.containsKey("state")) {
321                     try {
322                         item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
323                     } catch (IllegalArgumentException e) {
324                         LOGGER.error("unhandled state {} in source-output item #{}", properties.get("state"), id);
325                     }
326                 }
327                 if (properties.containsKey("muted")) {
328                     item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
329                 }
330                 if (properties.containsKey("volume")) {
331                     item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
332                 }
333                 if (properties.containsKey("source")) {
334                     item.setSource(client.getSource(Integer.valueOf(getNumberValue(properties.get("source")))));
335                 }
336                 items.add(item);
337             }
338         }
339         return items;
340     }
341
342     /**
343      * converts the volume value given by the pulseaudio server
344      * to a percentage value. The pulseaudio server sends 2 values for left and right channel volume
345      * e.g. 0: 80% 1: 80% which would be converted to 80
346      *
347      * @param vol
348      * @return
349      */
350     private static int parseVolume(String vol) {
351         int volumeTotal = 0;
352         int nChannels = 0;
353         for (String channel : vol.split(", ")) {
354             Matcher matcher = VOLUME_PATTERN.matcher(channel.trim());
355             if (matcher.find()) {
356                 volumeTotal += Integer.valueOf(matcher.group(3));
357                 nChannels++;
358             } else {
359                 LOGGER.debug("Unable to parse channel volume '{}'", channel);
360             }
361         }
362         if (nChannels > 0) {
363             return Math.round(volumeTotal / nChannels);
364         }
365         return 0;
366     }
367
368     /**
369      * sometimes the pulseaudio server "forgets" some line feeds which leeds to unparsable number values
370      * like 80NextProperty:
371      * this is a workaround to get the correct number value in these cases
372      *
373      * @param raw
374      * @return
375      */
376     private static int getNumberValue(String raw) {
377         int id = -1;
378         if (raw == null) {
379             return 0;
380         }
381         try {
382             id = Integer.valueOf(raw.trim());
383         } catch (NumberFormatException e) {
384             Matcher matcher = NUMBER_VALUE_PATTERN.matcher(raw.trim());
385             if (matcher.find()) {
386                 id = Integer.valueOf(matcher.group(1));
387             }
388         }
389         return id;
390     }
391 }