Skip to content

Commit ae20f93

Browse files
authored
[pulseaudio] Allow flexible parameters to find a given pulseaudio device (openhab#12598)
* [pulseaudio] Allow flexible parameters to find a given pulseaudio device 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]>
1 parent 1408899 commit ae20f93

19 files changed

+260
-62
lines changed

bundles/org.openhab.binding.pulseaudio/README.md

+7-3
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,12 @@ binding.pulseaudio:sourceOutput=false
3838

3939
## Thing Configuration
4040

41-
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.
42-
You can use `pactl -s <ip-address|hostname> list sinks | grep "name:"` to find the name of a sink.
41+
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.
42+
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.
43+
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:
44+
`pactl -s <ip-address|hostname> list sinks | grep "name:"`
45+
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`.
46+
4347

4448
## Channels
4549

@@ -74,7 +78,7 @@ This requires the module **module-simple-protocol-tcp** to be present on the tar
7478
```
7579
Bridge pulseaudio:bridge:<bridgname> "<Bridge Label>" @ "<Room>" [ host="<ipAddress>", port=4712 ] {
7680
Things:
77-
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
81+
Thing sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3", activateSimpleProtocolSink=true, simpleProtocolSinkPort=4711, additionalFilters="analog-stereo###internal"]
7882
Thing source microphone "microphone" @ "Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"]
7983
Thing sink-input openhabTTS "OH-Voice" @ "Room" [name="alsa_output.pci-0000_00_1f.3.hdmi-stereo-extra1"]
8084
Thing source-output remotePulseSink "Other Room Speaker" @ "Other Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"]

bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ public class PulseaudioBindingConstants {
4848
public static final String BRIDGE_PARAMETER_PORT = "port";
4949
public static final String BRIDGE_PARAMETER_REFRESH_INTERVAL = "refresh";
5050

51-
public static final String DEVICE_PARAMETER_NAME = "name";
51+
public static final String DEVICE_PARAMETER_NAME_OR_DESCRIPTION = "name";
52+
public static final String DEVICE_PARAMETER_ADDITIONAL_FILTERS = "additionalFilters";
5253
public static final String DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION = "activateSimpleProtocolSink";
5354
public static final String DEVICE_PARAMETER_AUDIO_SINK_PORT = "simpleProtocolSinkPort";
5455
public static final String DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT = "simpleProtocolSinkIdleTimeout";

bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java

+17-6
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@
2323
import java.net.SocketTimeoutException;
2424
import java.net.UnknownHostException;
2525
import java.util.ArrayList;
26+
import java.util.Comparator;
2627
import java.util.List;
2728
import java.util.Optional;
2829
import java.util.Random;
30+
import java.util.stream.Collectors;
2931

3032
import org.eclipse.jdt.annotation.NonNullByDefault;
3133
import org.eclipse.jdt.annotation.Nullable;
3234
import org.openhab.binding.pulseaudio.internal.cli.Parser;
35+
import org.openhab.binding.pulseaudio.internal.handler.DeviceIdentifier;
3336
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
3437
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig.State;
3538
import org.openhab.binding.pulseaudio.internal.items.Module;
@@ -258,15 +261,23 @@ public void sendCommand(String command) {
258261
}
259262

260263
/**
261-
* retrieves a {@link AbstractAudioDeviceConfig} by its name
264+
* retrieves a {@link AbstractAudioDeviceConfig} by its identifier
265+
* If several devices correspond to the deviceIdentifier, returns the first one (aphabetical order)
262266
*
267+
* @param The device identifier to match against
263268
* @return the corresponding {@link AbstractAudioDeviceConfig} to the given <code>name</code>
264269
*/
265-
public @Nullable AbstractAudioDeviceConfig getGenericAudioItem(String name) {
266-
for (AbstractAudioDeviceConfig item : items) {
267-
if (item.getPaName().equalsIgnoreCase(name)) {
268-
return item;
269-
}
270+
public @Nullable AbstractAudioDeviceConfig getGenericAudioItem(DeviceIdentifier deviceIdentifier) {
271+
List<AbstractAudioDeviceConfig> matchingDevices = items.stream()
272+
.filter(device -> device.matches(deviceIdentifier))
273+
.sorted(Comparator.comparing(AbstractAudioDeviceConfig::getPaName)).collect(Collectors.toList());
274+
if (matchingDevices.size() == 1) {
275+
return matchingDevices.get(0);
276+
} else if (matchingDevices.size() > 1) {
277+
logger.debug(
278+
"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",
279+
matchingDevices.size(), deviceIdentifier.getNameOrDescription());
280+
return matchingDevices.get(0);
270281
}
271282
return null;
272283
}

bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ private void registerDeviceDiscoveryService(PulseaudioBridgeHandler paBridgeHand
8989
private ThingUID getPulseaudioDeviceUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID,
9090
Configuration configuration, @Nullable ThingUID bridgeUID) {
9191
if (thingUID == null) {
92-
String name = (String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME);
92+
String name = (String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME_OR_DESCRIPTION);
9393
return new ThingUID(thingTypeUID, name, bridgeUID == null ? null : bridgeUID.getId());
9494
}
9595
return thingUID;
@@ -101,7 +101,9 @@ protected void removeHandler(ThingHandler thingHandler) {
101101
if (serviceRegistration != null) {
102102
PulseaudioDeviceDiscoveryService service = (PulseaudioDeviceDiscoveryService) bundleContext
103103
.getService(serviceRegistration.getReference());
104-
service.deactivate();
104+
if (service != null) {
105+
service.deactivate();
106+
}
105107
serviceRegistration.unregister();
106108
}
107109
discoveryServiceReg.remove(thingHandler);

bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public static Collection<Sink> parseSinks(String raw, PulseaudioClient client) {
128128
}
129129
}
130130
if (properties.containsKey("name")) {
131-
Sink sink = new Sink(id, properties.get("name"),
131+
Sink sink = new Sink(id, properties.get("name"), properties.get("device.description"), properties,
132132
client.getModule(getNumberValue(properties.get("module"))));
133133
if (properties.containsKey("state")) {
134134
try {
@@ -198,7 +198,8 @@ public static List<SinkInput> parseSinkInputs(String raw, PulseaudioClient clien
198198
if (properties.containsKey("sink")) {
199199
String name = properties.containsKey("media.name") ? properties.get("media.name")
200200
: properties.get("sink");
201-
SinkInput item = new SinkInput(id, name, client.getModule(getNumberValue(properties.get("module"))));
201+
SinkInput item = new SinkInput(id, name, properties.get("application.name"), properties,
202+
client.getModule(getNumberValue(properties.get("module"))));
202203
if (properties.containsKey("state")) {
203204
try {
204205
item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));
@@ -256,7 +257,7 @@ public static List<Source> parseSources(String raw, PulseaudioClient client) {
256257
}
257258
}
258259
if (properties.containsKey("name")) {
259-
Source source = new Source(id, properties.get("name"),
260+
Source source = new Source(id, properties.get("name"), properties.get("device.description"), properties,
260261
client.getModule(getNumberValue(properties.get("module"))));
261262
if (properties.containsKey("state")) {
262263
try {
@@ -316,8 +317,8 @@ public static List<SourceOutput> parseSourceOutputs(String raw, PulseaudioClient
316317
}
317318
}
318319
if (properties.containsKey("source")) {
319-
SourceOutput item = new SourceOutput(id, properties.get("source"),
320-
client.getModule(getNumberValue(properties.get("module"))));
320+
SourceOutput item = new SourceOutput(id, properties.get("source"), properties.get("application.name"),
321+
properties, client.getModule(getNumberValue(properties.get("module"))));
321322
if (properties.containsKey("state")) {
322323
try {
323324
item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state")));

bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDeviceDiscoveryService.java

+20-7
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
package org.openhab.binding.pulseaudio.internal.discovery;
1414

1515
import java.util.HashMap;
16+
import java.util.HashSet;
1617
import java.util.Map;
1718
import java.util.Set;
18-
import java.util.stream.Collectors;
19+
import java.util.regex.PatternSyntaxException;
1920

2021
import org.eclipse.jdt.annotation.NonNullByDefault;
2122
import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
23+
import org.openhab.binding.pulseaudio.internal.handler.DeviceIdentifier;
2224
import org.openhab.binding.pulseaudio.internal.handler.DeviceStatusListener;
2325
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler;
2426
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
@@ -27,6 +29,7 @@
2729
import org.openhab.binding.pulseaudio.internal.items.SinkInput;
2830
import org.openhab.binding.pulseaudio.internal.items.Source;
2931
import org.openhab.binding.pulseaudio.internal.items.SourceOutput;
32+
import org.openhab.core.config.core.Configuration;
3033
import org.openhab.core.config.discovery.AbstractDiscoveryService;
3134
import org.openhab.core.config.discovery.DiscoveryResult;
3235
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
@@ -70,7 +73,7 @@ public Set<ThingTypeUID> getSupportedThingTypes() {
7073

7174
@Override
7275
public void onDeviceAdded(Thing bridge, AbstractAudioDeviceConfig device) {
73-
if (getAlreadyConfiguredThings().contains(device.getPaName())) {
76+
if (getAlreadyConfiguredThings().stream().anyMatch(deviceIdentifier -> device.matches(deviceIdentifier))) {
7477
return;
7578
}
7679

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

107-
public Set<String> getAlreadyConfiguredThings() {
108-
return pulseaudioBridgeHandler.getThing().getThings().stream().map(Thing::getConfiguration)
109-
.map(conf -> (String) conf.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME))
110-
.collect(Collectors.toSet());
110+
public Set<DeviceIdentifier> getAlreadyConfiguredThings() {
111+
Set<DeviceIdentifier> alreadyConfiguredThings = new HashSet<>();
112+
for (Thing thing : pulseaudioBridgeHandler.getThing().getThings()) {
113+
Configuration configuration = thing.getConfiguration();
114+
try {
115+
alreadyConfiguredThings.add(new DeviceIdentifier(
116+
(String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME_OR_DESCRIPTION),
117+
(String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_ADDITIONAL_FILTERS)));
118+
} catch (PatternSyntaxException p) {
119+
logger.debug(
120+
"There is an error with an already configured things. Cannot compare with discovery, skipping it");
121+
}
122+
}
123+
return alreadyConfiguredThings;
111124
}
112125

113126
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Copyright (c) 2010-2022 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.pulseaudio.internal.handler;
14+
15+
import java.util.ArrayList;
16+
import java.util.Arrays;
17+
import java.util.List;
18+
import java.util.regex.Pattern;
19+
import java.util.regex.PatternSyntaxException;
20+
import java.util.stream.Collectors;
21+
22+
import org.eclipse.jdt.annotation.NonNullByDefault;
23+
import org.eclipse.jdt.annotation.Nullable;
24+
25+
/**
26+
* All informations needed to precisely identify a device
27+
*
28+
* @author Gwendal Roulleau - Initial contribution
29+
*
30+
*/
31+
@NonNullByDefault
32+
public class DeviceIdentifier {
33+
34+
private String nameOrDescription;
35+
private List<Pattern> additionalFilters = new ArrayList<>();
36+
37+
public DeviceIdentifier(String nameOrDescription, @Nullable String additionalFilters)
38+
throws PatternSyntaxException {
39+
super();
40+
this.nameOrDescription = nameOrDescription;
41+
if (additionalFilters != null && !additionalFilters.isEmpty()) {
42+
Arrays.asList(additionalFilters.split("###")).stream()
43+
.forEach(ad -> this.additionalFilters.add(Pattern.compile(ad)));
44+
}
45+
}
46+
47+
public String getNameOrDescription() {
48+
return nameOrDescription;
49+
}
50+
51+
public List<Pattern> getAdditionalFilters() {
52+
return additionalFilters;
53+
}
54+
55+
@Override
56+
public String toString() {
57+
List<Pattern> additionalFiltersFinal = additionalFilters;
58+
String additionalPatternToString = additionalFiltersFinal.stream().map(Pattern::pattern)
59+
.collect(Collectors.joining("###"));
60+
return "DeviceIdentifier [nameOrDescription=" + nameOrDescription + ", additionalFilter="
61+
+ additionalPatternToString + "]";
62+
}
63+
}

bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public synchronized void update() {
9797
} else {
9898
// browse all child handlers to update status according to the result of the query to the pulse audio server
9999
for (PulseaudioHandler pulseaudioHandler : childHandlersInitialized) {
100-
pulseaudioHandler.deviceUpdate(getDevice(pulseaudioHandler.getName()));
100+
pulseaudioHandler.deviceUpdate(getDevice(pulseaudioHandler.getDeviceIdentifier()));
101101
}
102102
}
103103
// browse query result to notify add event
@@ -129,8 +129,8 @@ public void handleCommand(ChannelUID channelUID, Command command) {
129129
}
130130
}
131131

132-
public @Nullable AbstractAudioDeviceConfig getDevice(String name) {
133-
return getClient().getGenericAudioItem(name);
132+
public @Nullable AbstractAudioDeviceConfig getDevice(@Nullable DeviceIdentifier deviceIdentifier) {
133+
return deviceIdentifier == null ? null : getClient().getGenericAudioItem(deviceIdentifier);
134134
}
135135

136136
public PulseaudioClient getClient() {

0 commit comments

Comments
 (0)