2 * Copyright (c) 2010-2022 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.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;
35 * Parsers for the pulseaudio return strings
37 * @author Tobias Bräutigam - Initial contribution
41 private static final Logger LOGGER = LoggerFactory.getLogger(Parser.class);
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]+).*$");
51 * parses the pulseaudio servers answer to the list-modules command and returns a list of
52 * {@link Module} objects
54 * @param raw the given string from the pulseaudio server
55 * @return list of modules
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) {
64 for (int i = 1; i < parts.length; i++) {
65 String[] lines = parts[i].split("\n");
66 Hashtable<String, String> properties = new Hashtable<>();
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());
74 id = Integer.valueOf(matcher.group(1));
75 properties.put(matcher.group(2).trim(), matcher.group(3).trim());
78 for (int j = 1; j < lines.length; j++) {
79 Matcher matcher = PATTERN.matcher(lines[j]);
81 properties.put(matcher.group(1).trim(), matcher.group(2).trim());
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"));
96 * parses the pulseaudio servers answer to the list-sinks command and returns a list of
97 * {@link Sink} objects
99 * @param raw the given string from the pulseaudio server
100 * @return list of sinks
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();
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<>();
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());
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());
130 if (properties.containsKey("name")) {
131 Sink sink = new Sink(id, properties.get("name"),
132 client.getModule(getNumberValue(properties.get("module"))));
133 if (properties.containsKey("state")) {
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);
140 if (properties.containsKey("muted")) {
141 sink.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
143 if (properties.containsKey("volume")) {
144 sink.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
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);
152 combinedSinks.add(sink);
154 sinks.put(sink.getUIDName(), sink);
157 for (Sink combinedSink : combinedSinks) {
158 for (String sinkName : combinedSink.getCombinedSinkNames()) {
159 combinedSink.addCombinedSink(sinks.get(sinkName));
162 return sinks.values();
166 * parses the pulseaudio servers answer to the list-sink-inputs command and returns a list of
167 * {@link SinkInput} objects
169 * @param raw the given string from the pulseaudio server
170 * @return list of sink-inputs
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) {
178 for (int i = 1; i < parts.length; i++) {
179 String[] lines = parts[i].split("\n");
180 Hashtable<String, String> properties = new Hashtable<>();
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());
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());
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, client.getModule(getNumberValue(properties.get("module"))));
202 if (properties.containsKey("state")) {
204 item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
205 } catch (IllegalArgumentException e) {
206 LOGGER.error("unhandled state {} in sink-input item #{}", properties.get("state"), id);
209 if (properties.containsKey("muted")) {
210 item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
212 if (properties.containsKey("volume")) {
213 item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
215 if (properties.containsKey("sink")) {
216 item.setSink(client.getSink(Integer.valueOf(getNumberValue(properties.get("sink")))));
225 * parses the pulseaudio servers answer to the list-sources command and returns a list of
226 * {@link Source} objects
228 * @param raw the given string from the pulseaudio server
229 * @return list of sources
231 public static List<Source> parseSources(String raw, PulseaudioClient client) {
232 List<Source> sources = new ArrayList<>();
233 String[] parts = raw.split("index: ");
234 if (parts.length <= 1) {
238 for (int i = 1; i < parts.length; i++) {
239 String[] lines = parts[i].split("\n");
240 Hashtable<String, String> properties = new Hashtable<>();
243 id = Integer.valueOf(lines[0].trim());
244 } catch (NumberFormatException e) {
245 // sometime the line feed is missing here
246 Matcher matcher = FALL_BACK_PATTERN.matcher(lines[0].trim());
247 if (matcher.find()) {
248 id = Integer.valueOf(matcher.group(1));
249 properties.put(matcher.group(2).trim(), matcher.group(3).trim());
252 for (int j = 1; j < lines.length; j++) {
253 Matcher matcher = PATTERN.matcher(lines[j]);
254 if (matcher.find()) {
255 properties.put(matcher.group(1).trim(), matcher.group(2).trim());
258 if (properties.containsKey("name")) {
259 Source source = new Source(id, properties.get("name"),
260 client.getModule(getNumberValue(properties.get("module"))));
261 if (properties.containsKey("state")) {
263 source.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
264 } catch (IllegalArgumentException e) {
265 LOGGER.error("unhandled state {} in source item #{}", properties.get("state"), id);
268 if (properties.containsKey("muted")) {
269 source.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
271 if (properties.containsKey("volume")) {
272 source.setVolume(parseVolume(properties.get("volume")));
274 if (properties.containsKey("monitor_of")) {
275 String monitorOf = properties.get("monitor_of");
276 source.setMonitorOf(client.getSink(Integer.valueOf(monitorOf)));
285 * parses the pulseaudio servers answer to the list-source-outputs command and returns a list of
286 * {@link SourceOutput} objects
288 * @param raw the given string from the pulseaudio server
289 * @return list of source-outputs
291 public static List<SourceOutput> parseSourceOutputs(String raw, PulseaudioClient client) {
292 List<SourceOutput> items = new ArrayList<>();
293 String[] parts = raw.split("index: ");
294 if (parts.length <= 1) {
298 for (int i = 1; i < parts.length; i++) {
299 String[] lines = parts[i].split("\n");
300 Hashtable<String, String> properties = new Hashtable<>();
303 id = Integer.valueOf(lines[0].trim());
304 } catch (NumberFormatException e) {
305 // sometime the line feed is missing here
306 Matcher matcher = FALL_BACK_PATTERN.matcher(lines[0].trim());
307 if (matcher.find()) {
308 id = Integer.valueOf(matcher.group(1));
309 properties.put(matcher.group(2).trim(), matcher.group(3).trim());
312 for (int j = 1; j < lines.length; j++) {
313 Matcher matcher = PATTERN.matcher(lines[j]);
314 if (matcher.find()) {
315 properties.put(matcher.group(1).trim(), matcher.group(2).trim());
318 if (properties.containsKey("source")) {
319 SourceOutput item = new SourceOutput(id, properties.get("source"),
320 client.getModule(getNumberValue(properties.get("module"))));
321 if (properties.containsKey("state")) {
323 item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
324 } catch (IllegalArgumentException e) {
325 LOGGER.error("unhandled state {} in source-output item #{}", properties.get("state"), id);
328 if (properties.containsKey("muted")) {
329 item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
331 if (properties.containsKey("volume")) {
332 item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
334 if (properties.containsKey("source")) {
335 item.setSource(client.getSource(Integer.valueOf(getNumberValue(properties.get("source")))));
344 * converts the volume value given by the pulseaudio server
345 * to a percentage value. The pulseaudio server sends 2 values for left and right channel volume
346 * e.g. 0: 80% 1: 80% which would be converted to 80
351 private static int parseVolume(String vol) {
354 for (String channel : vol.split(", ")) {
355 Matcher matcher = VOLUME_PATTERN.matcher(channel.trim());
356 if (matcher.find()) {
357 volumeTotal += Integer.valueOf(matcher.group(3));
360 LOGGER.debug("Unable to parse channel volume '{}'", channel);
364 return Math.round(volumeTotal / nChannels);
370 * sometimes the pulseaudio server "forgets" some line feeds which leeds to unparsable number values
371 * like 80NextProperty:
372 * this is a workaround to get the correct number value in these cases
377 private static int getNumberValue(@Nullable String raw) {
383 id = Integer.valueOf(raw.trim());
384 } catch (NumberFormatException e) {
385 Matcher matcher = NUMBER_VALUE_PATTERN.matcher(raw.trim());
386 if (matcher.find()) {
387 id = Integer.valueOf(matcher.group(1));