[pulseaudio] Add pulseaudio sink as openhab audio sink (#1895) (#10423)

* [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:
dalgwen
2021-04-09 22:44:38 +02:00
committed by GitHub
parent 89d735bb0f
commit c3b29e0fe6
12 changed files with 502 additions and 40 deletions

View File

@@ -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>

View File

@@ -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());
}
}

View File

@@ -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<>();

View File

@@ -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());
}
}

View File

@@ -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;

View File

@@ -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"))));

View File

@@ -73,7 +73,6 @@ public class PulseaudioDiscoveryParticipant implements MDNSDiscoveryParticipant
}
return result;
} catch (IOException e) {
result = null;
}
}
return result;

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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>