]> git.basschouten.com Git - openhab-addons.git/commitdiff
[pulseaudio] Allow flexible parameters to find a given pulseaudio device (#12598)
authorGwendal Roulleau <dalgwen@users.noreply.github.com>
Thu, 28 Apr 2022 21:25:23 +0000 (23:25 +0200)
committerGitHub <noreply@github.com>
Thu, 28 Apr 2022 21:25:23 +0000 (23:25 +0200)
* [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 #12555

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
19 files changed:
bundles/org.openhab.binding.pulseaudio/README.md
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDeviceDiscoveryService.java
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceIdentifier.java [new file with mode: 0644]
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/AbstractAudioDeviceConfig.java
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Sink.java
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SinkInput.java
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Source.java
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SourceOutput.java
bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/i18n/pulseaudio.properties
bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink-input.xml
bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml
bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source-output.xml
bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml

index 8f4dac6347399cfd102eb836b8337b451c5f460a..8536a1c4a36b1dc02c41ee33613332ac955ba021 100644 (file)
@@ -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
 
@@ -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"]
index a8277e88b3ee4eaae44758bd99e8210c6bdbc0da..94c9ce0ac85aba61c2a0cab96dd36215e70a5025 100644 (file)
@@ -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";
index 4cf99f0d4f17454a6e03a67c855d27c8ec3d5d91..0f69128e3d15343051f7e9a47236dd5fae528854 100644 (file)
@@ -23,13 +23,16 @@ import java.net.SocketException;
 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;
@@ -258,15 +261,23 @@ public class PulseaudioClient {
     }
 
     /**
-     * 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;
     }
index eb91a1dd4275d3c90409f1db9e68770e18b75c52..11d242fdb91dfdb0bfb26bb4e64a3336d4acbc16 100644 (file)
@@ -89,7 +89,7 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
     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;
@@ -101,7 +101,9 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
         if (serviceRegistration != null) {
             PulseaudioDeviceDiscoveryService service = (PulseaudioDeviceDiscoveryService) bundleContext
                     .getService(serviceRegistration.getReference());
-            service.deactivate();
+            if (service != null) {
+                service.deactivate();
+            }
             serviceRegistration.unregister();
         }
         discoveryServiceReg.remove(thingHandler);
index 47742c1796eed1c577003785133527e877fff9c7..b9bcd09074e3e311cbacc54110c944032a52d812 100644 (file)
@@ -128,7 +128,7 @@ public class Parser {
                 }
             }
             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 {
@@ -198,7 +198,8 @@ public class Parser {
             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")));
@@ -256,7 +257,7 @@ public class Parser {
                 }
             }
             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 {
@@ -316,8 +317,8 @@ public class Parser {
                 }
             }
             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")));
index b36e53472461d4d9a01fc86f0be6e5d421da4234..1ab1f22cc9f4f25db3d1e5cad4a8ec518263bfe8 100644 (file)
 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;
@@ -27,6 +29,7 @@ import org.openhab.binding.pulseaudio.internal.items.Sink;
 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;
@@ -70,7 +73,7 @@ public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService i
 
     @Override
     public void onDeviceAdded(Thing bridge, AbstractAudioDeviceConfig device) {
-        if (getAlreadyConfiguredThings().contains(device.getPaName())) {
+        if (getAlreadyConfiguredThings().stream().anyMatch(deviceIdentifier -> device.matches(deviceIdentifier))) {
             return;
         }
 
@@ -79,7 +82,7 @@ public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService i
         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;
@@ -104,10 +107,20 @@ public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService i
         }
     }
 
-    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
diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceIdentifier.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceIdentifier.java
new file mode 100644 (file)
index 0000000..5849264
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * 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.regex.PatternSyntaxException;
+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)
+            throws PatternSyntaxException {
+        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 + "]";
+    }
+}
index d6e0bacafa2e97c72b617193ed4984ebee400480..d4e5583dfcb8ab88cea05a2f5d80cf0a6343f66d 100644 (file)
@@ -97,7 +97,7 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
         } 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
@@ -129,8 +129,8 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
         }
     }
 
-    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() {
index b6821bd5ffa394a94f749fc59c7317f53a327544..676cbc1c8f938a185eb6554c563d3fea34dbe432 100644 (file)
@@ -24,6 +24,7 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.PatternSyntaxException;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -78,7 +79,7 @@ public class PulseaudioHandler extends BaseThingHandler {
                     SOURCE_THING_TYPE, SOURCE_OUTPUT_THING_TYPE).collect(Collectors.toSet()));
     private final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class);
 
-    private String name = "";
+    private @Nullable DeviceIdentifier deviceIdentifier;
     private @Nullable PulseAudioAudioSink audioSink;
     private @Nullable PulseAudioAudioSource audioSource;
     private @Nullable Integer savedVolume;
@@ -96,12 +97,20 @@ public class PulseaudioHandler extends BaseThingHandler {
     @Override
     public void initialize() {
         Configuration config = getThing().getConfiguration();
-        name = (String) config.get(DEVICE_PARAMETER_NAME);
+        try {
+            deviceIdentifier = new DeviceIdentifier((String) config.get(DEVICE_PARAMETER_NAME_OR_DESCRIPTION),
+                    (String) config.get(DEVICE_PARAMETER_ADDITIONAL_FILTERS));
+        } catch (PatternSyntaxException p) {
+            deviceIdentifier = null;
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "Incorrect regular expression: " + (String) config.get(DEVICE_PARAMETER_ADDITIONAL_FILTERS));
+            return;
+        }
         initializeWithTheBridge();
     }
 
-    public String getName() {
-        return name;
+    public @Nullable DeviceIdentifier getDeviceIdentifier() {
+        return deviceIdentifier;
     }
 
     private void audioSinkSetup() {
@@ -214,7 +223,7 @@ public class PulseaudioHandler extends BaseThingHandler {
 
     @Override
     public void dispose() {
-        logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
+        logger.trace("Thing {} {} disposed.", getThing().getUID(), safeGetDeviceNameOrDescription());
         super.dispose();
         audioSinkUnsetup();
         audioSourceUnsetup();
@@ -232,21 +241,22 @@ public class PulseaudioHandler extends BaseThingHandler {
         } else if (pulseaudioBridgeHandler.getThing().getStatus() != ThingStatus.ONLINE) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
         } else {
-            deviceUpdate(pulseaudioBridgeHandler.getDevice(name));
+            deviceUpdate(pulseaudioBridgeHandler.getDevice(deviceIdentifier));
         }
     }
 
     private synchronized @Nullable PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
         Bridge bridge = getBridge();
         if (bridge == null) {
-            logger.debug("Required bridge not defined for device {}.", name);
+            logger.debug("Required bridge not defined for device {}.", safeGetDeviceNameOrDescription());
             return null;
         }
         ThingHandler handler = bridge.getHandler();
         if (handler instanceof PulseaudioBridgeHandler) {
             return (PulseaudioBridgeHandler) handler;
         } else {
-            logger.debug("No available bridge handler found for device {} bridge {} .", name, bridge.getUID());
+            logger.debug("No available bridge handler found for device {} bridge {} .",
+                    safeGetDeviceNameOrDescription(), bridge.getUID());
             return null;
         }
     }
@@ -263,9 +273,9 @@ public class PulseaudioHandler extends BaseThingHandler {
             return;
         }
 
-        AbstractAudioDeviceConfig device = briHandler.getDevice(name);
+        AbstractAudioDeviceConfig device = briHandler.getDevice(deviceIdentifier);
         if (device == null) {
-            logger.warn("device {} not found", name);
+            logger.warn("device {} not found", safeGetDeviceNameOrDescription());
             deviceUpdate(null);
             return;
         } else {
@@ -274,7 +284,7 @@ public class PulseaudioHandler extends BaseThingHandler {
                 if (command instanceof IncreaseDecreaseType) {
                     // refresh to get the current volume level
                     briHandler.getClient().update();
-                    device = briHandler.getDevice(name);
+                    device = briHandler.getDevice(deviceIdentifier);
                     if (device == null) {
                         logger.warn("missing device info, aborting");
                         return;
@@ -360,7 +370,7 @@ public class PulseaudioHandler extends BaseThingHandler {
             if (briHandler != null) {
                 // refresh to get the current volume level
                 briHandler.getClient().update();
-                AbstractAudioDeviceConfig device = briHandler.getDevice(name);
+                AbstractAudioDeviceConfig device = briHandler.getDevice(deviceIdentifier);
                 if (device != null) {
                     savedVolume = savedVolumeFinal = device.getVolume();
                 }
@@ -375,7 +385,7 @@ public class PulseaudioHandler extends BaseThingHandler {
             logger.warn("bridge is not ready");
             return;
         }
-        AbstractAudioDeviceConfig device = briHandler.getDevice(name);
+        AbstractAudioDeviceConfig device = briHandler.getDevice(deviceIdentifier);
         if (device == null) {
             logger.warn("missing device info, aborting");
             return;
@@ -386,7 +396,7 @@ public class PulseaudioHandler extends BaseThingHandler {
     }
 
     public void deviceUpdate(@Nullable AbstractAudioDeviceConfig device) {
-        if (device != null && device.getPaName().equals(name)) {
+        if (device != null) {
             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
             logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL);
             int actualVolume = device.getVolume();
@@ -404,7 +414,7 @@ public class PulseaudioHandler extends BaseThingHandler {
             }
             audioSinkSetup();
             audioSourceSetup();
-        } else if (device == null) {
+        } else {
             updateState(VOLUME_CHANNEL, UnDefType.UNDEF);
             updateState(MUTE_CHANNEL, UnDefType.UNDEF);
             updateState(STATE_CHANNEL, UnDefType.UNDEF);
@@ -443,9 +453,10 @@ public class PulseaudioHandler extends BaseThingHandler {
         if (briHandler == null) {
             throw new IOException("bridge is not ready");
         }
-        AbstractAudioDeviceConfig device = briHandler.getDevice(name);
+        AbstractAudioDeviceConfig device = briHandler.getDevice(deviceIdentifier);
         if (device == null) {
-            throw new IOException("missing device info, device appears to be offline");
+            throw new IOException(
+                    "missing device info, device " + safeGetDeviceNameOrDescription() + " appears to be offline");
         }
         String simpleTcpPortPrefName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_PORT
                 : DEVICE_PARAMETER_AUDIO_SINK_PORT;
@@ -501,7 +512,7 @@ public class PulseaudioHandler extends BaseThingHandler {
         var idleTimeout = 3000;
         var handler = getPulseaudioBridgeHandler();
         if (handler != null) {
-            AbstractAudioDeviceConfig device = handler.getDevice(name);
+            AbstractAudioDeviceConfig device = handler.getDevice(deviceIdentifier);
             String idleTimeoutPropName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT
                     : DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT;
             var idleTimeoutB = (BigDecimal) getThing().getConfiguration().get(idleTimeoutPropName);
@@ -512,6 +523,11 @@ public class PulseaudioHandler extends BaseThingHandler {
         return idleTimeout;
     }
 
+    private String safeGetDeviceNameOrDescription() {
+        DeviceIdentifier deviceIdentifierFinal = deviceIdentifier;
+        return deviceIdentifierFinal == null ? "UNKNOWN" : deviceIdentifierFinal.getNameOrDescription();
+    }
+
     public int getBasicProtocolSOTimeout() {
         var soTimeout = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOCKET_SO_TIMEOUT);
         return soTimeout != null ? soTimeout.intValue() : 500;
index 68a842f2fe1367efc519c5296c2a136b6f9d1268..3c100595b4c475fd27971da1179b6521c0f16957 100644 (file)
  */
 package org.openhab.binding.pulseaudio.internal.items;
 
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pulseaudio.internal.handler.DeviceIdentifier;
 
 /**
  * GenericAudioItems are any kind of items that deal with audio data and can be
@@ -36,10 +41,40 @@ public abstract class AbstractAudioDeviceConfig extends AbstractDeviceConfig {
     protected boolean muted;
     protected int volume;
     protected @Nullable Module module;
+    protected String secondaryIdentifier;
+    protected Map<String, String> properties;
 
-    public AbstractAudioDeviceConfig(int id, String name, @Nullable Module module) {
+    public AbstractAudioDeviceConfig(int id, String name, @Nullable String secondaryIdentifier,
+            Map<String, String> properties, @Nullable Module module) {
         super(id, name);
         this.module = module;
+        this.secondaryIdentifier = secondaryIdentifier == null ? "" : secondaryIdentifier;
+        this.properties = properties;
+    }
+
+    /**
+     *
+     * @param deviceIdentifier The device identifier to check against
+     * @return true if this device match the requested identifier, false otherwise
+     */
+    public boolean matches(DeviceIdentifier deviceIdentifier) {
+        boolean matches = getPaName().equalsIgnoreCase(deviceIdentifier.getNameOrDescription())
+                || secondaryIdentifier.equalsIgnoreCase(deviceIdentifier.getNameOrDescription());
+        if (!matches) {
+            return false; // stop analysis right here, no need to parse properties
+        } else {
+            List<Pattern> additionalFilters = deviceIdentifier.getAdditionalFilters();
+            if (additionalFilters.isEmpty()) { // the additionalFilter property is not defined, don't check against
+                return true;
+            } else {
+                for (Pattern patternToMatch : additionalFilters) {
+                    if (!properties.values().stream().anyMatch(value -> patternToMatch.matcher(value).find())) {
+                        return false;
+                    }
+                }
+                return true;
+            }
+        }
     }
 
     public @Nullable Module getModule() {
index 0f31498277125282cf22818a3bb540663abafd28..ccea6228ce393d3835bc4610f8314e5fdeaf10cc 100644 (file)
@@ -14,6 +14,7 @@ package org.openhab.binding.pulseaudio.internal.items;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -31,8 +32,8 @@ public class Sink extends AbstractAudioDeviceConfig {
     protected List<String> combinedSinkNames;
     protected List<Sink> combinedSinks;
 
-    public Sink(int id, String name, @Nullable Module module) {
-        super(id, name, module);
+    public Sink(int id, String name, String description, Map<String, String> properties, @Nullable Module module) {
+        super(id, name, description, properties, module);
         combinedSinkNames = new ArrayList<>();
         combinedSinks = new ArrayList<>();
     }
index fa9cb5046a98c6ec3f649c2e6f67a8a7558eb790..a85f6c8d88d6710dc256fadca782c1d35598641d 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.pulseaudio.internal.items;
 
+import java.util.Map;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 
@@ -26,8 +28,8 @@ public class SinkInput extends AbstractAudioDeviceConfig {
     @Nullable
     private Sink sink;
 
-    public SinkInput(int id, String name, @Nullable Module module) {
-        super(id, name, module);
+    public SinkInput(int id, String name, String description, Map<String, String> properties, @Nullable Module module) {
+        super(id, name, description, properties, module);
     }
 
     public @Nullable Sink getSink() {
index c5162b3a51dc69e74b24eb132b8eec351e65d753..299531dd49c67ea02c0ba7ad529b32add0b7493e 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.pulseaudio.internal.items;
 
+import java.util.Map;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 
@@ -27,8 +29,8 @@ public class Source extends AbstractAudioDeviceConfig {
     @Nullable
     protected Sink monitorOf;
 
-    public Source(int id, String name, @Nullable Module module) {
-        super(id, name, module);
+    public Source(int id, String name, String description, Map<String, String> properties, @Nullable Module module) {
+        super(id, name, description, properties, module);
     }
 
     public @Nullable Sink getMonitorOf() {
index 0bda3854553c5edcb6953a7c3a05f4e5b620bddc..e82d637629c4a406fd5f70f6cba17e61998954d5 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.pulseaudio.internal.items;
 
+import java.util.Map;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 
@@ -26,8 +28,9 @@ public class SourceOutput extends AbstractAudioDeviceConfig {
     @Nullable
     private Source source;
 
-    public SourceOutput(int id, String name, @Nullable Module module) {
-        super(id, name, module);
+    public SourceOutput(int id, String name, String description, Map<String, String> properties,
+            @Nullable Module module) {
+        super(id, name, description, properties, module);
     }
 
     public @Nullable Source getSource() {
index 2a56feb6c7ddbbaf62137635ffb04eaa3796cbb8..a961124e0c739e74df1f12d6435ff9b7d26484fb 100644 (file)
@@ -42,7 +42,9 @@ thing-type.config.pulseaudio.combinedSink.name.description = The name of the com
 thing-type.config.pulseaudio.sink.activateSimpleProtocolSink.label = Create an Audio Sink with simple-protocol-tcp
 thing-type.config.pulseaudio.sink.activateSimpleProtocolSink.description = Activation of a corresponding sink in OpenHAB (module-simple-protocol-tcp must be available on the pulseaudio server)
 thing-type.config.pulseaudio.sink.name.label = Name
-thing-type.config.pulseaudio.sink.name.description = The name of one specific device.
+thing-type.config.pulseaudio.sink.name.description = The name of one specific device. You can also use the description.
+thing-type.config.pulseaudio.sink.additionalFilters.label = Additional Filters
+thing-type.config.pulseaudio.sink.additionalFilters.description = Additional filters to select the proper device on the pulseaudio server, in case of ambiguity. To be selected, the device should have at least a property value matching this regular expression. You can use multiple regular expressions (separator is ###).
 thing-type.config.pulseaudio.sink.simpleProtocolSOTimeout.label = Simple Protocol SO Timeout
 thing-type.config.pulseaudio.sink.simpleProtocolSOTimeout.description = Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp. You can tune this option if the socket disconnect frequently.
 thing-type.config.pulseaudio.sink.simpleProtocolSinkIdleTimeout.label = Idle Timeout
@@ -50,11 +52,15 @@ thing-type.config.pulseaudio.sink.simpleProtocolSinkIdleTimeout.description = Ti
 thing-type.config.pulseaudio.sink.simpleProtocolSinkPort.label = Simple Protocol Port
 thing-type.config.pulseaudio.sink.simpleProtocolSinkPort.description = Default Port to allocate for use by module-simple-protocol-tcp on the pulseaudio server
 thing-type.config.pulseaudio.sinkInput.name.label = Name
-thing-type.config.pulseaudio.sinkInput.name.description = The name of one specific device.
+thing-type.config.pulseaudio.sinkInput.name.description = The name of one specific device. You can also use the application name.
+thing-type.config.pulseaudio.sinkInput.additionalFilters.label = Additional Filters
+thing-type.config.pulseaudio.sinkInput.additionalFilters.description = Additional filters to select the proper device on the pulseaudio server, in case of ambiguity. To be selected, the device should have at least a property value matching this regular expression. You can use multiple regular expressions (separator is ###).
 thing-type.config.pulseaudio.source.activateSimpleProtocolSource.label = Create an Audio Source with simple-protocol-tcp
 thing-type.config.pulseaudio.source.activateSimpleProtocolSource.description = Activation of a corresponding source in OpenHAB (module-simple-protocol-tcp must be available on the pulseaudio server)
 thing-type.config.pulseaudio.source.name.label = Name
-thing-type.config.pulseaudio.source.name.description = The name of one specific device.
+thing-type.config.pulseaudio.source.name.description = The name of one specific device. You can also use the description.
+thing-type.config.pulseaudio.source.additionalFilters.label = Additional Filters
+thing-type.config.pulseaudio.source.additionalFilters.description = Additional filters to select the proper device on the pulseaudio server, in case of ambiguity. To be selected, the device should have at least a property value matching this regular expression. You can use multiple regular expressions (separator is ###).
 thing-type.config.pulseaudio.source.simpleProtocolSOTimeout.label = Simple Protocol SO Timeout
 thing-type.config.pulseaudio.source.simpleProtocolSOTimeout.description = Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp. You can tune this option if the socket disconnect frequently.
 thing-type.config.pulseaudio.source.simpleProtocolSourceChannels.label = Simple Protocol Channels
@@ -75,7 +81,9 @@ thing-type.config.pulseaudio.source.simpleProtocolSourcePort.description = Defau
 thing-type.config.pulseaudio.source.simpleProtocolSourceRate.label = Simple Protocol Rate
 thing-type.config.pulseaudio.source.simpleProtocolSourceRate.description = The audio sample rate to be used by module-simple-protocol-tcp on the pulseaudio server
 thing-type.config.pulseaudio.sourceOutput.name.label = Name
-thing-type.config.pulseaudio.sourceOutput.name.description = The name of one specific device.
+thing-type.config.pulseaudio.sourceOutput.name.description = The name of one specific device. You can also use the application name.
+thing-type.config.pulseaudio.sourceOutput.additionalFilters.label = Additional Filters
+thing-type.config.pulseaudio.sourceOutput.additionalFilters.description = Additional filters to select the proper device on the pulseaudio server, in case of ambiguity. To be selected, the device should have at least a property value matching this regular expression. You can use multiple regular expressions (separator is ###).
 
 # channel types
 
index 76ee975019e9d80e7123e1dd2cb2cef58d15fcd8..854b9513a703162218e2487b1257694f57b8832f 100644 (file)
                <config-description>
                        <parameter name="name" type="text" required="true">
                                <label>Name</label>
-                               <description>The name of one specific device.</description>
+                               <description>The name of one specific device. You can also use the application name.</description>
+                       </parameter>
+                       <parameter name="additionalFilters" type="text" required="false">
+                               <label>Additional Filters</label>
+                               <advanced>true</advanced>
+                               <description>Additional filters to select the proper device on the pulseaudio server, in case of ambiguity.
+                                       To be
+                                       selected, the device should have at least a property value matching this regular expression. You can use
+                                       multiple
+                                       regular expressions (separator ###).</description>
                        </parameter>
                </config-description>
        </thing-type>
index 6e4a44fe963992a431ba790c0ca27fde295cad38..dd5fb8910df0c384f23d7534c31dcb0f16218311 100644 (file)
@@ -20,7 +20,7 @@
                <config-description>
                        <parameter name="name" type="text" required="true">
                                <label>Name</label>
-                               <description>The name of one specific device.</description>
+                               <description>The name of one specific device. You can also use the description.</description>
                        </parameter>
                        <parameter name="activateSimpleProtocolSink" type="boolean" required="false">
                                <label>Create an Audio Sink with simple-protocol-tcp</label>
                                        pulseaudio server)</description>
                                <default>false</default>
                        </parameter>
+                       <parameter name="additionalFilters" type="text" required="false">
+                               <label>Additional Filters</label>
+                               <advanced>true</advanced>
+                               <description>Additional filters to select the proper device on the pulseaudio server, in case of ambiguity.
+                                       To be
+                                       selected, the device should have at least a property value matching this regular expression. You can use
+                                       multiple
+                                       regular expressions (separator ###).</description>
+                       </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>
index 847f3fe308d3950ec4a68044432cbdc506d6a330..cc7b04b91a79344dd27aae529dff9a72e839f559 100644 (file)
                <config-description>
                        <parameter name="name" type="text" required="true">
                                <label>Name</label>
-                               <description>The name of one specific device.</description>
+                               <description>The name of one specific device. You can also use the application name.</description>
+                       </parameter>
+                       <parameter name="additionalFilters" type="text" required="false">
+                               <label>Additional Filters</label>
+                               <advanced>true</advanced>
+                               <description>Additional filters to select the proper device on the pulseaudio server, in case of ambiguity.
+                                       To be
+                                       selected, the device should have at least a property value matching this regular expression. You can use
+                                       multiple
+                                       regular expressions (separator is ###).</description>
                        </parameter>
                </config-description>
        </thing-type>
index 9383b62bcbb224e3bbb92f6783142858c5ff9369..147ec817e824e8e37fcf4a69633123f030913b00 100644 (file)
                <config-description>
                        <parameter name="name" type="text" required="true">
                                <label>Name</label>
-                               <description>The name of one specific device.</description>
+                               <description>The name of one specific device. You can also use the description.</description>
+                       </parameter>
+                       <parameter name="additionalFilters" type="text" required="false">
+                               <label>Additional Filters</label>
+                               <advanced>true</advanced>
+                               <description>Additional filters to select the proper device on the pulseaudio server, in case of ambiguity.
+                                       To be
+                                       selected, the device should have at least a property value matching this regular expression. You can use
+                                       multiple
+                                       regular expressions (separator ###).</description>
                        </parameter>
                        <parameter name="activateSimpleProtocolSource" type="boolean" required="false">
                                <label>Create an Audio Source with simple-protocol-tcp</label>