Skip to content

Commit

Permalink
[pulseaudio] Allow flexible parameters to find a given pulseaudio device
Browse files Browse the repository at this point in the history
To identify the device on the pulseaudio server, you can now use the description instead of the technical id (a.k.a. "name").
To filter furthermore, you can also use the parameter additionalFilters (optional regular expressions that need to match a property value of a device on the pulseaudio server)

Closes openhab#12555

Signed-off-by: Gwendal Roulleau <[email protected]>
  • Loading branch information
dalgwen committed Apr 11, 2022
1 parent 2be9a65 commit e7bbc55
Show file tree
Hide file tree
Showing 19 changed files with 258 additions and 62 deletions.
10 changes: 7 additions & 3 deletions bundles/org.openhab.binding.pulseaudio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ 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.
You can use `pactl -s <ip-address|hostname> list sinks | grep "name:"` to find the name of a sink.
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.
A Pulseaudio device requires at least an identifier. For sinks and sources, you can use the name or the description. For sink inputs and source outputs, you can use the name or the application name.
To know without hesitation the correct value to use, you should use the command line utility `pactl`. For example, to find the name of a sink:
`pactl -s <ip-address|hostname> list sinks | grep "name:"`
If you need to narrow the identification of a device (in case name or description are not consistent and sufficient), you can use the `additionalFilters` parameter (optional/advanced parameter), in the form of one or several (separator '###') regular expression(s), each one matching a property value of the pulseaudio device. You can use every properties listed with `pactl`.


## Channels

Expand Down Expand Up @@ -74,7 +78,7 @@ This requires the module **module-simple-protocol-tcp** to be present on the tar
```
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, additionalFilters="analog-stereo###internal"]
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"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public class PulseaudioBindingConstants {
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 String DEVICE_PARAMETER_NAME_OR_DESCRIPTION = "name";
public static final String DEVICE_PARAMETER_ADDITIONAL_FILTERS = "additionalFilters";
public static final String DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION = "activateSimpleProtocolSink";
public static final String DEVICE_PARAMETER_AUDIO_SINK_PORT = "simpleProtocolSinkPort";
public static final String DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT = "simpleProtocolSinkIdleTimeout";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.pulseaudio.internal.cli.Parser;
import org.openhab.binding.pulseaudio.internal.handler.DeviceIdentifier;
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;
Expand Down Expand Up @@ -258,15 +261,23 @@ public void sendCommand(String command) {
}

/**
* retrieves a {@link AbstractAudioDeviceConfig} by its name
* retrieves a {@link AbstractAudioDeviceConfig} by its identifier
* If several devices correspond to the deviceIdentifier, returns the first one (aphabetical order)
*
* @param The device identifier to match against
* @return the corresponding {@link AbstractAudioDeviceConfig} to the given <code>name</code>
*/
public @Nullable AbstractAudioDeviceConfig getGenericAudioItem(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getPaName().equalsIgnoreCase(name)) {
return item;
}
public @Nullable AbstractAudioDeviceConfig getGenericAudioItem(DeviceIdentifier deviceIdentifier) {
List<AbstractAudioDeviceConfig> matchingDevices = items.stream()
.filter(device -> device.matches(deviceIdentifier))
.sorted(Comparator.comparing(AbstractAudioDeviceConfig::getPaName)).collect(Collectors.toList());
if (matchingDevices.size() == 1) {
return matchingDevices.get(0);
} else if (matchingDevices.size() > 1) {
logger.debug(
"Cannot select exactly one audio device, so, choosing the first. To choose without ambiguity between the {} devices matching the identifier {}, you can maybe use a more restrictive 'additionalFilter' parameter",
matchingDevices.size(), deviceIdentifier.getNameOrDescription());
return matchingDevices.get(0);
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ private void registerDeviceDiscoveryService(PulseaudioBridgeHandler paBridgeHand
private ThingUID getPulseaudioDeviceUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID,
Configuration configuration, @Nullable ThingUID bridgeUID) {
if (thingUID == null) {
String name = (String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME);
String name = (String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME_OR_DESCRIPTION);
return new ThingUID(thingTypeUID, name, bridgeUID == null ? null : bridgeUID.getId());
}
return thingUID;
Expand All @@ -101,7 +101,9 @@ protected void removeHandler(ThingHandler thingHandler) {
if (serviceRegistration != null) {
PulseaudioDeviceDiscoveryService service = (PulseaudioDeviceDiscoveryService) bundleContext
.getService(serviceRegistration.getReference());
service.deactivate();
if (service != null) {
service.deactivate();
}
serviceRegistration.unregister();
}
discoveryServiceReg.remove(thingHandler);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public static Collection<Sink> parseSinks(String raw, PulseaudioClient client) {
}
}
if (properties.containsKey("name")) {
Sink sink = new Sink(id, properties.get("name"),
Sink sink = new Sink(id, properties.get("name"), properties.get("device.description"), properties,
client.getModule(getNumberValue(properties.get("module"))));
if (properties.containsKey("state")) {
try {
Expand Down Expand Up @@ -198,7 +198,8 @@ public static List<SinkInput> parseSinkInputs(String raw, PulseaudioClient clien
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"))));
SinkInput item = new SinkInput(id, name, properties.get("application.name"), properties,
client.getModule(getNumberValue(properties.get("module"))));
if (properties.containsKey("state")) {
try {
item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
Expand Down Expand Up @@ -256,7 +257,7 @@ public static List<Source> parseSources(String raw, PulseaudioClient client) {
}
}
if (properties.containsKey("name")) {
Source source = new Source(id, properties.get("name"),
Source source = new Source(id, properties.get("name"), properties.get("device.description"), properties,
client.getModule(getNumberValue(properties.get("module"))));
if (properties.containsKey("state")) {
try {
Expand Down Expand Up @@ -316,8 +317,8 @@ public static List<SourceOutput> parseSourceOutputs(String raw, PulseaudioClient
}
}
if (properties.containsKey("source")) {
SourceOutput item = new SourceOutput(id, properties.get("source"),
client.getModule(getNumberValue(properties.get("module"))));
SourceOutput item = new SourceOutput(id, properties.get("source"), properties.get("application.name"),
properties, client.getModule(getNumberValue(properties.get("module"))));
if (properties.containsKey("state")) {
try {
item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
package org.openhab.binding.pulseaudio.internal.discovery;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.regex.PatternSyntaxException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
import org.openhab.binding.pulseaudio.internal.handler.DeviceIdentifier;
import org.openhab.binding.pulseaudio.internal.handler.DeviceStatusListener;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
Expand All @@ -27,6 +29,7 @@
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.core.Configuration;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
Expand Down Expand Up @@ -70,7 +73,7 @@ public Set<ThingTypeUID> getSupportedThingTypes() {

@Override
public void onDeviceAdded(Thing bridge, AbstractAudioDeviceConfig device) {
if (getAlreadyConfiguredThings().contains(device.getPaName())) {
if (getAlreadyConfiguredThings().stream().anyMatch(deviceIdentifier -> device.matches(deviceIdentifier))) {
return;
}

Expand All @@ -79,7 +82,7 @@ public void onDeviceAdded(Thing bridge, AbstractAudioDeviceConfig device) {
ThingTypeUID thingType = null;
Map<String, Object> properties = new HashMap<>();
// All devices need this parameter
properties.put(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME, uidName);
properties.put(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME_OR_DESCRIPTION, uidName);
if (device instanceof Sink) {
if (((Sink) device).isCombinedSink()) {
thingType = PulseaudioBindingConstants.COMBINED_SINK_THING_TYPE;
Expand All @@ -104,10 +107,20 @@ public void onDeviceAdded(Thing bridge, AbstractAudioDeviceConfig device) {
}
}

public Set<String> getAlreadyConfiguredThings() {
return pulseaudioBridgeHandler.getThing().getThings().stream().map(Thing::getConfiguration)
.map(conf -> (String) conf.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME))
.collect(Collectors.toSet());
public Set<DeviceIdentifier> getAlreadyConfiguredThings() {
Set<DeviceIdentifier> alreadyConfiguredThings = new HashSet<>();
for (Thing thing : pulseaudioBridgeHandler.getThing().getThings()) {
Configuration configuration = thing.getConfiguration();
try {
alreadyConfiguredThings.add(new DeviceIdentifier(
(String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME_OR_DESCRIPTION),
(String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_ADDITIONAL_FILTERS)));
} catch (PatternSyntaxException p) {
logger.debug(
"There is an error with an already configured things. Cannot compare with discovery, skipping it");
}
}
return alreadyConfiguredThings;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2022 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 java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;

/**
* All informations needed to precisely identify a device
*
* @author Gwendal Roulleau - Initial contribution
*
*/
@NonNullByDefault
public class DeviceIdentifier {

private String nameOrDescription;
private List<Pattern> additionalFilters = new ArrayList<>();

public DeviceIdentifier(String nameOrDescription, @Nullable String additionalFilters) {
super();
this.nameOrDescription = nameOrDescription;
if (additionalFilters != null && !additionalFilters.isEmpty()) {
Arrays.asList(additionalFilters.split("###")).stream()
.forEach(ad -> this.additionalFilters.add(Pattern.compile(ad)));
}
}

public String getNameOrDescription() {
return nameOrDescription;
}

public List<Pattern> getAdditionalFilters() {
return additionalFilters;
}

@Override
public String toString() {
List<Pattern> additionalFiltersFinal = additionalFilters;
String additionalPatternToString = additionalFiltersFinal.stream().map(Pattern::pattern)
.collect(Collectors.joining("###"));
return "DeviceIdentifier [nameOrDescription=" + nameOrDescription + ", additionalFilter="
+ additionalPatternToString + "]";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public synchronized void update() {
} else {
// browse all child handlers to update status according to the result of the query to the pulse audio server
for (PulseaudioHandler pulseaudioHandler : childHandlersInitialized) {
pulseaudioHandler.deviceUpdate(getDevice(pulseaudioHandler.getName()));
pulseaudioHandler.deviceUpdate(getDevice(pulseaudioHandler.getDeviceIdentifier()));
}
}
// browse query result to notify add event
Expand Down Expand Up @@ -129,8 +129,8 @@ public void handleCommand(ChannelUID channelUID, Command command) {
}
}

public @Nullable AbstractAudioDeviceConfig getDevice(String name) {
return getClient().getGenericAudioItem(name);
public @Nullable AbstractAudioDeviceConfig getDevice(@Nullable DeviceIdentifier deviceIdentifier) {
return deviceIdentifier == null ? null : getClient().getGenericAudioItem(deviceIdentifier);
}

public PulseaudioClient getClient() {
Expand Down
Loading

0 comments on commit e7bbc55

Please sign in to comment.