]> git.basschouten.com Git - openhab-addons.git/blob
9284011b7d57965c8167d9221ccb65c6858fd8fa
[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(Integer.valueOf(parseVolume(properties.get("volume"))));
269                 }
270                 if (properties.containsKey("monitor_of")) {
271                     source.setMonitorOf(client.getSink(Integer.valueOf(properties.get("monitor_of"))));
272                 }
273                 sources.add(source);
274             }
275         }
276         return sources;
277     }
278
279     /**
280      * parses the pulseaudio servers answer to the list-source-outputs command and returns a list of
281      * {@link SourceOutput} objects
282      *
283      * @param raw the given string from the pulseaudio server
284      * @return list of source-outputs
285      */
286     public static List<SourceOutput> parseSourceOutputs(String raw, PulseaudioClient client) {
287         List<SourceOutput> items = new ArrayList<>();
288         String[] parts = raw.split("index: ");
289         if (parts.length <= 1) {
290             return items;
291         }
292         // skip first part
293         for (int i = 1; i < parts.length; i++) {
294             String[] lines = parts[i].split("\n");
295             Hashtable<String, String> properties = new Hashtable<>();
296             int id = 0;
297             try {
298                 id = Integer.valueOf(lines[0].trim());
299             } catch (NumberFormatException e) {
300                 // sometime the line feed is missing here
301                 Matcher matcher = FALL_BACK_PATTERN.matcher(lines[0].trim());
302                 if (matcher.find()) {
303                     id = Integer.valueOf(matcher.group(1));
304                     properties.put(matcher.group(2).trim(), matcher.group(3).trim());
305                 }
306             }
307             for (int j = 1; j < lines.length; j++) {
308                 Matcher matcher = PATTERN.matcher(lines[j]);
309                 if (matcher.find()) {
310                     properties.put(matcher.group(1).trim(), matcher.group(2).trim());
311                 }
312             }
313             if (properties.containsKey("source")) {
314                 SourceOutput item = new SourceOutput(id, properties.get("source"),
315                         client.getModule(getNumberValue(properties.get("module"))));
316                 if (properties.containsKey("state")) {
317                     try {
318                         item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
319                     } catch (IllegalArgumentException e) {
320                         LOGGER.error("unhandled state {} in source-output item #{}", properties.get("state"), id);
321                     }
322                 }
323                 if (properties.containsKey("muted")) {
324                     item.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
325                 }
326                 if (properties.containsKey("volume")) {
327                     item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
328                 }
329                 if (properties.containsKey("source")) {
330                     item.setSource(client.getSource(Integer.valueOf(getNumberValue(properties.get("source")))));
331                 }
332                 items.add(item);
333             }
334         }
335         return items;
336     }
337
338     /**
339      * converts the volume value given by the pulseaudio server
340      * to a percentage value. The pulseaudio server sends 2 values for left and right channel volume
341      * e.g. 0: 80% 1: 80% which would be converted to 80
342      *
343      * @param vol
344      * @return
345      */
346     private static int parseVolume(String vol) {
347         int volumeTotal = 0;
348         int nChannels = 0;
349         for (String channel : vol.split(", ")) {
350             Matcher matcher = VOLUME_PATTERN.matcher(channel.trim());
351             if (matcher.find()) {
352                 volumeTotal += Integer.valueOf(matcher.group(3));
353                 nChannels++;
354             } else {
355                 LOGGER.debug("Unable to parse channel volume '{}'", channel);
356             }
357         }
358         if (nChannels > 0) {
359             return Math.round(volumeTotal / nChannels);
360         }
361         return 0;
362     }
363
364     /**
365      * sometimes the pulseaudio server "forgets" some line feeds which leeds to unparsable number values
366      * like 80NextProperty:
367      * this is a workaround to get the correct number value in these cases
368      *
369      * @param raw
370      * @return
371      */
372     private static int getNumberValue(String raw) {
373         int id = -1;
374         if (raw == null) {
375             return 0;
376         }
377         try {
378             id = Integer.valueOf(raw.trim());
379         } catch (NumberFormatException e) {
380             Matcher matcher = NUMBER_VALUE_PATTERN.matcher(raw.trim());
381             if (matcher.find()) {
382                 id = Integer.valueOf(matcher.group(1));
383             }
384         }
385         return id;
386     }
387 }