Skip to content

Commit

Permalink
[pulseaudio] Fix sink-input configuration and other (openhab#11272) (o…
Browse files Browse the repository at this point in the history
…penhab#11276)

* [pulseaudio] Fix sink-input configuration  and other small improvements (openhab#11272)

The binding requires a parameter to activate the parsing of sink-input entries on the pulseaudio server. This patch :
- document this behaviour
- fix the parsing of these parameters if a configuration file is used (the old method of casting launched a class cast exception)

Other small improvements :
- Force a refresh/new parsing when the configuration changes
- Fix scheduled disconnection : if a sound is played during the grace period, the scheduled disconnection is postponed, not added to the last
- add a possibility to never disconnect the audio sink (in order to have a lower latency when playing sound)
Closes openhab#11272

Signed-off-by: Gwendal Roulleau <[email protected]>

* Small fixes after proofreading

Signed-off-by: Gwendal Roulleau <[email protected]>

Co-authored-by: Gwendal Roulleau <[email protected]>
  • Loading branch information
dalgwen and dalgwen authored Oct 9, 2021
1 parent 87c2e0e commit 407834c
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 51 deletions.
23 changes: 22 additions & 1 deletion bundles/org.openhab.binding.pulseaudio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ You need a running pulseaudio server with module **module-cli-protocol-tcp** loa

The Pulseaudio bridge is discovered through mDNS in the local network.

## Binding Configuration (optional)

The Pulseaudio binding can be customized to handle different devices. The Sink support is activated by default and you need no further action to use it. If you want to use another type of device, or disable the Sink type, you have to switch the corresponding binding property.

- **sink:** Allow the binding to parse sink devices from the pulseaudio server
- **source:** Allow the binding to parse source devices from the pulseaudio server
- **sinkInput:** Allow the binding to parse sink-input devices from the pulseaudio server
- **sourceOutput:** Allow the binding to parse source-output devices from the pulseaudio server

You can use the GUI on the bindings page (click on the pulseaudio binding then "Expand for details"), or create a `<openHAB-conf>/services/pulseaudio.cfg` file and use the above options like this:

```
binding.pulseaudio:sink=true
binding.pulseaudio:source=false
binding.pulseaudio:sinkInput=false
binding.pulseaudio:sourceOutput=false
```

## 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.
Expand All @@ -42,17 +60,20 @@ Use the appropriate parameter in the sink thing to activate this possibility (ac
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", activateSimpleProtocolSink="true", simpleProtocolSinkPort="4711"] // the 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"]
Thing combined-sink hdmiAndAnalog "Zone 1+2" @ "Room" [name="combined"]
}
```

<!--
### pulseaudio.items
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
Expand Down Expand Up @@ -66,6 +67,8 @@ public class PulseAudioAudioSink implements AudioSink {

private boolean isIdle = true;

private @Nullable ScheduledFuture<?> scheduledDisconnection;

static {
SUPPORTED_FORMATS.add(AudioFormat.WAV);
SUPPORTED_FORMATS.add(AudioFormat.MP3);
Expand Down Expand Up @@ -254,8 +257,14 @@ public void process(@Nullable AudioStream audioStream)
}

public void scheduleDisconnect() {
logger.debug("Scheduling disconnect");
scheduler.schedule(this::disconnect, pulseaudioHandler.getIdleTimeout(), TimeUnit.MILLISECONDS);
if (scheduledDisconnection != null) {
scheduledDisconnection.cancel(true);
}
int idleTimeout = pulseaudioHandler.getIdleTimeout();
if (idleTimeout > -1) {
logger.debug("Scheduling disconnect");
scheduledDisconnection = scheduler.schedule(this::disconnect, idleTimeout, TimeUnit.MILLISECONDS);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* 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.util.HashSet;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* Contains the binding configuration
*
* @author Gwendal Roulleau - Initial contribution
*
*/
@NonNullByDefault
public class PulseAudioBindingConfiguration {

public boolean sink = true;

public boolean source = false;

public boolean sinkInput = false;

public boolean sourceOutput = false;

private Set<PulseAudioBindingConfigurationListener> listeners = new HashSet<>();

public void addPulseAudioBindingConfigurationListener(PulseAudioBindingConfigurationListener listener) {
listeners.add(listener);
}

public void removePulseAudioBindingConfigurationListener(PulseAudioBindingConfigurationListener listener) {
listeners.remove(listener);
}

public void update(PulseAudioBindingConfiguration newConfiguration) {
sink = newConfiguration.sink;
source = newConfiguration.source;
sinkInput = newConfiguration.sinkInput;
sourceOutput = newConfiguration.sourceOutput;

listeners.forEach(PulseAudioBindingConfigurationListener::bindingConfigurationChanged);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* 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 org.eclipse.jdt.annotation.NonNullByDefault;

/**
* Interface for listening to configuration change
*
* @author Gwendal Roulleau - Initial contribution
*
*/
@NonNullByDefault
public interface PulseAudioBindingConfigurationListener {

public void bindingConfigurationChanged();
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
*/
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;

Expand Down Expand Up @@ -57,13 +54,4 @@ public class PulseaudioBindingConstants {

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

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public class PulseaudioClient {
private List<AbstractAudioDeviceConfig> items;
private List<Module> modules;

/**
* Corresponding to the global binding configuration
*/
private PulseAudioBindingConfiguration configuration;

/**
* corresponding name to execute actions on sink items
*/
Expand Down Expand Up @@ -119,13 +124,10 @@ public class PulseaudioClient {
*/
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 {
public PulseaudioClient(String host, int port, PulseAudioBindingConfiguration configuration) throws IOException {
this.host = host;
this.port = port;
this.configuration = configuration;

items = new ArrayList<>();
modules = new ArrayList<>();
Expand All @@ -147,19 +149,19 @@ public synchronized void update() {

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)) {
if (configuration.sink) {
logger.debug("reading sinks");
newItems.addAll(Parser.parseSinks(listSinks(), this));
}
if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_THING_TYPE.getId())).orElse(false)) {
if (configuration.source) {
logger.debug("reading sources");
newItems.addAll(Parser.parseSources(listSources(), this));
}
if (Optional.ofNullable(TYPE_FILTERS.get(SINK_INPUT_THING_TYPE.getId())).orElse(false)) {
if (configuration.sinkInput) {
logger.debug("reading sink-inputs");
newItems.addAll(Parser.parseSinkInputs(listSinkInputs(), this));
}
if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_OUTPUT_THING_TYPE.getId())).orElse(false)) {
if (configuration.sourceOutput) {
logger.debug("reading source-outputs");
newItems.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
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;
Expand All @@ -36,7 +34,9 @@
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -56,6 +56,8 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {

private final Map<ThingHandler, ServiceRegistration<?>> discoveryServiceReg = new HashMap<>();

private PulseAudioBindingConfiguration configuration = new PulseAudioBindingConfiguration();

@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
Expand Down Expand Up @@ -109,7 +111,7 @@ protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();

if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
PulseaudioBridgeHandler handler = new PulseaudioBridgeHandler((Bridge) thing);
PulseaudioBridgeHandler handler = new PulseaudioBridgeHandler((Bridge) thing, configuration);
registerDeviceDiscoveryService(handler);
return handler;
} else if (PulseaudioHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
Expand All @@ -119,25 +121,16 @@ protected ThingHandler createHandler(Thing thing) {
return null;
}

@Override
protected synchronized void activate(ComponentContext componentContext) {
// The activate component call is used to access the bindings configuration
@Activate
protected synchronized void activate(ComponentContext componentContext, Map<String, Object> config) {
super.activate(componentContext);
modified(componentContext);
modified(config);
}

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));
}
@Modified
protected void modified(Map<String, Object> config) {
configuration.update(new Configuration(config).as(PulseAudioBindingConfiguration.class));
logger.debug("pulseaudio configuration update received ({})", config);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.openhab.binding.pulseaudio.internal.PulseAudioBindingConfiguration;
import org.openhab.binding.pulseaudio.internal.PulseAudioBindingConfigurationListener;
import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
import org.openhab.binding.pulseaudio.internal.PulseaudioClient;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
Expand All @@ -45,7 +47,7 @@
* @author Tobias Bräutigam - Initial contribution
*
*/
public class PulseaudioBridgeHandler extends BaseBridgeHandler {
public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseAudioBindingConfigurationListener {
private final Logger logger = LoggerFactory.getLogger(PulseaudioBridgeHandler.class);

public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
Expand All @@ -58,11 +60,17 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler {

private PulseaudioClient client;

private PulseAudioBindingConfiguration configuration;

private List<DeviceStatusListener> deviceStatusListeners = new CopyOnWriteArrayList<>();
private HashSet<String> lastActiveDevices = new HashSet<>();

private ScheduledFuture<?> pollingJob;
private Runnable pollingRunnable = () -> {
update();
};

private synchronized void update() {
client.update();
for (AbstractAudioDeviceConfig device : client.getItems()) {
if (lastActiveDevices != null && lastActiveDevices.contains(device.getPaName())) {
Expand All @@ -85,10 +93,11 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler {
}
}
}
};
}

public PulseaudioBridgeHandler(Bridge bridge) {
public PulseaudioBridgeHandler(Bridge bridge, PulseAudioBindingConfiguration configuration) {
super(bridge);
this.configuration = configuration;
}

@Override
Expand Down Expand Up @@ -132,7 +141,7 @@ public void initialize() {
if (host != null && !host.isEmpty()) {
Runnable connectRunnable = () -> {
try {
client = new PulseaudioClient(host, port);
client = new PulseaudioClient(host, port, configuration);
if (client.isConnected()) {
updateStatus(ThingStatus.ONLINE);
logger.info("Established connection to Pulseaudio server on Host '{}':'{}'.", host, port);
Expand All @@ -151,10 +160,13 @@ public void initialize() {
host, port);
updateStatus(ThingStatus.OFFLINE);
}

this.configuration.addPulseAudioBindingConfigurationListener(this);
}

@Override
public void dispose() {
this.configuration.removePulseAudioBindingConfigurationListener(this);
if (pollingJob != null) {
pollingJob.cancel(true);
}
Expand All @@ -174,4 +186,9 @@ public boolean registerDeviceStatusListener(DeviceStatusListener deviceStatusLis
public boolean unregisterDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
return deviceStatusListeners.remove(deviceStatusListener);
}

@Override
public void bindingConfigurationChanged() {
update();
}
}
Loading

0 comments on commit 407834c

Please sign in to comment.