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(properties.get("muted").equalsIgnoreCase("yes"));
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 for (String sinkName : properties.get("combine.slaves").replace("\"", "").split(",")) {
146 sink.addCombinedSinkName(sinkName);
148 combinedSinks.add(sink);
150 sinks.put(sink.getUIDName(), sink);
153 for (Sink combinedSink : combinedSinks) {
154 for (String sinkName : combinedSink.getCombinedSinkNames()) {
155 combinedSink.addCombinedSink(sinks.get(sinkName));
158 return sinks.values();
162 * parses the pulseaudio servers answer to the list-sink-inputs command and returns a list of
163 * {@link SinkInput} objects
165 * @param raw the given string from the pulseaudio server
166 * @return list of sink-inputs
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) {
174 for (int i = 1; i < parts.length; i++) {
175 String[] lines = parts[i].split("\n");
176 Hashtable<String, String> properties = new Hashtable<>();
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());
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());
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")) {
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);
205 if (properties.containsKey("muted")) {
206 item.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
208 if (properties.containsKey("volume")) {
209 item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
211 if (properties.containsKey("sink")) {
212 item.setSink(client.getSink(Integer.valueOf(getNumberValue(properties.get("sink")))));
221 * parses the pulseaudio servers answer to the list-sources command and returns a list of
222 * {@link Source} objects
224 * @param raw the given string from the pulseaudio server
225 * @return list of sources
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) {
234 for (int i = 1; i < parts.length; i++) {
235 String[] lines = parts[i].split("\n");
236 Hashtable<String, String> properties = new Hashtable<>();
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());
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());
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")) {
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);
264 if (properties.containsKey("muted")) {
265 source.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
267 if (properties.containsKey("volume")) {
268 source.setVolume(parseVolume(properties.get("volume")));
270 String monitorOf = properties.get("monitor_of");
271 if (monitorOf != null) {
272 source.setMonitorOf(client.getSink(Integer.valueOf(monitorOf)));
281 * parses the pulseaudio servers answer to the list-source-outputs command and returns a list of
282 * {@link SourceOutput} objects
284 * @param raw the given string from the pulseaudio server
285 * @return list of source-outputs
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) {
294 for (int i = 1; i < parts.length; i++) {
295 String[] lines = parts[i].split("\n");
296 Hashtable<String, String> properties = new Hashtable<>();
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());
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());
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")) {
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);
324 if (properties.containsKey("muted")) {
325 item.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
327 if (properties.containsKey("volume")) {
328 item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
330 if (properties.containsKey("source")) {
331 item.setSource(client.getSource(Integer.valueOf(getNumberValue(properties.get("source")))));
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
347 private static int parseVolume(String vol) {
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));
356 LOGGER.debug("Unable to parse channel volume '{}'", channel);
360 return Math.round(volumeTotal / nChannels);
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
373 private static int getNumberValue(String raw) {
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));