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