mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
* [pulseaudio] Add pulseaudio sink as openhab audio sink (#1895) This add to the pulseaudio binding the capability to use "pulseaudio sink" as an "openhab sink" to output sound from openhab to a pulse audio server on the network. You need to load module-simple-protocol-tcp sink in addition to the usual module-cli-protocol-tcp, and enable the sink in the thing configuration. Closes #1895 Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com> * Small corrections after review And getting rid of some other compilation warnings Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com> * Fix some registration errors and allow the binding to load the simple module remotely Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com> * Small corrections after reviews initialize audiosink in a thread with scheduler.submit clear some warning related code. Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com> Better interruptexception handling * Fix two small concurrency bugs Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com> Co-authored-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
This commit is contained in:
parent
89d735bb0f
commit
c3b29e0fe6
@ -6,7 +6,7 @@ This binding integrates pulseaudio devices.
|
||||
|
||||
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:
|
||||
You need a running pulseaudio server with module **module-cli-protocol-tcp** loaded and accessible by the server which runs your openHAB instance. The following pulseaudio devices are supported:
|
||||
|
||||
* Sink
|
||||
* Source
|
||||
@ -35,12 +35,18 @@ All devices support some of the following channels:
|
||||
| slaves | String | Slave sinks of a combined sink |
|
||||
| routeToSink | String | Shows the sink a sink-input is currently routed to |
|
||||
|
||||
## Audio sink
|
||||
|
||||
Sink things can register themselves as audio sink in openHAB. MP3 and WAV files are supported.
|
||||
Use the appropriate parameter in the sink thing to activate this possibility (activateSimpleProtocolSink).
|
||||
This requires the module **module-simple-protocol-tcp** to be present on the server which runs your openHAB instance. The binding will try to command (if not discovered first) the load of this module on the pulseaudio server.
|
||||
|
||||
## 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 sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3", activateSimpleProtocolSink="true", simpleProtocolSinkPort="4711"] // the 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"]
|
||||
|
@ -14,4 +14,25 @@
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Pulseaudio Binding</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.googlecode.soundlibs</groupId>
|
||||
<artifactId>mp3spi</artifactId>
|
||||
<version>1.9.5.4</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.googlecode.soundlibs</groupId>
|
||||
<artifactId>jlayer</artifactId>
|
||||
<version>1.0.1.4</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.googlecode.soundlibs</groupId>
|
||||
<artifactId>tritonus-share</artifactId>
|
||||
<version>0.3.7.4</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
@ -7,5 +7,8 @@
|
||||
<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>
|
||||
<bundle dependency="true">mvn:com.googlecode.soundlibs/tritonus-share/0.3.7.4</bundle>
|
||||
<bundle dependency="true">mvn:com.googlecode.soundlibs/mp3spi/1.9.5.4</bundle>
|
||||
<bundle dependency="true">mvn:com.googlecode.soundlibs/jlayer/1.0.1.4</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
|
@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
|
||||
import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
|
||||
|
||||
import javax.sound.sampled.AudioInputStream;
|
||||
import javax.sound.sampled.UnsupportedAudioFileException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
|
||||
import org.openhab.core.audio.AudioFormat;
|
||||
import org.openhab.core.audio.AudioSink;
|
||||
import org.openhab.core.audio.AudioStream;
|
||||
import org.openhab.core.audio.FixedLengthAudioStream;
|
||||
import org.openhab.core.audio.UnsupportedAudioFormatException;
|
||||
import org.openhab.core.audio.UnsupportedAudioStreamException;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The audio sink for openhab, implemented by a connection to a pulseaudio sink
|
||||
*
|
||||
* @author Gwendal Roulleau - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PulseAudioAudioSink implements AudioSink {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class);
|
||||
|
||||
private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
|
||||
private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
|
||||
|
||||
private PulseaudioHandler pulseaudioHandler;
|
||||
|
||||
private @Nullable Socket clientSocket;
|
||||
|
||||
static {
|
||||
SUPPORTED_FORMATS.add(AudioFormat.WAV);
|
||||
SUPPORTED_FORMATS.add(AudioFormat.MP3);
|
||||
SUPPORTED_STREAMS.add(FixedLengthAudioStream.class);
|
||||
}
|
||||
|
||||
public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler) {
|
||||
this.pulseaudioHandler = pulseaudioHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return pulseaudioHandler.getThing().getUID().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getLabel(@Nullable Locale locale) {
|
||||
return pulseaudioHandler.getThing().getLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert MP3 to PCM, as this is the only possible format
|
||||
*
|
||||
* @param input
|
||||
* @return
|
||||
*/
|
||||
private @Nullable InputStream getPCMStreamFromMp3Stream(InputStream input) {
|
||||
try {
|
||||
MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader();
|
||||
AudioInputStream sourceAIS = mpegAudioFileReader.getAudioInputStream(input);
|
||||
javax.sound.sampled.AudioFormat sourceFormat = sourceAIS.getFormat();
|
||||
|
||||
MpegFormatConversionProvider mpegconverter = new MpegFormatConversionProvider();
|
||||
javax.sound.sampled.AudioFormat convertFormat = new javax.sound.sampled.AudioFormat(
|
||||
javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(), 16,
|
||||
sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false);
|
||||
|
||||
return mpegconverter.getAudioInputStream(convertFormat, sourceAIS);
|
||||
|
||||
} catch (IOException | UnsupportedAudioFileException e) {
|
||||
logger.warn("Cannot convert this mp3 stream to pcm stream: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to pulseaudio with the simple protocol
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws InterruptedException when interrupted during the loading module wait
|
||||
*/
|
||||
public void connectIfNeeded() throws IOException, InterruptedException {
|
||||
Socket clientSocketLocal = clientSocket;
|
||||
if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) {
|
||||
String host = pulseaudioHandler.getHost();
|
||||
int port = pulseaudioHandler.getSimpleTcpPort();
|
||||
clientSocket = new Socket(host, port);
|
||||
clientSocket.setSoTimeout(500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the socket to pulseaudio simple protocol
|
||||
*/
|
||||
public void disconnect() {
|
||||
if (clientSocket != null) {
|
||||
try {
|
||||
clientSocket.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(@Nullable AudioStream audioStream)
|
||||
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
|
||||
|
||||
if (audioStream == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
InputStream audioInputStream = null;
|
||||
try {
|
||||
|
||||
if (AudioFormat.MP3.isCompatible(audioStream.getFormat())) {
|
||||
audioInputStream = getPCMStreamFromMp3Stream(audioStream);
|
||||
} else if (AudioFormat.WAV.isCompatible(audioStream.getFormat())) {
|
||||
audioInputStream = audioStream;
|
||||
} else {
|
||||
throw new UnsupportedAudioFormatException("pulseaudio audio sink can only play pcm or mp3 stream",
|
||||
audioStream.getFormat());
|
||||
}
|
||||
|
||||
for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
|
||||
try {
|
||||
connectIfNeeded();
|
||||
if (audioInputStream != null && clientSocket != null) {
|
||||
// send raw audio to the socket and to pulse audio
|
||||
audioInputStream.transferTo(clientSocket.getOutputStream());
|
||||
break;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown
|
||||
if (countAttempt == 2) { // we won't retry : log and quit
|
||||
if (logger.isWarnEnabled()) {
|
||||
String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown";
|
||||
logger.warn(
|
||||
"Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
|
||||
pulseaudioHandler.getHost(), port, e.getMessage());
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (InterruptedException ie) {
|
||||
logger.info("Interrupted during sink audio connection: {}", ie.getMessage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
if (audioInputStream != null) {
|
||||
audioInputStream.close();
|
||||
}
|
||||
audioStream.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<AudioFormat> getSupportedFormats() {
|
||||
return SUPPORTED_FORMATS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<? extends AudioStream>> getSupportedStreams() {
|
||||
return SUPPORTED_STREAMS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PercentType getVolume() {
|
||||
return new PercentType(pulseaudioHandler.getLastVolume());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVolume(PercentType volume) {
|
||||
pulseaudioHandler.setVolume(volume.intValue());
|
||||
}
|
||||
}
|
@ -51,6 +51,11 @@ public class PulseaudioBindingConstants {
|
||||
public static final String BRIDGE_PARAMETER_REFRESH_INTERVAL = "refresh";
|
||||
|
||||
public static final String DEVICE_PARAMETER_NAME = "name";
|
||||
public static final String DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION = "activateSimpleProtocolSink";
|
||||
public static final String DEVICE_PARAMETER_AUDIO_SINK_PORT = "simpleProtocolSinkPort";
|
||||
|
||||
public static final String MODULE_SIMPLE_PROTOCOL_TCP_NAME = "module-simple-protocol-tcp";
|
||||
public static final int MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT = 4711;
|
||||
|
||||
public static final Map<String, Boolean> TYPE_FILTERS = new HashMap<>();
|
||||
|
||||
|
@ -24,7 +24,10 @@ import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
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;
|
||||
@ -138,28 +141,30 @@ public class PulseaudioClient {
|
||||
/**
|
||||
* updates the item states and their relationships
|
||||
*/
|
||||
public void update() {
|
||||
modules.clear();
|
||||
modules.addAll(Parser.parseModules(listModules()));
|
||||
public synchronized void update() {
|
||||
// one step copy
|
||||
modules = new ArrayList<Module>(Parser.parseModules(listModules()));
|
||||
|
||||
items.clear();
|
||||
if (TYPE_FILTERS.get(SINK_THING_TYPE.getId())) {
|
||||
List<AbstractAudioDeviceConfig> newItems = new ArrayList<>(); // prepare new list before assigning it
|
||||
newItems.clear();
|
||||
if (Optional.ofNullable(TYPE_FILTERS.get(SINK_THING_TYPE.getId())).orElse(false)) {
|
||||
logger.debug("reading sinks");
|
||||
items.addAll(Parser.parseSinks(listSinks(), this));
|
||||
newItems.addAll(Parser.parseSinks(listSinks(), this));
|
||||
}
|
||||
if (TYPE_FILTERS.get(SOURCE_THING_TYPE.getId())) {
|
||||
if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_THING_TYPE.getId())).orElse(false)) {
|
||||
logger.debug("reading sources");
|
||||
items.addAll(Parser.parseSources(listSources(), this));
|
||||
newItems.addAll(Parser.parseSources(listSources(), this));
|
||||
}
|
||||
if (TYPE_FILTERS.get(SINK_INPUT_THING_TYPE.getId())) {
|
||||
if (Optional.ofNullable(TYPE_FILTERS.get(SINK_INPUT_THING_TYPE.getId())).orElse(false)) {
|
||||
logger.debug("reading sink-inputs");
|
||||
items.addAll(Parser.parseSinkInputs(listSinkInputs(), this));
|
||||
newItems.addAll(Parser.parseSinkInputs(listSinkInputs(), this));
|
||||
}
|
||||
if (TYPE_FILTERS.get(SOURCE_OUTPUT_THING_TYPE.getId())) {
|
||||
if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_OUTPUT_THING_TYPE.getId())).orElse(false)) {
|
||||
logger.debug("reading source-outputs");
|
||||
items.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this));
|
||||
newItems.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this));
|
||||
}
|
||||
logger.debug("Pulseaudio server {}: {} modules and {} items updated", host, modules.size(), items.size());
|
||||
logger.debug("Pulseaudio server {}: {} modules and {} items updated", host, modules.size(), newItems.size());
|
||||
items = newItems;
|
||||
}
|
||||
|
||||
private String listModules() {
|
||||
@ -377,6 +382,74 @@ public class PulseaudioClient {
|
||||
item.setVolume(Math.round(100f / 65536f * vol));
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate or load (if needed) the simple protocol tcp module for the given sink
|
||||
* and returns the port.
|
||||
* The module loading (if needed) will be tried several times, on a new random port each time.
|
||||
*
|
||||
* @param item the sink we are searching for
|
||||
* @param simpleTcpPortPref the port to use if we have to load the module
|
||||
* @return the port on which the module is listening
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public Optional<Integer> loadModuleSimpleProtocolTcpIfNeeded(AbstractAudioDeviceConfig item,
|
||||
Integer simpleTcpPortPref) throws InterruptedException {
|
||||
int currentTry = 0;
|
||||
int simpleTcpPortToTry = simpleTcpPortPref;
|
||||
do {
|
||||
Optional<Integer> simplePort = findSimpleProtocolTcpModule(item);
|
||||
|
||||
if (simplePort.isPresent()) {
|
||||
return simplePort;
|
||||
} else {
|
||||
sendRawCommand("load-module module-simple-protocol-tcp sink=" + item.getPaName() + " port="
|
||||
+ simpleTcpPortToTry);
|
||||
simpleTcpPortToTry = new Random().nextInt(64512) + 1024; // a random port above 1024
|
||||
}
|
||||
Thread.sleep(100);
|
||||
currentTry++;
|
||||
} while (currentTry < 3);
|
||||
|
||||
logger.warn("The pulseaudio binding tried 3 times to load the module-simple-protocol-tcp"
|
||||
+ " on random port on the pulseaudio server and give up trying");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a simple protocol module corresponding to the given sink in argument
|
||||
* and returns the port it listens to
|
||||
*
|
||||
* @param item
|
||||
* @return
|
||||
*/
|
||||
private Optional<Integer> findSimpleProtocolTcpModule(AbstractAudioDeviceConfig item) {
|
||||
update();
|
||||
|
||||
List<Module> modulesCopy = new ArrayList<Module>(modules);
|
||||
return modulesCopy.stream() // iteration on modules
|
||||
.filter(module -> MODULE_SIMPLE_PROTOCOL_TCP_NAME.equals(module.getPaName())) // filter on module name
|
||||
.filter(module -> extractArgumentFromLine("sink", module.getArgument()) // extract sink in argument
|
||||
.map(sinkName -> sinkName.equals(item.getPaName())).orElse(false)) // filter on sink name
|
||||
.findAny() // get a corresponding module
|
||||
.map(module -> extractArgumentFromLine("port", module.getArgument())
|
||||
.orElse(Integer.toString(MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT))) // get port
|
||||
.map(portS -> Integer.parseInt(portS));
|
||||
}
|
||||
|
||||
private @NonNull Optional<@NonNull String> extractArgumentFromLine(String argumentWanted, String argumentLine) {
|
||||
String argument = null;
|
||||
int startPortIndex = argumentLine.indexOf(argumentWanted + "=");
|
||||
if (startPortIndex != -1) {
|
||||
startPortIndex = startPortIndex + argumentWanted.length() + 1;
|
||||
int endPortIndex = argumentLine.indexOf(" ", startPortIndex);
|
||||
if (endPortIndex == -1) {
|
||||
endPortIndex = argumentLine.length();
|
||||
}
|
||||
argument = argumentLine.substring(startPortIndex, endPortIndex);
|
||||
}
|
||||
return Optional.ofNullable(argument);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the item names that can be used in commands
|
||||
*
|
||||
@ -404,13 +477,14 @@ public class PulseaudioClient {
|
||||
* values from 0 - 100)
|
||||
*/
|
||||
public void setVolumePercent(AbstractAudioDeviceConfig item, int vol) {
|
||||
int volumeToSet = vol;
|
||||
if (item == null) {
|
||||
return;
|
||||
}
|
||||
if (vol <= 100) {
|
||||
vol = toAbsoluteVolume(vol);
|
||||
if (volumeToSet <= 100) {
|
||||
volumeToSet = toAbsoluteVolume(volumeToSet);
|
||||
}
|
||||
setVolume(item, vol);
|
||||
setVolume(item, volumeToSet);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -583,6 +657,8 @@ public class PulseaudioClient {
|
||||
} 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 (SocketException e) {
|
||||
logger.warn("Socket exception while sending pulseaudio command: {}", e.getMessage());
|
||||
} catch (IOException e) {
|
||||
logger.error("Exception while reading socket: {}", e.getMessage());
|
||||
}
|
||||
@ -619,7 +695,7 @@ public class PulseaudioClient {
|
||||
} catch (NoRouteToHostException e) {
|
||||
logger.error("no route to host {}", host);
|
||||
} catch (SocketException e) {
|
||||
logger.error("{}", e.getLocalizedMessage(), e);
|
||||
logger.error("cannot connect to host {} : {}", host, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,25 +92,28 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
@Override
|
||||
protected void removeHandler(ThingHandler thingHandler) {
|
||||
if (this.discoveryServiceReg.containsKey(thingHandler)) {
|
||||
ServiceRegistration<?> serviceRegistration = this.discoveryServiceReg.get(thingHandler);
|
||||
if (serviceRegistration != null) {
|
||||
PulseaudioDeviceDiscoveryService service = (PulseaudioDeviceDiscoveryService) bundleContext
|
||||
.getService(discoveryServiceReg.get(thingHandler).getReference());
|
||||
.getService(serviceRegistration.getReference());
|
||||
service.deactivate();
|
||||
discoveryServiceReg.get(thingHandler).unregister();
|
||||
discoveryServiceReg.remove(thingHandler);
|
||||
serviceRegistration.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 new PulseaudioHandler(thing, bundleContext);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -135,15 +135,18 @@ public class Parser {
|
||||
}
|
||||
}
|
||||
if (properties.containsKey("muted")) {
|
||||
sink.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
|
||||
sink.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
|
||||
}
|
||||
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);
|
||||
String sinkNames = properties.get("combine.slaves");
|
||||
if (sinkNames != null) {
|
||||
for (String sinkName : sinkNames.replace("\"", "").split(",")) {
|
||||
sink.addCombinedSinkName(sinkName);
|
||||
}
|
||||
}
|
||||
combinedSinks.add(sink);
|
||||
}
|
||||
@ -203,7 +206,7 @@ public class Parser {
|
||||
}
|
||||
}
|
||||
if (properties.containsKey("muted")) {
|
||||
item.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
|
||||
item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
|
||||
}
|
||||
if (properties.containsKey("volume")) {
|
||||
item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
|
||||
@ -262,7 +265,7 @@ public class Parser {
|
||||
}
|
||||
}
|
||||
if (properties.containsKey("muted")) {
|
||||
source.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
|
||||
source.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
|
||||
}
|
||||
if (properties.containsKey("volume")) {
|
||||
source.setVolume(parseVolume(properties.get("volume")));
|
||||
@ -322,7 +325,7 @@ public class Parser {
|
||||
}
|
||||
}
|
||||
if (properties.containsKey("muted")) {
|
||||
item.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
|
||||
item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
|
||||
}
|
||||
if (properties.containsKey("volume")) {
|
||||
item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
|
||||
|
@ -73,7 +73,6 @@ public class PulseaudioDiscoveryParticipant implements MDNSDiscoveryParticipant
|
||||
}
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
result = null;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
@ -155,8 +155,12 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler {
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
pollingJob.cancel(true);
|
||||
client.disconnect();
|
||||
if (pollingJob != null) {
|
||||
pollingJob.cancel(true);
|
||||
}
|
||||
if (client != null) {
|
||||
client.disconnect();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -14,18 +14,26 @@ 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.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.openhab.binding.pulseaudio.internal.PulseAudioAudioSink;
|
||||
import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
|
||||
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.audio.AudioSink;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
@ -44,6 +52,8 @@ 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.osgi.framework.BundleContext;
|
||||
import org.osgi.framework.ServiceRegistration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -68,8 +78,17 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
|
||||
|
||||
private String name;
|
||||
|
||||
public PulseaudioHandler(Thing thing) {
|
||||
private PulseAudioAudioSink audioSink;
|
||||
|
||||
private Integer savedVolume;
|
||||
|
||||
private Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
|
||||
|
||||
private BundleContext bundleContext;
|
||||
|
||||
public PulseaudioHandler(Thing thing, BundleContext bundleContext) {
|
||||
super(thing);
|
||||
this.bundleContext = bundleContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -80,6 +99,42 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
|
||||
// until we get an update put the Thing offline
|
||||
updateStatus(ThingStatus.OFFLINE);
|
||||
deviceOnlineWatchdog();
|
||||
|
||||
// if it's a SINK thing, then maybe we have to activate the audio sink
|
||||
if (PulseaudioBindingConstants.SINK_THING_TYPE.equals(thing.getThingTypeUID())) {
|
||||
// check the property to see if we it's enabled :
|
||||
Boolean sinkActivated = (Boolean) thing.getConfiguration()
|
||||
.get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION);
|
||||
if (sinkActivated != null && sinkActivated) {
|
||||
audioSinkSetup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void audioSinkSetup() {
|
||||
final PulseaudioHandler thisHandler = this;
|
||||
scheduler.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Register the sink as an audio sink in openhab
|
||||
logger.trace("Registering an audio sink for pulse audio sink thing {}", thing.getUID());
|
||||
PulseAudioAudioSink audioSink = new PulseAudioAudioSink(thisHandler);
|
||||
setAudioSink(audioSink);
|
||||
try {
|
||||
audioSink.connectIfNeeded();
|
||||
} catch (IOException e) {
|
||||
logger.warn("pulseaudio binding cannot connect to the module-simple-protocol-tcp on {} ({})",
|
||||
getHost(), e.getMessage());
|
||||
} catch (InterruptedException i) {
|
||||
logger.info("Interrupted during sink audio connection: {}", i.getMessage());
|
||||
return;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
|
||||
.registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
|
||||
audioSinkRegistrations.put(thing.getUID().toString(), reg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -89,9 +144,21 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
|
||||
refreshJob = null;
|
||||
}
|
||||
updateStatus(ThingStatus.OFFLINE);
|
||||
bridgeHandler.unregisterDeviceStatusListener(this);
|
||||
bridgeHandler = null;
|
||||
logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
|
||||
super.dispose();
|
||||
|
||||
if (audioSink != null) {
|
||||
audioSink.disconnect();
|
||||
}
|
||||
|
||||
// Unregister the potential pulse audio sink's audio sink
|
||||
ServiceRegistration<AudioSink> reg = audioSinkRegistrations.remove(getThing().getUID().toString());
|
||||
if (reg != null) {
|
||||
logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID());
|
||||
reg.unregister();
|
||||
}
|
||||
}
|
||||
|
||||
private void deviceOnlineWatchdog() {
|
||||
@ -162,15 +229,15 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
|
||||
// refresh to get the current volume level
|
||||
bridge.getClient().update();
|
||||
device = bridge.getDevice(name);
|
||||
int volume = device.getVolume();
|
||||
savedVolume = device.getVolume();
|
||||
if (command.equals(IncreaseDecreaseType.INCREASE)) {
|
||||
volume = Math.min(100, volume + 5);
|
||||
savedVolume = Math.min(100, savedVolume + 5);
|
||||
}
|
||||
if (command.equals(IncreaseDecreaseType.DECREASE)) {
|
||||
volume = Math.max(0, volume - 5);
|
||||
savedVolume = Math.max(0, savedVolume - 5);
|
||||
}
|
||||
bridge.getClient().setVolumePercent(device, volume);
|
||||
updateState = new PercentType(volume);
|
||||
bridge.getClient().setVolumePercent(device, savedVolume);
|
||||
updateState = new PercentType(savedVolume);
|
||||
} else if (command instanceof PercentType) {
|
||||
DecimalType volume = (DecimalType) command;
|
||||
bridge.getClient().setVolumePercent(device, volume.intValue());
|
||||
@ -227,12 +294,37 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use last checked volume for faster access
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public int getLastVolume() {
|
||||
if (savedVolume == null) {
|
||||
PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
|
||||
AbstractAudioDeviceConfig device = bridge.getDevice(name);
|
||||
// refresh to get the current volume level
|
||||
bridge.getClient().update();
|
||||
device = bridge.getDevice(name);
|
||||
savedVolume = device.getVolume();
|
||||
}
|
||||
return savedVolume == null ? 50 : savedVolume;
|
||||
}
|
||||
|
||||
public void setVolume(int volume) {
|
||||
PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
|
||||
AbstractAudioDeviceConfig device = bridge.getDevice(name);
|
||||
bridge.getClient().setVolumePercent(device, volume);
|
||||
updateState(VOLUME_CHANNEL, new PercentType(volume));
|
||||
}
|
||||
|
||||
@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()));
|
||||
savedVolume = device.getVolume();
|
||||
updateState(VOLUME_CHANNEL, new PercentType(savedVolume));
|
||||
updateState(MUTE_CHANNEL, device.isMuted() ? OnOffType.ON : OnOffType.OFF);
|
||||
updateState(STATE_CHANNEL,
|
||||
device.getState() != null ? new StringType(device.getState().toString()) : new StringType("-"));
|
||||
@ -248,11 +340,40 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
|
||||
}
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge != null) {
|
||||
return (String) bridge.getConfiguration().get(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST);
|
||||
} else {
|
||||
logger.error("A bridge must be configured for this pulseaudio thing");
|
||||
return "null";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will scan the pulseaudio server to find the port on which the module/sink is listening
|
||||
* If no module is listening, then it will command the module to load on the pulse audio server,
|
||||
*
|
||||
* @return the port on which the pulseaudio server is listening for this sink
|
||||
* @throws InterruptedException when interrupted during the loading module wait
|
||||
*/
|
||||
public int getSimpleTcpPort() throws InterruptedException {
|
||||
Integer simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration()
|
||||
.get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_PORT)).intValue();
|
||||
|
||||
PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler();
|
||||
AbstractAudioDeviceConfig device = bridgeHandler.getDevice(name);
|
||||
return getPulseaudioBridgeHandler().getClient().loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPortPref)
|
||||
.orElse(simpleTcpPortPref);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) {
|
||||
if (device.getPaName().equals(name)) {
|
||||
bridgeHandler.unregisterDeviceStatusListener(this);
|
||||
bridgeHandler = null;
|
||||
audioSink.disconnect();
|
||||
audioSink = null;
|
||||
updateStatus(ThingStatus.OFFLINE);
|
||||
}
|
||||
}
|
||||
@ -261,4 +382,8 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
|
||||
public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) {
|
||||
logger.trace("new device discovered {} by {}", device, bridge);
|
||||
}
|
||||
|
||||
public void setAudioSink(PulseAudioAudioSink audioSink) {
|
||||
this.audioSink = audioSink;
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
</supported-bridge-type-refs>
|
||||
<label>A Pulseaudio Sink</label>
|
||||
<description>represents a pulseaudio sink</description>
|
||||
<category>Speaker</category>
|
||||
|
||||
<channels>
|
||||
<channel id="volume" typeId="volume"/>
|
||||
@ -21,6 +22,17 @@
|
||||
<label>Name</label>
|
||||
<description>The name of one specific device.</description>
|
||||
</parameter>
|
||||
<parameter name="activateSimpleProtocolSink" type="boolean" required="false">
|
||||
<label>Create an Audio Sink with simple-protocol-tcp</label>
|
||||
<description>Activation of a corresponding sink in OpenHAB (module-simple-protocol-tcp must be available on the
|
||||
pulseaudio server)</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
<parameter name="simpleProtocolSinkPort" type="integer" required="false">
|
||||
<label>Simple Protocol Port</label>
|
||||
<description>Default Port to allocate for use by module-simple-protocol-tcp on the pulseaudio server</description>
|
||||
<default>4711</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user