2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.pulseaudio.internal.cli;
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;
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;
33 * Parsers for the pulseaudio return strings
35 * @author Tobias Bräutigam - Initial contribution
38 private static final Logger LOGGER = LoggerFactory.getLogger(Parser.class);
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]+).*$");
48 * parses the pulseaudio servers answer to the list-modules command and returns a list of
49 * {@link Module} objects
51 * @param raw the given string from the pulseaudio server
52 * @return list of modules
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) {
61 for (int i = 1; i < parts.length; i++) {
62 String[] lines = parts[i].split("\n");
63 Hashtable<String, String> properties = new Hashtable<>();
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());
71 id = Integer.valueOf(matcher.group(1));
72 properties.put(matcher.group(2).trim(), matcher.group(3).trim());
75 for (int j = 1; j < lines.length; j++) {
76 Matcher matcher = PATTERN.matcher(lines[j]);
78 properties.put(matcher.group(1).trim(), matcher.group(2).trim());
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"));
93 * parses the pulseaudio servers answer to the list-sinks command and returns a list of
94 * {@link Sink} objects
96 * @param raw the given string from the pulseaudio server
97 * @return list of sinks
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();
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<>();
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());
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());
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")) {
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);
137 if (properties.containsKey("muted")) {
138 sink.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
140 if (properties.containsKey("volume")) {
141 sink.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
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);
151 combinedSinks.add(sink);
153 sinks.put(sink.getUIDName(), sink);
156 for (Sink combinedSink : combinedSinks) {
157 for (String sinkName : combinedSink.getCombinedSinkNames()) {
158 combinedSink.addCombinedSink(sinks.get(sinkName));
161 return sinks.values();
165 * parses the pulseaudio servers answer to the list-sink-inputs command and returns a list of
166 * {@link SinkInput} objects
168 * @param raw the given string from the pulseaudio server
169 * @return list of sink-inputs
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) {
177 for (int i = 1; i < parts.length; i++) {
178 String[] lines = parts[i].split("\n");
179 Hashtable<String, String> properties = new Hashtable<>();
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());
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());
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")) {
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);
208 if (properties.containsKey("muted")) {
209 item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
211 if (properties.containsKey("volume")) {
212 item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
214 if (properties.containsKey("sink")) {
215 item.setSink(client.getSink(Integer.valueOf(getNumberValue(properties.get("sink")))));
224 * parses the pulseaudio servers answer to the list-sources command and returns a list of
225 * {@link Source} objects
227 * @param raw the given string from the pulseaudio server
228 * @return list of sources
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) {
237 for (int i = 1; i < parts.length; i++) {
238 String[] lines = parts[i].split("\n");
239 Hashtable<String, String> properties = new Hashtable<>();
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());
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());
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")) {
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);
267 if (properties.containsKey("muted")) {
268 source.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
270 if (properties.containsKey("volume")) {
271 source.setVolume(parseVolume(properties.get("volume")));
273 String monitorOf = properties.get("monitor_of");
274 if (monitorOf != null) {
275 source.setMonitorOf(client.getSink(Integer.valueOf(monitorOf)));
284 * parses the pulseaudio servers answer to the list-source-outputs command and returns a list of
285 * {@link SourceOutput} objects
287 * @param raw the given string from the pulseaudio server
288 * @return list of source-outputs
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) {
297 for (int i = 1; i < parts.length; i++) {
298 String[] lines = parts[i].split("\n");
299 Hashtable<String, String> properties = new Hashtable<>();
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());
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());
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")) {
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);
327 if (properties.containsKey("muted")) {
328 item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
330 if (properties.containsKey("volume")) {
331 item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
333 if (properties.containsKey("source")) {
334 item.setSource(client.getSource(Integer.valueOf(getNumberValue(properties.get("source")))));
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
350 private static int parseVolume(String vol) {
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));
359 LOGGER.debug("Unable to parse channel volume '{}'", channel);
363 return Math.round(volumeTotal / nChannels);
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
376 private static int getNumberValue(String raw) {
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));