2 * Copyright (c) 2010-2023 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"), properties.get("device.description"), properties,
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, properties.get("application.name"), properties,
202 client.getModule(getNumberValue(properties.get("module"))));
203 if (properties.containsKey("state")) {
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);
210 if (properties.containsKey("muted")) {
211 item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
213 if (properties.containsKey("volume")) {
214 item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
216 if (properties.containsKey("sink")) {
217 item.setSink(client.getSink(Integer.valueOf(getNumberValue(properties.get("sink")))));
226 * parses the pulseaudio servers answer to the list-sources command and returns a list of
227 * {@link Source} objects
229 * @param raw the given string from the pulseaudio server
230 * @return list of sources
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) {
239 for (int i = 1; i < parts.length; i++) {
240 String[] lines = parts[i].split("\n");
241 Hashtable<String, String> properties = new Hashtable<>();
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());
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());
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")) {
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);
269 if (properties.containsKey("muted")) {
270 source.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
272 if (properties.containsKey("volume")) {
273 source.setVolume(parseVolume(properties.get("volume")));
275 if (properties.containsKey("monitor_of")) {
276 String monitorOf = properties.get("monitor_of");
277 source.setMonitorOf(client.getSink(Integer.valueOf(monitorOf)));
286 * parses the pulseaudio servers answer to the list-source-outputs command and returns a list of
287 * {@link SourceOutput} objects
289 * @param raw the given string from the pulseaudio server
290 * @return list of source-outputs
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) {
299 for (int i = 1; i < parts.length; i++) {
300 String[] lines = parts[i].split("\n");
301 Hashtable<String, String> properties = new Hashtable<>();
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());
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());
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")) {
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);
329 if (properties.containsKey("muted")) {
330 item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
332 if (properties.containsKey("volume")) {
333 item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
335 if (properties.containsKey("source")) {
336 item.setSource(client.getSource(Integer.valueOf(getNumberValue(properties.get("source")))));
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
352 private static int parseVolume(String vol) {
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));
361 LOGGER.debug("Unable to parse channel volume '{}'", channel);
365 return Math.round(volumeTotal / nChannels);
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
378 private static int getNumberValue(@Nullable String raw) {
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));