/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.binding.pulseaudio.cli;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openhab.binding.pulseaudio.internal.PulseaudioClient;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
import org.openhab.binding.pulseaudio.internal.items.Module;
import org.openhab.binding.pulseaudio.internal.items.Sink;
import org.openhab.binding.pulseaudio.internal.items.SinkInput;
import org.openhab.binding.pulseaudio.internal.items.Source;
import org.openhab.binding.pulseaudio.internal.items.SourceOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Parsers for the pulseaudio return strings
*
* @author Tobias Bräutigam
* @since 1.2.0
*/
public class Parser {
private static final Logger logger = LoggerFactory.getLogger(Parser.class);
private static final Pattern pattern = Pattern.compile("^\\s+([a-z\\s._]+)[:=]\\s*<?\"?([^>\"]+)\"?>?$");
private static final Pattern volumePattern = Pattern.compile(
"^(0|front-left|mono):(\\s[0-9]+\\s/\\s)?\\s*([0-9]+)%\\s*(/\\s[\\-0-9]+,[0-9]{2}\\sdB,\\s*)?(1|front-right)?:?(\\s[0-9]+\\s/\\s)?\\s*([0-9]+)?%?\\s*(/\\s[\\-0-9]+,[0-9]{2}\\sdB)?.*$");
private static final Pattern fallBackPattern = Pattern
.compile("^([0-9]+)([a-z\\s._]+)[:=]\\s*<?\"?([^>\"]+)\"?>?$");
private static final Pattern numberValuePattern = Pattern.compile("^([0-9]+).*$");
/**
* parses the pulseaudio servers answer to the list-modules command and returns a list of
* {@link Module} objects
*
* @param raw the given string from the pulseaudio server
* @return list of modules
*/
public static List<Module> parseModules(String raw) {
List<Module> modules = new ArrayList<Module>();
// System.out.println(raw);
String[] parts = raw.split("index: ");
if (parts.length <= 1) {
return modules;
}
// skip first part
for (int i = 1; i < parts.length; i++) {
String[] lines = parts[i].split("\n");
Hashtable<String, String> properties = new Hashtable<String, String>();
int id = 0;
try {
id = Integer.valueOf(lines[0].trim());
} catch (NumberFormatException e) {
// sometime the line feed is missing here
Matcher matcher = fallBackPattern.matcher(lines[0].trim());
if (matcher.find()) {
id = Integer.valueOf(matcher.group(1));
properties.put(matcher.group(2).trim(), matcher.group(3).trim());
}
}
for (int j = 1; j < lines.length; j++) {
Matcher matcher = pattern.matcher(lines[j]);
if (matcher.find()) {
// System.out.println(matcher.group(1).trim()+": "+matcher.group(2).trim());
properties.put(matcher.group(1).trim(), matcher.group(2).trim());
}
}
if (properties.containsKey("name")) {
Module module = new Module(id, properties.get("name"));
if (properties.containsKey("argument")) {
module.setArgument(properties.get("argument"));
}
modules.add(module);
}
}
return modules;
}
/**
* parses the pulseaudio servers answer to the list-sinks command and returns a list of
* {@link Sink} objects
*
* @param raw the given string from the pulseaudio server
* @return list of sinks
*/
public static Collection<Sink> parseSinks(String raw, PulseaudioClient client) {
Hashtable<String, Sink> sinks = new Hashtable<String, Sink>();
// System.out.println(raw);
String[] parts = raw.split("index: ");
if (parts.length <= 1) {
return sinks.values();
}
// skip first part
List<Sink> combinedSinks = new ArrayList<Sink>();
for (int i = 1; i < parts.length; i++) {
String[] lines = parts[i].split("\n");
Hashtable<String, String> properties = new Hashtable<String, String>();
int id = 0;
try {
id = Integer.valueOf(lines[0].trim());
} catch (NumberFormatException e) {
// sometime the line feed is missing here
Matcher matcher = fallBackPattern.matcher(lines[0].trim());
if (matcher.find()) {
id = Integer.valueOf(matcher.group(1));
properties.put(matcher.group(2).trim(), matcher.group(3).trim());
}
}
for (int j = 1; j < lines.length; j++) {
Matcher matcher = pattern.matcher(lines[j]);
if (matcher.find()) {
// System.out.println(matcher.group(1).trim()+": "+matcher.group(2).trim());
properties.put(matcher.group(1).trim(), matcher.group(2).trim());
}
}
if (properties.containsKey("name")) {
Sink sink = new Sink(id, properties.get("name"),
client.getModule(getNumberValue(properties.get("module"))));
if (properties.containsKey("state")) {
try {
sink.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
} catch (IllegalArgumentException e) {
logger.error("unhandled state " + properties.get("state") + " in sink item #" + id);
}
}
if (properties.containsKey("muted")) {
sink.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
}
if (properties.containsKey("volume")) {
sink.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
}
if (properties.containsKey("combine.slaves")) {
// this is a combined sink, the combined sink object
// should
// be
for (String sinkName : properties.get("combine.slaves").replace("\"", "").split(",")) {
sink.addCombinedSinkName(sinkName);
}
combinedSinks.add(sink);
}
sinks.put(sink.getName(), sink);
}
}
for (Sink combinedSink : combinedSinks) {
for (String sinkName : combinedSink.getCombinedSinkNames()) {
combinedSink.addCombinedSink(sinks.get(sinkName));
}
}
return sinks.values();
}
/**
* parses the pulseaudio servers answer to the list-sink-inputs command and returns a list of
* {@link SinkInput} objects
*
* @param raw the given string from the pulseaudio server
* @return list of sink-inputs
*/
public static List<SinkInput> parseSinkInputs(String raw, PulseaudioClient client) {
List<SinkInput> items = new ArrayList<SinkInput>();
String[] parts = raw.split("index: ");
if (parts.length <= 1) {
return items;
}
// skip first part
for (int i = 1; i < parts.length; i++) {
String[] lines = parts[i].split("\n");
Hashtable<String, String> properties = new Hashtable<String, String>();
int id = 0;
try {
id = Integer.valueOf(lines[0].trim());
} catch (NumberFormatException e) {
// sometime the line feed is missing here
Matcher matcher = fallBackPattern.matcher(lines[0].trim());
if (matcher.find()) {
id = Integer.valueOf(matcher.group(1));
properties.put(matcher.group(2).trim(), matcher.group(3).trim());
}
}
for (int j = 1; j < lines.length; j++) {
Matcher matcher = pattern.matcher(lines[j]);
if (matcher.find()) {
properties.put(matcher.group(1).trim(), matcher.group(2).trim());
}
}
if (properties.containsKey("sink")) {
String name = properties.containsKey("media.name") ? properties.get("media.name")
: properties.get("sink");
SinkInput item = new SinkInput(id, name, client.getModule(getNumberValue(properties.get("module"))));
if (properties.containsKey("state")) {
try {
item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
} catch (IllegalArgumentException e) {
logger.error("unhandled state " + properties.get("state") + " in sink-input item #" + id);
}
}
if (properties.containsKey("muted")) {
item.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
}
if (properties.containsKey("volume")) {
item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
}
if (properties.containsKey("sink")) {
item.setSink(client.getSink(Integer.valueOf(parseVolume(properties.get("sink")))));
}
items.add(item);
}
}
return items;
}
/**
* parses the pulseaudio servers answer to the list-sources command and returns a list of
* {@link Source} objects
*
* @param raw the given string from the pulseaudio server
* @return list of sources
*/
public static List<Source> parseSources(String raw, PulseaudioClient client) {
List<Source> sources = new ArrayList<Source>();
// System.out.println(raw);
String[] parts = raw.split("index: ");
if (parts.length <= 1) {
return sources;
}
// skip first part
for (int i = 1; i < parts.length; i++) {
String[] lines = parts[i].split("\n");
Hashtable<String, String> properties = new Hashtable<String, String>();
int id = 0;
try {
id = Integer.valueOf(lines[0].trim());
} catch (NumberFormatException e) {
// sometime the line feed is missing here
Matcher matcher = fallBackPattern.matcher(lines[0].trim());
if (matcher.find()) {
id = Integer.valueOf(matcher.group(1));
properties.put(matcher.group(2).trim(), matcher.group(3).trim());
}
}
for (int j = 1; j < lines.length; j++) {
Matcher matcher = pattern.matcher(lines[j]);
if (matcher.find()) {
properties.put(matcher.group(1).trim(), matcher.group(2).trim());
}
}
if (properties.containsKey("name")) {
Source source = new Source(id, properties.get("name"),
client.getModule(getNumberValue(properties.get("module"))));
if (properties.containsKey("state")) {
try {
source.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
} catch (IllegalArgumentException e) {
logger.error("unhandled state " + properties.get("state") + " in source item #" + id);
}
}
if (properties.containsKey("muted")) {
source.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
}
if (properties.containsKey("volume")) {
source.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
}
if (properties.containsKey("monitor_of")) {
source.setMonitorOf(client.getSink(Integer.valueOf(parseVolume(properties.get("monitor_of")))));
}
sources.add(source);
}
}
return sources;
}
/**
* parses the pulseaudio servers answer to the list-source-outputs command and returns a list of
* {@link SourceOutput} objects
*
* @param raw the given string from the pulseaudio server
* @return list of source-outputs
*/
public static List<SourceOutput> parseSourceOutputs(String raw, PulseaudioClient client) {
List<SourceOutput> items = new ArrayList<SourceOutput>();
// System.out.println(raw);
String[] parts = raw.split("index: ");
if (parts.length <= 1) {
return items;
}
// skip first part
for (int i = 1; i < parts.length; i++) {
String[] lines = parts[i].split("\n");
Hashtable<String, String> properties = new Hashtable<String, String>();
int id = 0;
try {
id = Integer.valueOf(lines[0].trim());
} catch (NumberFormatException e) {
// sometime the line feed is missing here
Matcher matcher = fallBackPattern.matcher(lines[0].trim());
if (matcher.find()) {
id = Integer.valueOf(matcher.group(1));
properties.put(matcher.group(2).trim(), matcher.group(3).trim());
}
}
for (int j = 1; j < lines.length; j++) {
Matcher matcher = pattern.matcher(lines[j]);
if (matcher.find()) {
properties.put(matcher.group(1).trim(), matcher.group(2).trim());
}
}
if (properties.containsKey("source")) {
SourceOutput item = new SourceOutput(id, properties.get("source"),
client.getModule(getNumberValue(properties.get("module"))));
if (properties.containsKey("state")) {
try {
item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
} catch (IllegalArgumentException e) {
logger.error("unhandled state " + properties.get("state") + " in source-output item #" + id);
}
}
if (properties.containsKey("muted")) {
item.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
}
if (properties.containsKey("volume")) {
item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
}
if (properties.containsKey("sink")) {
item.setSource(client.getSource(Integer.valueOf(parseVolume(properties.get("source")))));
}
items.add(item);
}
}
return items;
}
/**
* converts the volume value given by the pulseaudio server
* to a percentage value. The pulseaudio server sends 2 values for left and right channel volume
* e.g. 0: 80% 1: 80% which would be converted to 80
*
* @param vol
* @return
*/
private static int parseVolume(String vol) {
Matcher matcher = volumePattern.matcher(vol);
if (matcher.find()) {
if (matcher.group(7) == null) {
return Integer.valueOf(matcher.group(3));
} else {
return Math.round((Integer.valueOf(matcher.group(3)) + Integer.valueOf(matcher.group(7))) / 2);
}
}
return 0;
}
/**
* sometimes the pulseaudio server "forgets" some line feeds which leeds to unparsable number values
* like 80NextProperty:
* this is a workaround to get the correct number value in these cases
*
* @param raw
* @return
*/
private static int getNumberValue(String raw) {
int id = -1;
if (raw == null) {
return 0;
}
try {
id = Integer.valueOf(raw.trim());
} catch (NumberFormatException e) {
Matcher matcher = numberValuePattern.matcher(raw.trim());
if (matcher.find()) {
id = Integer.valueOf(matcher.group(1));
}
}
return id;
}
}