added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.pulseaudio</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,55 @@
# Pulseaudio Binding
This binding integrates pulseaudio devices.
## Supported Things
The Pulseaudio bridge is required as a "bridge" for accessing any other Pulseaudio devices.
You need a running pulseaudio server whith module **module-cli-protocol-tcp** loaded and accessible by the server which runs your openHAB instance. The following pulseaudio devices are supported:
* Sink
* Source
* Sink-Input
* Source-Output
* Combined-Sink
## Discovery
The Pulseaudio bridge is discovered through mDNS in the local network.
## Thing Configuration
The Pulseaudio bridge requires the host (ip address or a hostname) and a port (default: 4712) as a configuration value in order for the binding to know where to access it.
You can use `pactl -s <ip-address|hostname> list-sinks | grep "name:"` to find the name of a sink.
## Channels
All devices support some of the following channels:
| Channel Type ID | Item Type | Description |
|-----------------|-----------|-------------------------------------------------------------------------|
| volume | Dimmer | Volume of an audio device in percent |
| mute | Switch | Mutes the device |
| state | String | Current state of the device (suspended, idle, running, corked, drained) |
| slaves | String | Slave sinks of a combined sink |
| routeToSink | String | Shows the sink a sink-input is currently routed to |
## Full Example
### pulseaudio.things
```
Bridge pulseaudio:bridge:<bridgname> "<Bridge Label>" @ "<Room>" [ host="<ipAddress>", port=4712 ] {
Things:
Thing sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3"] // this name corresponds to pactl list-sinks output
Thing source microphone "microphone" @ "Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"]
Thing sink-input openhabTTS "OH-Voice" @ "Room" [name="alsa_output.pci-0000_00_1f.3.hdmi-stereo-extra1"]
Thing source-output remotePulseSink "Other Room Speaker" @ "Other Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"]
Thing combined-sink hdmiAndAnalog "Zone 1+2" @ "Room" [name="combined"]
}
```
<!--
### pulseaudio.items
```
```
-->

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.pulseaudio</artifactId>
<name>openHAB Add-ons :: Bundles :: Pulseaudio Binding</name>
</project>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.pulseaudio-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-pulseaudio" description="Pulseaudio Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-mdns</feature>
<feature>openhab-transport-upnp</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.pulseaudio/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link PulseaudioBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Tobias Bräutigam - Initial contribution
*/
@NonNullByDefault
public class PulseaudioBindingConstants {
public static final String BINDING_ID = "pulseaudio";
// List of all Thing Type UIDs
public static final ThingTypeUID COMBINED_SINK_THING_TYPE = new ThingTypeUID(BINDING_ID, "combinedSink");
public static final ThingTypeUID SINK_THING_TYPE = new ThingTypeUID(BINDING_ID, "sink");
public static final ThingTypeUID SOURCE_THING_TYPE = new ThingTypeUID(BINDING_ID, "source");
public static final ThingTypeUID SINK_INPUT_THING_TYPE = new ThingTypeUID(BINDING_ID, "sinkInput");
public static final ThingTypeUID SOURCE_OUTPUT_THING_TYPE = new ThingTypeUID(BINDING_ID, "sourceOutput");
public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "bridge");
// List of all Channel ids
public static final String VOLUME_CHANNEL = "volume";
public static final String MUTE_CHANNEL = "mute";
public static final String STATE_CHANNEL = "state";
public static final String SLAVES_CHANNEL = "slaves";
public static final String ROUTE_TO_SINK_CHANNEL = "routeToSink";
// List of all Parameters
public static final String BRIDGE_PARAMETER_HOST = "host";
public static final String BRIDGE_PARAMETER_PORT = "port";
public static final String BRIDGE_PARAMETER_REFRESH_INTERVAL = "refresh";
public static final String DEVICE_PARAMETER_NAME = "name";
public static final Map<String, Boolean> TYPE_FILTERS = new HashMap<>();
static {
TYPE_FILTERS.put(SINK_THING_TYPE.getId(), true);
TYPE_FILTERS.put(SINK_INPUT_THING_TYPE.getId(), false);
TYPE_FILTERS.put(SOURCE_THING_TYPE.getId(), false);
TYPE_FILTERS.put(SOURCE_OUTPUT_THING_TYPE.getId(), false);
}
}

View File

@@ -0,0 +1,632 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal;
import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.pulseaudio.internal.cli.Parser;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig.State;
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;
/**
* The client connects to a pulseaudio server via TCP. It reads the current state of the
* pulseaudio server (available sinks, sources,...) and can send commands to the server.
* The syntax of the commands is the same as for the pactl command line tool provided by pulseaudio.
*
* On the pulseaudio server the module-cli-protocol-tcp has to be loaded.
*
* @author Tobias Bräutigam - Initial contribution
*/
public class PulseaudioClient {
private final Logger logger = LoggerFactory.getLogger(PulseaudioClient.class);
private String host;
private int port;
private Socket client;
private List<AbstractAudioDeviceConfig> items;
private List<Module> modules;
/**
* corresponding name to execute actions on sink items
*/
private static final String ITEM_SINK = "sink";
/**
* corresponding name to execute actions on source items
*/
private static final String ITEM_SOURCE = "source";
/**
* corresponding name to execute actions on sink-input items
*/
private static final String ITEM_SINK_INPUT = "sink-input";
/**
* corresponding name to execute actions on source-output items
*/
private static final String ITEM_SOURCE_OUTPUT = "source-output";
/**
* command to list the loaded modules
*/
private static final String CMD_LIST_MODULES = "list-modules";
/**
* command to list the sinks
*/
private static final String CMD_LIST_SINKS = "list-sinks";
/**
* command to list the sources
*/
private static final String CMD_LIST_SOURCES = "list-sources";
/**
* command to list the sink-inputs
*/
private static final String CMD_LIST_SINK_INPUTS = "list-sink-inputs";
/**
* command to list the source-outputs
*/
private static final String CMD_LIST_SOURCE_OUTPUTS = "list-source-outputs";
/**
* command to load a module
*/
private static final String CMD_LOAD_MODULE = "load-module";
/**
* command to unload a module
*/
private static final String CMD_UNLOAD_MODULE = "unload-module";
/**
* name of the module-combine-sink
*/
private static final String MODULE_COMBINE_SINK = "module-combine-sink";
public PulseaudioClient() throws IOException {
this("localhost", 4712);
}
public PulseaudioClient(String host, int port) throws IOException {
this.host = host;
this.port = port;
items = new ArrayList<>();
modules = new ArrayList<>();
connect();
update();
}
public boolean isConnected() {
return client.isConnected();
}
/**
* updates the item states and their relationships
*/
public void update() {
modules.clear();
modules.addAll(Parser.parseModules(listModules()));
items.clear();
if (TYPE_FILTERS.get(SINK_THING_TYPE.getId())) {
logger.debug("reading sinks");
items.addAll(Parser.parseSinks(listSinks(), this));
}
if (TYPE_FILTERS.get(SOURCE_THING_TYPE.getId())) {
logger.debug("reading sources");
items.addAll(Parser.parseSources(listSources(), this));
}
if (TYPE_FILTERS.get(SINK_INPUT_THING_TYPE.getId())) {
logger.debug("reading sink-inputs");
items.addAll(Parser.parseSinkInputs(listSinkInputs(), this));
}
if (TYPE_FILTERS.get(SOURCE_OUTPUT_THING_TYPE.getId())) {
logger.debug("reading source-outputs");
items.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this));
}
logger.debug("Pulseaudio server {}: {} modules and {} items updated", host, modules.size(), items.size());
}
private String listModules() {
return this.sendRawRequest(CMD_LIST_MODULES);
}
private String listSinks() {
return this.sendRawRequest(CMD_LIST_SINKS);
}
private String listSources() {
return this.sendRawRequest(CMD_LIST_SOURCES);
}
private String listSinkInputs() {
return this.sendRawRequest(CMD_LIST_SINK_INPUTS);
}
private String listSourceOutputs() {
return this.sendRawRequest(CMD_LIST_SOURCE_OUTPUTS);
}
/**
* retrieves a module by its id
*
* @param id
* @return the corresponding {@link Module} to the given <code>id</code>
*/
public Module getModule(int id) {
for (Module module : modules) {
if (module.getId() == id) {
return module;
}
}
return null;
}
/**
* send the command directly to the pulseaudio server
* for a list of available commands please take a look at
* http://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/CLI
*
* @param command
*/
public void sendCommand(String command) {
sendRawCommand(command);
}
/**
* retrieves a {@link Sink} by its name
*
* @return the corresponding {@link Sink} to the given <code>name</code>
*/
public Sink getSink(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getPaName().equalsIgnoreCase(name) && item instanceof Sink) {
return (Sink) item;
}
}
return null;
}
/**
* retrieves a {@link Sink} by its id
*
* @return the corresponding {@link Sink} to the given <code>id</code>
*/
public Sink getSink(int id) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getId() == id && item instanceof Sink) {
return (Sink) item;
}
}
return null;
}
/**
* retrieves a {@link SinkInput} by its name
*
* @return the corresponding {@link SinkInput} to the given <code>name</code>
*/
public SinkInput getSinkInput(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getPaName().equalsIgnoreCase(name) && item instanceof SinkInput) {
return (SinkInput) item;
}
}
return null;
}
/**
* retrieves a {@link SinkInput} by its id
*
* @return the corresponding {@link SinkInput} to the given <code>id</code>
*/
public SinkInput getSinkInput(int id) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getId() == id && item instanceof SinkInput) {
return (SinkInput) item;
}
}
return null;
}
/**
* retrieves a {@link Source} by its name
*
* @return the corresponding {@link Source} to the given <code>name</code>
*/
public Source getSource(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getPaName().equalsIgnoreCase(name) && item instanceof Source) {
return (Source) item;
}
}
return null;
}
/**
* retrieves a {@link Source} by its id
*
* @return the corresponding {@link Source} to the given <code>id</code>
*/
public Source getSource(int id) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getId() == id && item instanceof Source) {
return (Source) item;
}
}
return null;
}
/**
* retrieves a {@link SourceOutput} by its name
*
* @return the corresponding {@link SourceOutput} to the given <code>name</code>
*/
public SourceOutput getSourceOutput(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getPaName().equalsIgnoreCase(name) && item instanceof SourceOutput) {
return (SourceOutput) item;
}
}
return null;
}
/**
* retrieves a {@link SourceOutput} by its id
*
* @return the corresponding {@link SourceOutput} to the given <code>id</code>
*/
public SourceOutput getSourceOutput(int id) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getId() == id && item instanceof SourceOutput) {
return (SourceOutput) item;
}
}
return null;
}
/**
* retrieves a {@link AbstractAudioDeviceConfig} by its name
*
* @return the corresponding {@link AbstractAudioDeviceConfig} to the given <code>name</code>
*/
public AbstractAudioDeviceConfig getGenericAudioItem(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getPaName().equalsIgnoreCase(name)) {
return item;
}
}
return null;
}
public List<AbstractAudioDeviceConfig> getItems() {
return items;
}
/**
* changes the <code>mute</code> state of the corresponding {@link Sink}
*
* @param item the {@link Sink} to handle
* @param mute mutes the sink if true, unmutes if false
*/
public void setMute(AbstractAudioDeviceConfig item, boolean mute) {
if (item == null) {
return;
}
String itemCommandName = getItemCommandName(item);
if (itemCommandName == null) {
return;
}
String muteString = mute ? "1" : "0";
sendRawCommand("set-" + itemCommandName + "-mute " + item.getId() + " " + muteString);
// update internal data
item.setMuted(mute);
}
/**
* change the volume of a {@link AbstractAudioDeviceConfig}
*
* @param item the {@link AbstractAudioDeviceConfig} to handle
* @param vol the new volume value the {@link AbstractAudioDeviceConfig} should be changed to (possible values from
* 0 - 65536)
*/
public void setVolume(AbstractAudioDeviceConfig item, int vol) {
if (item == null) {
return;
}
String itemCommandName = getItemCommandName(item);
if (itemCommandName == null) {
return;
}
sendRawCommand("set-" + itemCommandName + "-volume " + item.getId() + " " + vol);
item.setVolume(Math.round(100f / 65536f * vol));
}
/**
* returns the item names that can be used in commands
*
* @param item
* @return
*/
private String getItemCommandName(AbstractAudioDeviceConfig item) {
if (item instanceof Sink) {
return ITEM_SINK;
} else if (item instanceof Source) {
return ITEM_SOURCE;
} else if (item instanceof SinkInput) {
return ITEM_SINK_INPUT;
} else if (item instanceof SourceOutput) {
return ITEM_SOURCE_OUTPUT;
}
return null;
}
/**
* change the volume of a {@link AbstractAudioDeviceConfig}
*
* @param item the {@link AbstractAudioDeviceConfig} to handle
* @param vol the new volume percent value the {@link AbstractAudioDeviceConfig} should be changed to (possible
* values from 0 - 100)
*/
public void setVolumePercent(AbstractAudioDeviceConfig item, int vol) {
if (item == null) {
return;
}
if (vol <= 100) {
vol = toAbsoluteVolume(vol);
}
setVolume(item, vol);
}
/**
* transform a percent volume to a value that can be send to the pulseaudio server (0-65536)
*
* @param percent
* @return
*/
private int toAbsoluteVolume(int percent) {
return (int) Math.round(65536f / 100f * Double.valueOf(percent));
}
/**
* changes the combined sinks slaves to the given <code>sinks</code>
*
* @param combinedSink the combined sink which slaves should be changed
* @param sinks the list of new slaves
*/
public void setCombinedSinkSlaves(Sink combinedSink, List<Sink> sinks) {
if (combinedSink == null || !combinedSink.isCombinedSink()) {
return;
}
List<String> slaves = new ArrayList<>();
for (Sink sink : sinks) {
slaves.add(sink.getPaName());
}
// 1. delete old combined-sink
sendRawCommand(CMD_UNLOAD_MODULE + " " + combinedSink.getModule().getId());
// 2. add new combined-sink with same name and all slaves
sendRawCommand(CMD_LOAD_MODULE + " " + MODULE_COMBINE_SINK + " sink_name=" + combinedSink.getPaName()
+ " slaves=" + StringUtils.join(slaves, ","));
// 3. update internal data structure because the combined sink has a new number + other slaves
update();
}
/**
* sets the sink a sink-input should be routed to
*
* @param sinkInput the sink-input to be rerouted
* @param sink the new sink the sink-input should be routed to
*/
public void moveSinkInput(SinkInput sinkInput, Sink sink) {
if (sinkInput == null || sink == null) {
return;
}
sendRawCommand("move-sink-input " + sinkInput.getId() + " " + sink.getId());
sinkInput.setSink(sink);
}
/**
* sets the sink a source-output should be routed to
*
* @param sourceOutput the source-output to be rerouted
* @param source the new source the source-output should be routed to
*/
public void moveSourceOutput(SourceOutput sourceOutput, Source source) {
if (sourceOutput == null || source == null) {
return;
}
sendRawCommand("move-sink-input " + sourceOutput.getId() + " " + source.getId());
sourceOutput.setSource(source);
}
/**
* suspend a source
*
* @param source the source which state should be changed
* @param suspend suspend it or not
*/
public void suspendSource(Source source, boolean suspend) {
if (source == null) {
return;
}
if (suspend) {
sendRawCommand("suspend-source " + source.getId() + " 1");
source.setState(State.SUSPENDED);
} else {
sendRawCommand("suspend-source " + source.getId() + " 0");
// unsuspending the source could result in different states (RUNNING,IDLE,...)
// update to get the new state
update();
}
}
/**
* suspend a sink
*
* @param sink the sink which state should be changed
* @param suspend suspend it or not
*/
public void suspendSink(Sink sink, boolean suspend) {
if (sink == null) {
return;
}
if (suspend) {
sendRawCommand("suspend-sink " + sink.getId() + " 1");
sink.setState(State.SUSPENDED);
} else {
sendRawCommand("suspend-sink " + sink.getId() + " 0");
// unsuspending the sink could result in different states (RUNNING,IDLE,...)
// update to get the new state
update();
}
}
/**
* changes the combined sinks slaves to the given <code>sinks</code>
*
* @param combinedSinkName the combined sink which slaves should be changed
* @param sinks the list of new slaves
*/
public void setCombinedSinkSlaves(String combinedSinkName, List<Sink> sinks) {
if (getSink(combinedSinkName) != null) {
return;
}
List<String> slaves = new ArrayList<>();
for (Sink sink : sinks) {
slaves.add(sink.getPaName());
}
// add new combined-sink with same name and all slaves
sendRawCommand(CMD_LOAD_MODULE + " " + MODULE_COMBINE_SINK + " sink_name=" + combinedSinkName + " slaves="
+ StringUtils.join(slaves, ","));
// update internal data structure because the combined sink is new
update();
}
private void sendRawCommand(String command) {
checkConnection();
try {
PrintStream out = new PrintStream(client.getOutputStream(), true);
logger.trace("sending command {} to pa-server {}", command, host);
out.print(command + "\r\n");
out.close();
client.close();
} catch (IOException e) {
logger.error("{}", e.getLocalizedMessage(), e);
}
}
private String sendRawRequest(String command) {
logger.trace("_sendRawRequest({})", command);
checkConnection();
String result = "";
try {
PrintStream out = new PrintStream(client.getOutputStream(), true);
out.print(command + "\r\n");
InputStream instr = client.getInputStream();
try {
byte[] buff = new byte[1024];
int retRead = 0;
int lc = 0;
do {
retRead = instr.read(buff);
lc++;
if (retRead > 0) {
String line = new String(buff, 0, retRead);
// System.out.println("'"+line+"'");
if (line.endsWith(">>> ") && lc > 1) {
result += line.substring(0, line.length() - 4);
break;
}
result += line.trim();
}
} while (retRead > 0);
} catch (SocketTimeoutException e) {
// Timeout -> as newer PA versions (>=5.0) do not send the >>> we have no chance
// to detect the end of the answer, except by this timeout
} catch (IOException e) {
logger.error("Exception while reading socket: {}", e.getMessage());
}
instr.close();
out.close();
client.close();
return result;
} catch (IOException e) {
logger.error("{}", e.getLocalizedMessage(), e);
}
return result;
}
private void checkConnection() {
if (client == null || client.isClosed() || !client.isConnected()) {
try {
connect();
} catch (IOException e) {
logger.error("{}", e.getLocalizedMessage(), e);
}
}
}
/**
* Connects to the pulseaudio server (timeout 500ms)
*/
private void connect() throws IOException {
try {
client = new Socket(host, port);
client.setSoTimeout(500);
} catch (UnknownHostException e) {
logger.error("unknown socket host {}", host);
} catch (SocketException e) {
logger.error("{}", e.getLocalizedMessage(), e);
}
}
/**
* Disconnects from the pulseaudio server
*/
public void disconnect() {
if (client != null) {
try {
client.close();
} catch (IOException e) {
logger.error("{}", e.getLocalizedMessage(), e);
}
}
}
}

View File

@@ -0,0 +1,140 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.openhab.binding.pulseaudio.internal.discovery.PulseaudioDeviceDiscoveryService;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PulseaudioHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Tobias Bräutigam - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.pulseaudio")
public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(PulseaudioHandlerFactory.class);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.concat(PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.stream(),
PulseaudioHandler.SUPPORTED_THING_TYPES_UIDS.stream()).collect(Collectors.toSet()));
private final Map<ThingHandler, ServiceRegistration<?>> discoveryServiceReg = new HashMap<>();
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
public Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration, ThingUID thingUID,
ThingUID bridgeUID) {
if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
return super.createThing(thingTypeUID, configuration, thingUID, null);
}
if (PulseaudioHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
ThingUID deviceUID = getPulseaudioDeviceUID(thingTypeUID, thingUID, configuration, bridgeUID);
return super.createThing(thingTypeUID, configuration, deviceUID, bridgeUID);
}
throw new IllegalArgumentException("The thing type " + thingTypeUID + " is not supported by the binding.");
}
private void registerDeviceDiscoveryService(PulseaudioBridgeHandler paBridgeHandler) {
PulseaudioDeviceDiscoveryService discoveryService = new PulseaudioDeviceDiscoveryService(paBridgeHandler);
discoveryService.activate();
this.discoveryServiceReg.put(paBridgeHandler,
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
}
private ThingUID getPulseaudioDeviceUID(ThingTypeUID thingTypeUID, ThingUID thingUID, Configuration configuration,
ThingUID bridgeUID) {
if (thingUID == null) {
String name = (String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME);
return new ThingUID(thingTypeUID, name, bridgeUID.getId());
}
return thingUID;
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (this.discoveryServiceReg.containsKey(thingHandler)) {
PulseaudioDeviceDiscoveryService service = (PulseaudioDeviceDiscoveryService) bundleContext
.getService(discoveryServiceReg.get(thingHandler).getReference());
service.deactivate();
discoveryServiceReg.get(thingHandler).unregister();
discoveryServiceReg.remove(thingHandler);
}
super.removeHandler(thingHandler);
}
@Override
protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
PulseaudioBridgeHandler handler = new PulseaudioBridgeHandler((Bridge) thing);
registerDeviceDiscoveryService(handler);
return handler;
} else if (PulseaudioHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
return new PulseaudioHandler(thing);
}
return null;
}
@Override
protected synchronized void activate(ComponentContext componentContext) {
super.activate(componentContext);
modified(componentContext);
}
protected synchronized void modified(ComponentContext componentContext) {
Dictionary<String, ?> properties = componentContext.getProperties();
logger.info("pulseaudio configuration update received ({})", properties);
if (properties == null) {
return;
}
Enumeration<String> e = properties.keys();
while (e.hasMoreElements()) {
String k = e.nextElement();
if (PulseaudioBindingConstants.TYPE_FILTERS.containsKey(k)) {
PulseaudioBindingConstants.TYPE_FILTERS.put(k, (boolean) properties.get(k));
}
logger.debug("update received {}: {}", k, properties.get(k));
}
}
}

View File

@@ -0,0 +1,387 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal.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 - Initial contribution
*/
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 VOLUME_PATTERN = Pattern
.compile("^([\\w\\-]+):( *[\\d]+ \\/)? *([\\d]+)% *\\/? *([\\d\\-., dB]+)?$");
private static final Pattern FALL_BACK_PATTERN = Pattern
.compile("^([0-9]+)([a-z\\s._]+)[:=]\\s*<?\"?([^>\"]+)\"?>?$");
private static final Pattern NUMBER_VALUE_PATTERN = 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<>();
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<>();
int id = 0;
try {
id = Integer.valueOf(lines[0].trim());
} catch (NumberFormatException e) {
// sometime the line feed is missing here
Matcher matcher = FALL_BACK_PATTERN.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")) {
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[] parts = raw.split("index: ");
if (parts.length <= 1) {
return sinks.values();
}
// skip first part
List<Sink> combinedSinks = new ArrayList<>();
for (int i = 1; i < parts.length; i++) {
String[] lines = parts[i].split("\n");
Hashtable<String, String> properties = new Hashtable<>();
int id = 0;
try {
id = Integer.valueOf(lines[0].trim());
} catch (NumberFormatException e) {
// sometime the line feed is missing here
Matcher matcher = FALL_BACK_PATTERN.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")) {
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 {} in sink item #{}", properties.get("state"), 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.getUIDName(), 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<>();
String[] parts = raw.split("index: ");
if (parts.length <= 1) {
return items;
}
for (int i = 1; i < parts.length; i++) {
String[] lines = parts[i].split("\n");
Hashtable<String, String> properties = new Hashtable<>();
int id = 0;
try {
id = Integer.valueOf(lines[0].trim());
} catch (NumberFormatException e) {
// sometime the line feed is missing here
Matcher matcher = FALL_BACK_PATTERN.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 {} in sink-input item #{}", properties.get("state"), 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(getNumberValue(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<>();
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<>();
int id = 0;
try {
id = Integer.valueOf(lines[0].trim());
} catch (NumberFormatException e) {
// sometime the line feed is missing here
Matcher matcher = FALL_BACK_PATTERN.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 {} in source item #{}", properties.get("state"), 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(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<>();
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<>();
int id = 0;
try {
id = Integer.valueOf(lines[0].trim());
} catch (NumberFormatException e) {
// sometime the line feed is missing here
Matcher matcher = FALL_BACK_PATTERN.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 {} in source-output item #{}", properties.get("state"), 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("source")) {
item.setSource(client.getSource(Integer.valueOf(getNumberValue(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) {
int volumeTotal = 0;
int nChannels = 0;
for (String channel : vol.split(", ")) {
Matcher matcher = VOLUME_PATTERN.matcher(channel.trim());
if (matcher.find()) {
volumeTotal += Integer.valueOf(matcher.group(3));
nChannels++;
} else {
LOGGER.debug("Unable to parse channel volume '{}'", channel);
}
}
if (nChannels > 0) {
return Math.round(volumeTotal / nChannels);
}
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 = NUMBER_VALUE_PATTERN.matcher(raw.trim());
if (matcher.find()) {
id = Integer.valueOf(matcher.group(1));
}
}
return id;
}
}

View File

@@ -0,0 +1,114 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal.discovery;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
import org.openhab.binding.pulseaudio.internal.handler.DeviceStatusListener;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
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.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PulseaudioDeviceDiscoveryService} class is used to discover Pulseaudio
* devices on a Pulseaudio server.
*
* @author Tobias Bräutigam - Initial contribution
*/
public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService implements DeviceStatusListener {
private final Logger logger = LoggerFactory.getLogger(PulseaudioDeviceDiscoveryService.class);
private PulseaudioBridgeHandler pulseaudioBridgeHandler;
public PulseaudioDeviceDiscoveryService(PulseaudioBridgeHandler pulseaudioBridgeHandler) {
super(PulseaudioHandler.SUPPORTED_THING_TYPES_UIDS, 10, true);
this.pulseaudioBridgeHandler = pulseaudioBridgeHandler;
}
public void activate() {
pulseaudioBridgeHandler.registerDeviceStatusListener(this);
}
@Override
public void deactivate() {
pulseaudioBridgeHandler.unregisterDeviceStatusListener(this);
}
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return PulseaudioHandler.SUPPORTED_THING_TYPES_UIDS;
}
@Override
public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) {
String uidName = device.getPaName();
logger.debug("device {} found", device);
ThingTypeUID thingType = null;
Map<String, Object> properties = new HashMap<>();
// All devices need this parameter
properties.put(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME, uidName);
if (device instanceof Sink) {
if (((Sink) device).isCombinedSink()) {
thingType = PulseaudioBindingConstants.COMBINED_SINK_THING_TYPE;
} else {
thingType = PulseaudioBindingConstants.SINK_THING_TYPE;
}
} else if (device instanceof SinkInput) {
thingType = PulseaudioBindingConstants.SINK_INPUT_THING_TYPE;
} else if (device instanceof Source) {
thingType = PulseaudioBindingConstants.SOURCE_THING_TYPE;
} else if (device instanceof SourceOutput) {
thingType = PulseaudioBindingConstants.SOURCE_OUTPUT_THING_TYPE;
}
if (thingType != null) {
logger.trace("Adding new pulseaudio {} with name '{}' to smarthome inbox",
device.getClass().getSimpleName(), uidName);
ThingUID thingUID = new ThingUID(thingType, bridge.getUID(), device.getUIDName());
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withBridge(bridge.getUID()).withLabel(device.getUIDName()).build();
thingDiscovered(discoveryResult);
}
}
@Override
protected void startScan() {
// this can be ignored here as we discover via the PulseaudioClient.update() mechanism
}
@Override
public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) {
// this can be ignored here
}
@Override
public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) {
// this can be ignored here
}
}

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal.discovery;
import java.io.IOException;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.jmdns.ServiceInfo;
import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PulseaudioDiscoveryParticipant} is responsible processing the
* results of searches for mDNS services of type _pulse-server._tcp.local.
*
* @author Tobias Bräutigam - Initial contribution
*/
@Component(immediate = true)
public class PulseaudioDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(PulseaudioDiscoveryParticipant.class);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS;
}
@Override
public DiscoveryResult createResult(ServiceInfo info) {
DiscoveryResult result = null;
ThingUID uid = getThingUID(info);
if (uid != null) {
Map<String, Object> properties = new HashMap<>(3);
String label = "Pulseaudio server";
try {
label = info.getName();
} catch (Exception e) {
// ignore and use default label
}
// remove the domain from the name
String hostname = info.getServer().replace("." + info.getDomain() + ".", "");
try (Socket testSocket = new Socket(hostname, 4712)) {
logger.debug("testing connection to pulseaudio server {}:4712", hostname);
if (testSocket.isConnected()) {
properties.put(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST, hostname);
// we do not read the port here because the given port is 4713 and we need 4712 to query the server
result = DiscoveryResultBuilder.create(uid).withProperties(properties).withLabel(label).build();
logger.trace("Created a DiscoveryResult for device '{}' on host '{}'", info.getName(), hostname);
}
return result;
} catch (IOException e) {
result = null;
}
}
return result;
}
@Override
public ThingUID getThingUID(ServiceInfo info) {
if (info != null) {
logger.debug("ServiceInfo: {}", info);
if (info.getType() != null) {
if (info.getType().equals(getServiceType())) {
logger.trace("Discovered a pulseaudio server thing with name '{}'", info.getName());
return new ThingUID(PulseaudioBindingConstants.BRIDGE_THING_TYPE,
info.getName().replace("@", "_AT_"));
}
}
}
return null;
}
@Override
public String getServiceType() {
return "_pulse-server._tcp.local.";
}
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal.handler;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingUID;
/**
* The {@link DeviceStatusListener} is notified when a device status has changed
* or a device has been removed or added.
*
* @author Tobias Bräutigam - Initial contribution
*
*/
public interface DeviceStatusListener {
/**
* This method is called whenever the state of the given device has changed.
*
* @param bridge The Pulseaudio bridge the changed device is connected to.
* @param device The device which received the state update.
*/
public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device);
/**
* This method us called whenever a device is removed.
*
* @param bridge The Pulseaudio bridge the removed device was connected to.
* @param device The device which is removed.
*/
public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device);
/**
* This method us called whenever a device is added.
*
* @param bridge The Pulseaudio bridge the added device was connected to.
* @param device The device which is added.
*/
public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device);
}

View File

@@ -0,0 +1,173 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal.handler;
import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
import org.openhab.binding.pulseaudio.internal.PulseaudioClient;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link PulseaudioBridgeHandler} is the handler for a Pulseaudio server and
* connects it to the framework.
*
* @author Tobias Bräutigam - Initial contribution
*
*/
public class PulseaudioBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(PulseaudioBridgeHandler.class);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.singleton(PulseaudioBindingConstants.BRIDGE_THING_TYPE);
public String host = "localhost";
public int port = 4712;
public int refreshInterval = 30000;
private PulseaudioClient client;
private List<DeviceStatusListener> deviceStatusListeners = new CopyOnWriteArrayList<>();
private HashSet<String> lastActiveDevices = new HashSet<>();
private ScheduledFuture<?> pollingJob;
private Runnable pollingRunnable = () -> {
client.update();
for (AbstractAudioDeviceConfig device : client.getItems()) {
if (lastActiveDevices != null && lastActiveDevices.contains(device.getPaName())) {
for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
try {
deviceStatusListener.onDeviceStateChanged(getThing().getUID(), device);
} catch (Exception e) {
logger.error("An exception occurred while calling the DeviceStatusListener", e);
}
}
} else {
for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
try {
deviceStatusListener.onDeviceAdded(getThing(), device);
deviceStatusListener.onDeviceStateChanged(getThing().getUID(), device);
} catch (Exception e) {
logger.error("An exception occurred while calling the DeviceStatusListener", e);
}
lastActiveDevices.add(device.getPaName());
}
}
}
};
public PulseaudioBridgeHandler(Bridge bridge) {
super(bridge);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
client.update();
} else {
logger.warn("received invalid command for pulseaudio bridge '{}'.", host);
}
}
private synchronized void startAutomaticRefresh() {
if (pollingJob == null || pollingJob.isCancelled()) {
pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, refreshInterval, TimeUnit.MILLISECONDS);
}
}
public AbstractAudioDeviceConfig getDevice(String name) {
return client.getGenericAudioItem(name);
}
public PulseaudioClient getClient() {
return client;
}
@Override
public void initialize() {
logger.debug("Initializing Pulseaudio handler.");
Configuration conf = this.getConfig();
if (conf.get(BRIDGE_PARAMETER_HOST) != null) {
this.host = String.valueOf(conf.get(BRIDGE_PARAMETER_HOST));
}
if (conf.get(BRIDGE_PARAMETER_PORT) != null) {
this.port = ((BigDecimal) conf.get(BRIDGE_PARAMETER_PORT)).intValue();
}
if (conf.get(BRIDGE_PARAMETER_REFRESH_INTERVAL) != null) {
this.refreshInterval = ((BigDecimal) conf.get(BRIDGE_PARAMETER_REFRESH_INTERVAL)).intValue();
}
if (host != null && !host.isEmpty()) {
Runnable connectRunnable = () -> {
try {
client = new PulseaudioClient(host, port);
if (client.isConnected()) {
updateStatus(ThingStatus.ONLINE);
logger.info("Established connection to Pulseaudio server on Host '{}':'{}'.", host, port);
startAutomaticRefresh();
}
} catch (IOException e) {
logger.error("Couldn't connect to Pulsaudio server [Host '{}':'{}']: {}", host, port,
e.getLocalizedMessage());
updateStatus(ThingStatus.OFFLINE);
}
};
scheduler.schedule(connectRunnable, 0, TimeUnit.SECONDS);
} else {
logger.warn(
"Couldn't connect to Pulseaudio server because of missing connection parameters [Host '{}':'{}'].",
host, port);
updateStatus(ThingStatus.OFFLINE);
}
}
@Override
public void dispose() {
pollingJob.cancel(true);
client.disconnect();
super.dispose();
}
public boolean registerDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
if (deviceStatusListener == null) {
throw new IllegalArgumentException("It's not allowed to pass a null deviceStatusListener.");
}
return deviceStatusListeners.add(deviceStatusListener);
}
public boolean unregisterDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
return deviceStatusListeners.remove(deviceStatusListener);
}
}

View File

@@ -0,0 +1,266 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal.handler;
import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
import org.openhab.binding.pulseaudio.internal.items.Sink;
import org.openhab.binding.pulseaudio.internal.items.SinkInput;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PulseaudioHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Tobias Bräutigam - Initial contribution
*/
public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusListener {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(SINK_THING_TYPE, COMBINED_SINK_THING_TYPE, SINK_INPUT_THING_TYPE,
SOURCE_THING_TYPE, SOURCE_OUTPUT_THING_TYPE).collect(Collectors.toSet()));
private int refresh = 60; // refresh every minute as default
private ScheduledFuture<?> refreshJob;
private PulseaudioBridgeHandler bridgeHandler;
private final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class);
private String name;
public PulseaudioHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
Configuration config = getThing().getConfiguration();
name = (String) config.get(DEVICE_PARAMETER_NAME);
// until we get an update put the Thing offline
updateStatus(ThingStatus.OFFLINE);
deviceOnlineWatchdog();
}
@Override
public void dispose() {
if (refreshJob != null && !refreshJob.isCancelled()) {
refreshJob.cancel(true);
refreshJob = null;
}
updateStatus(ThingStatus.OFFLINE);
bridgeHandler = null;
logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
super.dispose();
}
private void deviceOnlineWatchdog() {
Runnable runnable = () -> {
try {
PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler();
if (bridgeHandler != null) {
if (bridgeHandler.getDevice(name) == null) {
updateStatus(ThingStatus.OFFLINE);
bridgeHandler = null;
} else {
updateStatus(ThingStatus.ONLINE);
}
} else {
logger.debug("Bridge for pulseaudio device {} not found.", name);
updateStatus(ThingStatus.OFFLINE);
}
} catch (Exception e) {
logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
bridgeHandler = null;
}
};
refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refresh, TimeUnit.SECONDS);
}
private synchronized PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
if (this.bridgeHandler == null) {
Bridge bridge = getBridge();
if (bridge == null) {
logger.debug("Required bridge not defined for device {}.", name);
return null;
}
ThingHandler handler = bridge.getHandler();
if (handler instanceof PulseaudioBridgeHandler) {
this.bridgeHandler = (PulseaudioBridgeHandler) handler;
this.bridgeHandler.registerDeviceStatusListener(this);
} else {
logger.debug("No available bridge handler found for device {} bridge {} .", name, bridge.getUID());
return null;
}
}
return this.bridgeHandler;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
if (bridge == null) {
logger.warn("pulseaudio server bridge handler not found. Cannot handle command without bridge.");
return;
}
if (command instanceof RefreshType) {
bridge.handleCommand(channelUID, command);
return;
}
AbstractAudioDeviceConfig device = bridge.getDevice(name);
if (device == null) {
logger.warn("device {} not found", name);
updateStatus(ThingStatus.OFFLINE);
bridgeHandler = null;
return;
} else {
State updateState = UnDefType.UNDEF;
if (channelUID.getId().equals(VOLUME_CHANNEL)) {
if (command instanceof IncreaseDecreaseType) {
// refresh to get the current volume level
bridge.getClient().update();
device = bridge.getDevice(name);
int volume = device.getVolume();
if (command.equals(IncreaseDecreaseType.INCREASE)) {
volume = Math.min(100, volume + 5);
}
if (command.equals(IncreaseDecreaseType.DECREASE)) {
volume = Math.max(0, volume - 5);
}
bridge.getClient().setVolumePercent(device, volume);
updateState = new PercentType(volume);
} else if (command instanceof PercentType) {
DecimalType volume = (DecimalType) command;
bridge.getClient().setVolumePercent(device, volume.intValue());
updateState = (PercentType) command;
} else if (command instanceof DecimalType) {
// set volume
DecimalType volume = (DecimalType) command;
bridge.getClient().setVolume(device, volume.intValue());
updateState = (DecimalType) command;
}
} else if (channelUID.getId().equals(MUTE_CHANNEL)) {
if (command instanceof OnOffType) {
bridge.getClient().setMute(device, OnOffType.ON.equals(command));
updateState = (OnOffType) command;
}
} else if (channelUID.getId().equals(SLAVES_CHANNEL)) {
if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
if (command instanceof StringType) {
List<Sink> slaves = new ArrayList<>();
for (String slaveName : command.toString().split(",")) {
Sink slave = bridge.getClient().getSink(slaveName.trim());
if (slave != null) {
slaves.add(slave);
}
}
if (!slaves.isEmpty()) {
bridge.getClient().setCombinedSinkSlaves(((Sink) device), slaves);
}
}
} else {
logger.error("{} is no combined sink", device);
}
} else if (channelUID.getId().equals(ROUTE_TO_SINK_CHANNEL)) {
if (device instanceof SinkInput) {
Sink newSink = null;
if (command instanceof DecimalType) {
newSink = bridge.getClient().getSink(((DecimalType) command).intValue());
} else {
newSink = bridge.getClient().getSink(command.toString());
}
if (newSink != null) {
logger.debug("rerouting {} to {}", device, newSink);
bridge.getClient().moveSinkInput(((SinkInput) device), newSink);
updateState = new StringType(newSink.getPaName());
} else {
logger.error("no sink {} found", command.toString());
}
}
}
logger.trace("updating {} to {}", channelUID, updateState);
if (!updateState.equals(UnDefType.UNDEF)) {
updateState(channelUID, updateState);
}
}
}
@Override
public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) {
if (device.getPaName().equals(name)) {
updateStatus(ThingStatus.ONLINE);
logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL);
updateState(VOLUME_CHANNEL, new PercentType(device.getVolume()));
updateState(MUTE_CHANNEL, device.isMuted() ? OnOffType.ON : OnOffType.OFF);
updateState(STATE_CHANNEL,
device.getState() != null ? new StringType(device.getState().toString()) : new StringType("-"));
if (device instanceof SinkInput) {
updateState(ROUTE_TO_SINK_CHANNEL,
((SinkInput) device).getSink() != null
? new StringType(((SinkInput) device).getSink().getPaName())
: new StringType("-"));
}
if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
updateState(SLAVES_CHANNEL,
new StringType(StringUtils.join(((Sink) device).getCombinedSinkNames(), ",")));
}
}
}
@Override
public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) {
if (device.getPaName().equals(name)) {
bridgeHandler.unregisterDeviceStatusListener(this);
bridgeHandler = null;
updateStatus(ThingStatus.OFFLINE);
}
}
@Override
public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) {
logger.trace("new device discovered {} by {}", device, bridge);
}
}

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal.items;
/**
* GenericAudioItems are any kind of items that deal with audio data and can be
* muted or their volume can be changed.
*
* @author Tobias Bräutigam - Initial contribution
*/
public abstract class AbstractAudioDeviceConfig extends AbstractDeviceConfig {
public enum State {
SUSPENDED,
IDLE,
RUNNING,
CORKED,
DRAINED
}
protected State state;
protected boolean muted;
protected int volume;
protected Module module;
public AbstractAudioDeviceConfig(int id, String name, Module module) {
super(id, name);
this.module = module;
}
public Module getModule() {
return module;
}
public void setModule(Module module) {
this.module = module;
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
}
public boolean isMuted() {
return muted;
}
public void setMuted(boolean muted) {
this.muted = muted;
}
public int getVolume() {
return volume;
}
public void setVolume(int volume) {
this.volume = volume;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + " #" + id + " (Module: " + module + ") " + name + ", muted: " + muted
+ ", state: " + state + ", volume: " + volume;
}
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal.items;
/**
* Abstract root class for all items in an pulseaudio server. Every item in a
* pulseaudio server has a name and a unique id which can be inherited by this
* class.
*
* @author Tobias Bräutigam - Initial contribution
*/
public abstract class AbstractDeviceConfig {
protected int id;
protected String name;
public AbstractDeviceConfig(int id, String name) {
this.id = id;
this.name = name;
}
/**
* returns the internal id of this device
*
* @return
*/
public int getId() {
return id;
}
public String getUIDName() {
return name.replaceAll("[^A-Za-z0-9_]", "_");
}
public String getPaName() {
return name;
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal.items;
/**
* In order to add a {@link Sink} to the pulseaudio server you have to
* load a corresponding module. Current Module objects are needed to
* be able to remove sinks from the pulseaudio server.
*
* @author Tobias Bräutigam - Initial contribution
*/
public class Module extends AbstractDeviceConfig {
private String argument;
public Module(int id, String name) {
super(id, name);
}
public String getArgument() {
return argument;
}
public void setArgument(String argument) {
this.argument = argument;
}
@Override
public String toString() {
return name;
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal.items;
import java.util.ArrayList;
import java.util.List;
/**
* On a Pulseaudio server Sinks are the devices the audio streams are routed to
* (playback devices) it can be a single item or a group of other Sinks that are
* combined to one playback device
*
* @author Tobias Bräutigam - Initial contribution
*/
public class Sink extends AbstractAudioDeviceConfig {
protected List<String> combinedSinkNames;
protected List<Sink> combinedSinks;
public Sink(int id, String name, Module module) {
super(id, name, module);
combinedSinkNames = new ArrayList<>();
combinedSinks = new ArrayList<>();
}
public void addCombinedSinkName(String name) {
this.combinedSinkNames.add(name);
}
public boolean isCombinedSink() {
return !combinedSinkNames.isEmpty();
}
public List<String> getCombinedSinkNames() {
return combinedSinkNames;
}
public List<Sink> getCombinedSinks() {
return combinedSinks;
}
public void setCombinedSinks(List<Sink> combinedSinks) {
this.combinedSinks = combinedSinks;
}
public void addCombinedSink(Sink sink) {
if (sink != null) {
this.combinedSinks.add(sink);
}
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal.items;
/**
* A SinkInput is an audio stream which can be routed to a {@link Sink}
*
* @author Tobias Bräutigam - Initial contribution
*/
public class SinkInput extends AbstractAudioDeviceConfig {
private Sink sink;
public SinkInput(int id, String name, Module module) {
super(id, name, module);
}
public Sink getSink() {
return sink;
}
public void setSink(Sink sink) {
this.sink = sink;
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal.items;
/**
* A Source is a device which is the source of an audio stream (recording
* device) For example microphones or line-in jacks.
*
* @author Tobias Bräutigam - Initial contribution
*/
public class Source extends AbstractAudioDeviceConfig {
protected Sink monitorOf;
public Source(int id, String name, Module module) {
super(id, name, module);
}
public Sink getMonitorOf() {
return monitorOf;
}
public void setMonitorOf(Sink sink) {
this.monitorOf = sink;
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal.items;
/**
* A SourceOutput is the audio stream which is produced by a (@link Source}
*
* @author Tobias Bräutigam - Initial contribution
*/
public class SourceOutput extends AbstractAudioDeviceConfig {
private Source source;
public SourceOutput(int id, String name, Module module) {
super(id, name, module);
}
public Source getSource() {
return source;
}
public void setSource(Source source) {
this.source = source;
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="pulseaudio" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Pulseaudio Binding</name>
<description>This is the binding for Pulseaudio.</description>
<author>Tobias Bräutigam</author>
</binding:binding>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="binding:pulseaudio">
<parameter name="sink" type="boolean">
<label>Import Sinks</label>
<description>Activate the import of sink elements.</description>
<default>true</default>
</parameter>
<parameter name="sinkInput" type="boolean">
<label>Import Sink Inputs</label>
<description>Activate the import of sink-input elements.</description>
<default>false</default>
</parameter>
<parameter name="source" type="boolean">
<label>Import Sources</label>
<description>Activate the import of source elements.</description>
<default>false</default>
</parameter>
<parameter name="sourceOutput" type="boolean">
<label>Import Source Outputs</label>
<description>Activate the import of source-output elements.</description>
<default>false</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="pulseaudio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="bridge">
<label>Pulseaudio Server</label>
<description>This bridge represents a pulseaudio server.</description>
<config-description>
<parameter name="host" type="text">
<label>Hostname</label>
<description>Hostname or IP address of the pulseaudio server</description>
<default>localhost</default>
<required>true</required>
</parameter>
<parameter name="port" type="integer">
<label>Port</label>
<description>Port of the pulseaudio server</description>
<default>4712</default>
<required>false</required>
</parameter>
<parameter name="refreshInterval" type="integer">
<label>Refresh Interval</label>
<description>The refresh interval in ms which is used to poll given pulseaudio server.</description>
<default>30000</default>
<required>false</required>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="pulseaudio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="volume">
<item-type>Dimmer</item-type>
<label>Volume</label>
<description>Volume of an audio item in percent</description>
<category>SoundVolume</category>
<state min="0" max="100" step="1" pattern="%d" readOnly="false">
</state>
</channel-type>
<channel-type id="mute">
<item-type>Switch</item-type>
<label>Mute</label>
<description>Mutes the device</description>
</channel-type>
<channel-type id="state" advanced="true">
<item-type>String</item-type>
<label>State</label>
<description>Current state of the device</description>
<state readOnly="true">
<options>
<option value="SUSPENDED">Suspended</option>
<option value="IDLE">Idle</option>
<option value="RUNNING">Running</option>
<option value="CORKED">Corked</option>
<option value="DRAINED">Drained</option>
</options>
</state>
</channel-type>
<channel-type id="slaves" advanced="true">
<item-type>String</item-type>
<label>Slaves</label>
<description>Slave sinks of a combined sink</description>
</channel-type>
<channel-type id="routeToSink" advanced="true">
<item-type>String</item-type>
<label>Route to Sink</label>
<description>Shows the sink a sink-input is currently routed to</description>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="pulseaudio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="combinedSink">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>A Pulseaudio Combined Sink</label>
<description>represents a group of pulseaudio sinks, which are combined for synchronous audio</description>
<channels>
<channel id="volume" typeId="volume"/>
<channel id="mute" typeId="mute"/>
<channel id="state" typeId="state"/>
<channel id="slaves" typeId="slaves"/>
</channels>
<config-description>
<parameter name="name" type="text">
<label>Name</label>
<description>The name of the combined sink.</description>
<required>true</required>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="pulseaudio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="sinkInput">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>A Pulseaudio Sink-input</label>
<description>represents a pulseaudio sink-input</description>
<channels>
<channel id="volume" typeId="volume"/>
<channel id="mute" typeId="mute"/>
<channel id="state" typeId="state"/>
<channel id="routeToSink" typeId="routeToSink"/>
</channels>
<config-description>
<parameter name="name" type="text">
<label>Name</label>
<description>The name of one specific device.</description>
<required>true</required>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="pulseaudio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="sink">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>A Pulseaudio Sink</label>
<description>represents a pulseaudio sink</description>
<channels>
<channel id="volume" typeId="volume"/>
<channel id="mute" typeId="mute"/>
<channel id="state" typeId="state"/>
</channels>
<config-description>
<parameter name="name" type="text">
<label>Name</label>
<description>The name of one specific device.</description>
<required>true</required>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="pulseaudio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="sourceOutput">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>A Pulseaudio Source Output</label>
<description>represents a pulseaudio source-output</description>
<channels>
<channel id="volume" typeId="volume"/>
<channel id="mute" typeId="mute"/>
<channel id="state" typeId="state"/>
</channels>
<config-description>
<parameter name="name" type="text">
<label>Name</label>
<description>The name of one specific device.</description>
<required>true</required>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="pulseaudio"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="source">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>A Pulseaudio Source</label>
<description>represents a pulseaudio source</description>
<channels>
<channel id="volume" typeId="volume"/>
<channel id="mute" typeId="mute"/>
<channel id="state" typeId="state"/>
</channels>
<config-description>
<parameter name="name" type="text">
<label>Name</label>
<description>The name of one specific device.</description>
<required>true</required>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>