added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.binding.pulseaudio/.classpath
Normal file
32
bundles/org.openhab.binding.pulseaudio/.classpath
Normal 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>
|
||||
23
bundles/org.openhab.binding.pulseaudio/.project
Normal file
23
bundles/org.openhab.binding.pulseaudio/.project
Normal 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>
|
||||
13
bundles/org.openhab.binding.pulseaudio/NOTICE
Normal file
13
bundles/org.openhab.binding.pulseaudio/NOTICE
Normal 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
|
||||
55
bundles/org.openhab.binding.pulseaudio/README.md
Normal file
55
bundles/org.openhab.binding.pulseaudio/README.md
Normal 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
|
||||
```
|
||||
|
||||
```
|
||||
-->
|
||||
17
bundles/org.openhab.binding.pulseaudio/pom.xml
Normal file
17
bundles/org.openhab.binding.pulseaudio/pom.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user