]> git.basschouten.com Git - openhab-addons.git/commitdiff
[pulseaudio] Fix sink-input configuration and other (#11272) (#11276)
authordalgwen <dalgwen@users.noreply.github.com>
Sat, 9 Oct 2021 09:44:20 +0000 (11:44 +0200)
committerGitHub <noreply@github.com>
Sat, 9 Oct 2021 09:44:20 +0000 (11:44 +0200)
* [pulseaudio] Fix sink-input configuration  and other small improvements (#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 #11272

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
* Small fixes after proofreading

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
Co-authored-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
bundles/org.openhab.binding.pulseaudio/README.md
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioBindingConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioBindingConfigurationListener.java [new file with mode: 0644]
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/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/resources/OH-INF/thing/sink.xml

index 571c82dc6ad87b8471afb68ef89f7146767b93d6..2c8a4aa5f3e66495e739944a610c4829d810c26a 100644 (file)
@@ -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.
@@ -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
 ```
index ba3ba0f1c67ef59e254700ad4f693d91fce57398..9de4d3cc7ccc7909361e4c261418a5b4d4012d17 100644 (file)
@@ -22,6 +22,7 @@ import java.util.Locale;
 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;
@@ -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);
@@ -254,8 +257,14 @@ public class PulseAudioAudioSink implements AudioSink {
     }
 
     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
diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioBindingConfiguration.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioBindingConfiguration.java
new file mode 100644 (file)
index 0000000..17e0c22
--- /dev/null
@@ -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);
+    }
+}
diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioBindingConfigurationListener.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioBindingConfigurationListener.java
new file mode 100644 (file)
index 0000000..affe0e4
--- /dev/null
@@ -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();
+}
index 1a6a8954262499b36131eba189b25b3711bbf691..1324c28a8f1ce4ab245947bf68ff24d02731e402 100644 (file)
@@ -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;
 
@@ -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);
-    }
 }
index 54cbc552b2a213093cb09644dc421ac01f0e2849..b4d8be4070941480a5c203d909f4ed5cb930e5e9 100644 (file)
@@ -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
      */
@@ -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<>();
@@ -147,19 +149,19 @@ public class PulseaudioClient {
 
         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));
         }
index 06c32ab26605d9ef35ea90df704ae38ae62b426f..7884d4b6182899e7c1ea283c5e8287f95a848c49 100644 (file)
@@ -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;
@@ -36,7 +34,9 @@ import org.openhab.core.thing.binding.ThingHandler;
 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;
 
@@ -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);
@@ -109,7 +111,7 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
         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)) {
@@ -119,25 +121,16 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
         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);
     }
 }
index c91ecfe2c55de7292b26f76c46b57a28e5f219d2..c2047d797995740b3e004e9fd086da2074fbfabc 100644 (file)
@@ -24,6 +24,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
 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;
@@ -45,7 +47,7 @@ import org.slf4j.LoggerFactory;
  * @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
@@ -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())) {
@@ -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
@@ -132,7 +141,7 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler {
         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);
@@ -151,10 +160,13 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler {
                     host, port);
             updateStatus(ThingStatus.OFFLINE);
         }
+
+        this.configuration.addPulseAudioBindingConfigurationListener(this);
     }
 
     @Override
     public void dispose() {
+        this.configuration.removePulseAudioBindingConfigurationListener(this);
         if (pollingJob != null) {
             pollingJob.cancel(true);
         }
@@ -174,4 +186,9 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler {
     public boolean unregisterDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
         return deviceStatusListeners.remove(deviceStatusListener);
     }
+
+    @Override
+    public void bindingConfigurationChanged() {
+        update();
+    }
 }
index 2aae655d039dad4671008987ff56d85efb71ade2..5b662e947b4daaf4df0ae0ca7920f3011717a874 100644 (file)
@@ -146,8 +146,10 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
             refreshJob = null;
         }
         updateStatus(ThingStatus.OFFLINE);
-        bridgeHandler.unregisterDeviceStatusListener(this);
-        bridgeHandler = null;
+        if (bridgeHandler != null) {
+            bridgeHandler.unregisterDeviceStatusListener(this);
+            bridgeHandler = null;
+        }
         logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
         super.dispose();
 
index e96dbdfc8cbaf1bc92e4497f64b442a54629f5ea..121faa7a5a92b9e023dc04f41429ba47b0a64443 100644 (file)
@@ -36,7 +36,7 @@
                        <parameter name="simpleProtocolSinkIdleTimeout" type="integer" required="false">
                                <label>Idle Timeout</label>
                                <description>Timeout in ms after which the connection will be closed when no stream is running. This ensures that
-                                       your speaker is not on all the time and the pulseaudio sink can go to idle mode.
+                                       your speaker is not on all the time and the pulseaudio sink can go to idle mode. -1 for no disconnection.
                                </description>
                                <default>30000</default>
                        </parameter>