]> git.basschouten.com Git - openhab-addons.git/commitdiff
[pulseaudio] register audio sources in openhab (#12376)
authorGiviMAD <GiviMAD@users.noreply.github.com>
Thu, 3 Mar 2022 12:01:07 +0000 (13:01 +0100)
committerGitHub <noreply@github.com>
Thu, 3 Mar 2022 12:01:07 +0000 (13:01 +0100)
* [pulseaudio] register audio sources in openha
* [pulseaudio] fix audio source reconnection
* [pulseaudio] audio source check record property and customize SOTimeout
* [pulseaudio] use pipe streams
* [pulseaudio] synchronize commands and update after module load

Signed-off-by: Miguel Álvarez Díez <miguelwork92@gmail.com>
13 files changed:
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/PulseAudioAudioSource.java [new file with mode: 0644]
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioBindingConfiguration.java
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/PulseaudioSimpleProtocolStream.java [new file with mode: 0644]
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceStatusListener.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/binding/binding.xml
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.xml
bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml

index cf41c9ba7f53100fb33fb0a7c2d3961c639998f0..82abe6815d4340f02e91a8ef51ab0edd37354557 100644 (file)
@@ -20,7 +20,7 @@ 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.
+The Pulseaudio binding can be customized to handle different devices. The Sink and Source 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/Source 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
@@ -31,7 +31,7 @@ You can use the GUI on the bindings page (click on the pulseaudio binding then "
 
 ```
 binding.pulseaudio:sink=true
-binding.pulseaudio:source=false
+binding.pulseaudio:source=true
 binding.pulseaudio:sinkInput=false
 binding.pulseaudio:sourceOutput=false
 ```
@@ -59,6 +59,14 @@ Sink things can register themselves as audio sink in openHAB. MP3 and WAV files
 Use the appropriate parameter in the sink thing to activate this possibility (activateSimpleProtocolSink).
 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.
 
+
+## Audio source
+
+Source things can register themselves as audio source in openHAB.
+WAV input format, rate and channels can be configured on the thing configuration. (defaults to pcm_signed,16000,1)
+Use the appropriate parameter in the source thing to activate this possibility (activateSimpleProtocolSource).
+This requires the module **module-simple-protocol-tcp** to be present on the target pulseaudio server. The binding will load this module on the pulseaudio server.
+
 ## Full Example
 
 ### pulseaudio.things
index 58be3a598fa13b244c351b625678e68c7f94f2b6..7862a6d29e071a59dfad864e064b5248fc2cc240 100644 (file)
@@ -17,11 +17,8 @@ import java.net.Socket;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.HashSet;
-import java.util.Locale;
 import java.util.Set;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
 
 import javax.sound.sampled.UnsupportedAudioFileException;
 
@@ -34,7 +31,6 @@ import org.openhab.core.audio.AudioStream;
 import org.openhab.core.audio.FixedLengthAudioStream;
 import org.openhab.core.audio.UnsupportedAudioFormatException;
 import org.openhab.core.audio.UnsupportedAudioStreamException;
-import org.openhab.core.library.types.PercentType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -42,25 +38,18 @@ import org.slf4j.LoggerFactory;
  * The audio sink for openhab, implemented by a connection to a pulseaudio sink
  *
  * @author Gwendal Roulleau - Initial contribution
+ * @author Miguel Álvarez - move some code to the PulseaudioSimpleProtocolStream class so sink and source can extend
+ *         from it.
  *
  */
 @NonNullByDefault
-public class PulseAudioAudioSink implements AudioSink {
+public class PulseAudioAudioSink extends PulseaudioSimpleProtocolStream implements AudioSink {
 
     private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class);
 
     private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
     private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
 
-    private PulseaudioHandler pulseaudioHandler;
-    private ScheduledExecutorService scheduler;
-
-    private @Nullable Socket clientSocket;
-
-    private boolean isIdle = true;
-
-    private @Nullable ScheduledFuture<?> scheduledDisconnection;
-
     static {
         SUPPORTED_FORMATS.add(AudioFormat.WAV);
         SUPPORTED_FORMATS.add(AudioFormat.MP3);
@@ -68,60 +57,15 @@ public class PulseAudioAudioSink implements AudioSink {
     }
 
     public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) {
-        this.pulseaudioHandler = pulseaudioHandler;
-        this.scheduler = scheduler;
-    }
-
-    @Override
-    public String getId() {
-        return pulseaudioHandler.getThing().getUID().toString();
-    }
-
-    @Override
-    public @Nullable String getLabel(@Nullable Locale locale) {
-        return pulseaudioHandler.getThing().getLabel();
-    }
-
-    /**
-     * Connect to pulseaudio with the simple protocol
-     *
-     * @throws IOException
-     * @throws InterruptedException when interrupted during the loading module wait
-     */
-    public void connectIfNeeded() throws IOException, InterruptedException {
-        Socket clientSocketLocal = clientSocket;
-        if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) {
-            String host = pulseaudioHandler.getHost();
-            int port = pulseaudioHandler.getSimpleTcpPort();
-            clientSocket = new Socket(host, port);
-            clientSocket.setSoTimeout(500);
-        }
-    }
-
-    /**
-     * Disconnect the socket to pulseaudio simple protocol
-     */
-    public void disconnect() {
-        final Socket clientSocketLocal = clientSocket;
-        if (clientSocketLocal != null && isIdle) {
-            logger.debug("Disconnecting");
-            try {
-                clientSocketLocal.close();
-            } catch (IOException e) {
-            }
-        } else {
-            logger.debug("Stream still running or socket not open");
-        }
+        super(pulseaudioHandler, scheduler);
     }
 
     @Override
     public void process(@Nullable AudioStream audioStream)
             throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
-
         if (audioStream == null) {
             return;
         }
-
         try (ConvertedInputStream normalizedPCMStream = new ConvertedInputStream(audioStream)) {
             for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
                 try {
@@ -129,7 +73,7 @@ public class PulseAudioAudioSink implements AudioSink {
                     final Socket clientSocketLocal = clientSocket;
                     if (clientSocketLocal != null) {
                         // send raw audio to the socket and to pulse audio
-                        isIdle = false;
+                        setIdle(false);
                         Instant start = Instant.now();
                         normalizedPCMStream.transferTo(clientSocketLocal.getOutputStream());
                         if (normalizedPCMStream.getDuration() != -1) { // ensure, if the sound has a duration
@@ -147,12 +91,10 @@ public class PulseAudioAudioSink implements AudioSink {
                 } catch (IOException e) {
                     disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown
                     if (countAttempt == 2) { // we won't retry : log and quit
-                        if (logger.isWarnEnabled()) {
-                            String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown";
-                            logger.warn(
-                                    "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
-                                    pulseaudioHandler.getHost(), port, e.getMessage());
-                        }
+                        String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown";
+                        logger.warn(
+                                "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
+                                pulseaudioHandler.getHost(), port, e.getMessage());
                         break;
                     }
                 } catch (InterruptedException ie) {
@@ -166,18 +108,7 @@ public class PulseAudioAudioSink implements AudioSink {
         } finally {
             scheduleDisconnect();
         }
-        isIdle = true;
-    }
-
-    public void scheduleDisconnect() {
-        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);
-        }
+        setIdle(true);
     }
 
     @Override
@@ -189,14 +120,4 @@ public class PulseAudioAudioSink implements AudioSink {
     public Set<Class<? extends AudioStream>> getSupportedStreams() {
         return SUPPORTED_STREAMS;
     }
-
-    @Override
-    public PercentType getVolume() {
-        return new PercentType(pulseaudioHandler.getLastVolume());
-    }
-
-    @Override
-    public void setVolume(PercentType volume) {
-        pulseaudioHandler.setVolume(volume.intValue());
-    }
 }
diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSource.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSource.java
new file mode 100644 (file)
index 0000000..9a1b39f
--- /dev/null
@@ -0,0 +1,249 @@
+/**
+ * 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;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.Socket;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
+import org.openhab.core.audio.AudioException;
+import org.openhab.core.audio.AudioFormat;
+import org.openhab.core.audio.AudioSource;
+import org.openhab.core.audio.AudioStream;
+import org.openhab.core.common.ThreadPoolManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The audio source for openhab, implemented by a connection to a pulseaudio source using Simple TCP protocol
+ *
+ * @author Miguel Álvarez - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class PulseAudioAudioSource extends PulseaudioSimpleProtocolStream implements AudioSource {
+
+    private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSource.class);
+    private final Set<PipedOutputStream> pipeOutputs = new HashSet<>();
+    private final ScheduledExecutorService executor;
+
+    private @Nullable Future<?> pipeWriteTask;
+
+    public PulseAudioAudioSource(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) {
+        super(pulseaudioHandler, scheduler);
+        executor = ThreadPoolManager
+                .getScheduledPool("OH-binding-" + pulseaudioHandler.getThing().getUID() + "-source");
+    }
+
+    @Override
+    public Set<AudioFormat> getSupportedFormats() {
+        var supportedFormats = new HashSet<AudioFormat>();
+        var audioFormat = pulseaudioHandler.getSourceAudioFormat();
+        if (audioFormat != null) {
+            supportedFormats.add(audioFormat);
+        }
+        return supportedFormats;
+    }
+
+    @Override
+    public AudioStream getInputStream(AudioFormat audioFormat) throws AudioException {
+        try {
+            for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
+                try {
+                    connectIfNeeded();
+                    final Socket clientSocketLocal = clientSocket;
+                    if (clientSocketLocal == null) {
+                        break;
+                    }
+                    var sourceFormat = pulseaudioHandler.getSourceAudioFormat();
+                    if (sourceFormat == null) {
+                        throw new AudioException("Unable to get source audio format");
+                    }
+                    if (!audioFormat.isCompatible(sourceFormat)) {
+                        throw new AudioException("Incompatible audio format requested");
+                    }
+                    setIdle(true);
+                    var pipeOutput = new PipedOutputStream();
+                    registerPipe(pipeOutput);
+                    var pipeInput = new PipedInputStream(pipeOutput, 1024 * 20) {
+                        @Override
+                        public void close() throws IOException {
+                            unregisterPipe(pipeOutput);
+                            super.close();
+                        }
+                    };
+                    // get raw audio from the pulse audio socket
+                    return new PulseAudioStream(sourceFormat, pipeInput, (idle) -> {
+                        setIdle(idle);
+                        if (idle) {
+                            scheduleDisconnect();
+                        } else {
+                            // ensure pipe is writing
+                            startPipeWrite();
+                        }
+                    });
+                } catch (IOException e) {
+                    disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown
+                    if (countAttempt == 2) { // we won't retry : log and quit
+                        String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown";
+                        logger.warn(
+                                "Error while trying to get audio from pulseaudio audio source. Cannot connect to {}:{}, error: {}",
+                                pulseaudioHandler.getHost(), port, e.getMessage());
+                        setIdle(true);
+                        throw e;
+                    }
+                } catch (InterruptedException ie) {
+                    logger.info("Interrupted during source audio connection: {}", ie.getMessage());
+                    setIdle(true);
+                    throw new AudioException(ie);
+                }
+                countAttempt++;
+            }
+        } catch (IOException e) {
+            throw new AudioException(e);
+        } finally {
+            scheduleDisconnect();
+        }
+        setIdle(true);
+        throw new AudioException("Unable to create input stream");
+    }
+
+    private synchronized void registerPipe(PipedOutputStream pipeOutput) {
+        this.pipeOutputs.add(pipeOutput);
+        startPipeWrite();
+    }
+
+    private void startPipeWrite() {
+        if (pipeWriteTask == null) {
+            this.pipeWriteTask = executor.submit(() -> {
+                int lengthRead;
+                byte[] buffer = new byte[1024];
+                while (true) {
+                    var stream = getSourceInputStream();
+                    if (stream != null) {
+                        try {
+                            lengthRead = stream.read(buffer);
+                            for (var output : pipeOutputs) {
+                                output.write(buffer, 0, lengthRead);
+                                output.flush();
+                            }
+                        } catch (IOException e) {
+                            logger.warn("IOException while reading from pulse source: {}", e.getMessage());
+                        } catch (RuntimeException e) {
+                            logger.warn("RuntimeException while reading from pulse source: {}", e.getMessage());
+                        }
+                    } else {
+                        logger.warn("Unable to get source input stream");
+                    }
+                }
+            });
+        }
+    }
+
+    private synchronized void unregisterPipe(PipedOutputStream pipeOutput) {
+        this.pipeOutputs.remove(pipeOutput);
+        stopPipeWriteTask();
+        try {
+            pipeOutput.close();
+        } catch (IOException ignored) {
+        }
+    }
+
+    private void stopPipeWriteTask() {
+        var pipeWriteTask = this.pipeWriteTask;
+        if (pipeOutputs.isEmpty() && pipeWriteTask != null) {
+            pipeWriteTask.cancel(true);
+            this.pipeWriteTask = null;
+        }
+    }
+
+    private @Nullable InputStream getSourceInputStream() {
+        try {
+            connectIfNeeded();
+        } catch (IOException | InterruptedException ignored) {
+        }
+        try {
+            return (clientSocket != null) ? clientSocket.getInputStream() : null;
+        } catch (IOException ignored) {
+            return null;
+        }
+    }
+
+    @Override
+    public void disconnect() {
+        stopPipeWriteTask();
+        super.disconnect();
+    }
+
+    static class PulseAudioStream extends AudioStream {
+        private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSource.class);
+        private final AudioFormat format;
+        private final InputStream input;
+        private final Consumer<Boolean> setIdle;
+        private boolean closed = false;
+
+        public PulseAudioStream(AudioFormat format, InputStream input, Consumer<Boolean> setIdle) {
+            this.input = input;
+            this.format = format;
+            this.setIdle = setIdle;
+        }
+
+        @Override
+        public AudioFormat getFormat() {
+            return format;
+        }
+
+        @Override
+        public int read() throws IOException {
+            byte[] b = new byte[1];
+            int bytesRead = read(b);
+            if (-1 == bytesRead) {
+                return bytesRead;
+            }
+            Byte bb = Byte.valueOf(b[0]);
+            return bb.intValue();
+        }
+
+        @Override
+        public int read(byte @Nullable [] b) throws IOException {
+            return read(b, 0, b.length);
+        }
+
+        @Override
+        public int read(byte @Nullable [] b, int off, int len) throws IOException {
+            logger.trace("reading from pulseaudio stream");
+            if (closed) {
+                throw new IOException("Stream is closed");
+            }
+            setIdle.accept(false);
+            return input.read(b, off, len);
+        }
+
+        @Override
+        public void close() throws IOException {
+            closed = true;
+            setIdle.accept(true);
+            input.close();
+        }
+    };
+}
index 6c16dbe4fa1437574e3b8e3ff540216c3da29189..7de2c44a7eeb561cc5cbf602ba030d531dfb2869 100644 (file)
@@ -28,7 +28,7 @@ public class PulseAudioBindingConfiguration {
 
     public boolean sink = true;
 
-    public boolean source = false;
+    public boolean source = true;
 
     public boolean sinkInput = false;
 
index c9fc05a1871093821b6df9a8af8f7dd262f35e9d..a8277e88b3ee4eaae44758bd99e8210c6bdbc0da 100644 (file)
@@ -20,6 +20,7 @@ import org.openhab.core.thing.ThingTypeUID;
  * used across the whole binding.
  *
  * @author Tobias Bräutigam - Initial contribution
+ * @author Miguel Álvarez - Add new configuration options to sink and source
  */
 @NonNullByDefault
 public class PulseaudioBindingConstants {
@@ -51,6 +52,13 @@ public class PulseaudioBindingConstants {
     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";
+    public static final String DEVICE_PARAMETER_AUDIO_SOURCE_ACTIVATION = "activateSimpleProtocolSource";
+    public static final String DEVICE_PARAMETER_AUDIO_SOURCE_PORT = "simpleProtocolSourcePort";
+    public static final String DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT = "simpleProtocolSourceIdleTimeout";
+    public static final String DEVICE_PARAMETER_AUDIO_SOURCE_RATE = "simpleProtocolSourceRate";
+    public static final String DEVICE_PARAMETER_AUDIO_SOURCE_FORMAT = "simpleProtocolSourceFormat";
+    public static final String DEVICE_PARAMETER_AUDIO_SOURCE_CHANNELS = "simpleProtocolSourceChannels";
+    public static final String DEVICE_PARAMETER_AUDIO_SOCKET_SO_TIMEOUT = "simpleProtocolSOTimeout";
 
     public static final String MODULE_SIMPLE_PROTOCOL_TCP_NAME = "module-simple-protocol-tcp";
     public static final int MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT = 4711;
index 0204c6b958a213a6bdd1e8d546c0c86460760a48..94bf0d28e61829391c3e6dffa166de77274a3ef7 100644 (file)
@@ -17,6 +17,7 @@ import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintStream;
+import java.math.BigDecimal;
 import java.net.NoRouteToHostException;
 import java.net.Socket;
 import java.net.SocketException;
@@ -28,6 +29,8 @@ import java.util.Optional;
 import java.util.Random;
 
 import org.eclipse.jdt.annotation.NonNull;
+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.items.AbstractAudioDeviceConfig;
 import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig.State;
@@ -47,14 +50,16 @@ import org.slf4j.LoggerFactory;
  * On the pulseaudio server the module-cli-protocol-tcp has to be loaded.
  *
  * @author Tobias Bräutigam - Initial contribution
+ * @author Miguel Álvarez - changes for loading audio source module and nullability annotations
  */
+@NonNullByDefault
 public class PulseaudioClient {
 
     private final Logger logger = LoggerFactory.getLogger(PulseaudioClient.class);
 
     private String host;
     private int port;
-    private Socket client;
+    private @Nullable Socket client;
 
     private List<AbstractAudioDeviceConfig> items;
     private List<Module> modules;
@@ -195,7 +200,7 @@ public class PulseaudioClient {
      * @param id
      * @return the corresponding {@link Module} to the given <code>id</code>
      */
-    public Module getModule(int id) {
+    public @Nullable Module getModule(int id) {
         for (Module module : modules) {
             if (module.getId() == id) {
                 return module;
@@ -220,7 +225,7 @@ public class PulseaudioClient {
      *
      * @return the corresponding {@link Sink} to the given <code>name</code>
      */
-    public Sink getSink(String name) {
+    public @Nullable Sink getSink(String name) {
         for (AbstractAudioDeviceConfig item : items) {
             if (item.getPaName().equalsIgnoreCase(name) && item instanceof Sink) {
                 return (Sink) item;
@@ -234,7 +239,7 @@ public class PulseaudioClient {
      *
      * @return the corresponding {@link Sink} to the given <code>id</code>
      */
-    public Sink getSink(int id) {
+    public @Nullable Sink getSink(int id) {
         for (AbstractAudioDeviceConfig item : items) {
             if (item.getId() == id && item instanceof Sink) {
                 return (Sink) item;
@@ -248,7 +253,7 @@ public class PulseaudioClient {
      *
      * @return the corresponding {@link SinkInput} to the given <code>name</code>
      */
-    public SinkInput getSinkInput(String name) {
+    public @Nullable SinkInput getSinkInput(String name) {
         for (AbstractAudioDeviceConfig item : items) {
             if (item.getPaName().equalsIgnoreCase(name) && item instanceof SinkInput) {
                 return (SinkInput) item;
@@ -262,7 +267,7 @@ public class PulseaudioClient {
      *
      * @return the corresponding {@link SinkInput} to the given <code>id</code>
      */
-    public SinkInput getSinkInput(int id) {
+    public @Nullable SinkInput getSinkInput(int id) {
         for (AbstractAudioDeviceConfig item : items) {
             if (item.getId() == id && item instanceof SinkInput) {
                 return (SinkInput) item;
@@ -276,7 +281,7 @@ public class PulseaudioClient {
      *
      * @return the corresponding {@link Source} to the given <code>name</code>
      */
-    public Source getSource(String name) {
+    public @Nullable Source getSource(String name) {
         for (AbstractAudioDeviceConfig item : items) {
             if (item.getPaName().equalsIgnoreCase(name) && item instanceof Source) {
                 return (Source) item;
@@ -290,7 +295,7 @@ public class PulseaudioClient {
      *
      * @return the corresponding {@link Source} to the given <code>id</code>
      */
-    public Source getSource(int id) {
+    public @Nullable Source getSource(int id) {
         for (AbstractAudioDeviceConfig item : items) {
             if (item.getId() == id && item instanceof Source) {
                 return (Source) item;
@@ -304,7 +309,7 @@ public class PulseaudioClient {
      *
      * @return the corresponding {@link SourceOutput} to the given <code>name</code>
      */
-    public SourceOutput getSourceOutput(String name) {
+    public @Nullable SourceOutput getSourceOutput(String name) {
         for (AbstractAudioDeviceConfig item : items) {
             if (item.getPaName().equalsIgnoreCase(name) && item instanceof SourceOutput) {
                 return (SourceOutput) item;
@@ -318,7 +323,7 @@ public class PulseaudioClient {
      *
      * @return the corresponding {@link SourceOutput} to the given <code>id</code>
      */
-    public SourceOutput getSourceOutput(int id) {
+    public @Nullable SourceOutput getSourceOutput(int id) {
         for (AbstractAudioDeviceConfig item : items) {
             if (item.getId() == id && item instanceof SourceOutput) {
                 return (SourceOutput) item;
@@ -332,7 +337,7 @@ public class PulseaudioClient {
      *
      * @return the corresponding {@link AbstractAudioDeviceConfig} to the given <code>name</code>
      */
-    public AbstractAudioDeviceConfig getGenericAudioItem(String name) {
+    public @Nullable AbstractAudioDeviceConfig getGenericAudioItem(String name) {
         for (AbstractAudioDeviceConfig item : items) {
             if (item.getPaName().equalsIgnoreCase(name)) {
                 return item;
@@ -351,7 +356,7 @@ public class PulseaudioClient {
      * @param item the {@link Sink} to handle
      * @param mute mutes the sink if true, unmutes if false
      */
-    public void setMute(AbstractAudioDeviceConfig item, boolean mute) {
+    public void setMute(@Nullable AbstractAudioDeviceConfig item, boolean mute) {
         if (item == null) {
             return;
         }
@@ -395,20 +400,27 @@ public class PulseaudioClient {
      * @throws InterruptedException
      */
     public Optional<Integer> loadModuleSimpleProtocolTcpIfNeeded(AbstractAudioDeviceConfig item,
-            Integer simpleTcpPortPref) throws InterruptedException {
+            Integer simpleTcpPortPref, @Nullable String format, @Nullable BigDecimal rate,
+            @Nullable BigDecimal channels) throws InterruptedException {
         int currentTry = 0;
         int simpleTcpPortToTry = simpleTcpPortPref;
+        String itemType = getItemCommandName(item);
         do {
-            Optional<Integer> simplePort = findSimpleProtocolTcpModule(item);
+            Optional<Integer> simplePort = findSimpleProtocolTcpModule(item, format, rate, channels);
 
             if (simplePort.isPresent()) {
                 return simplePort;
             } else {
-                sendRawCommand("load-module module-simple-protocol-tcp sink=" + item.getPaName() + " port="
-                        + simpleTcpPortToTry);
+                String moduleOptions = itemType + "=" + item.getPaName() + " port=" + simpleTcpPortToTry;
+                if (item instanceof Source && format != null && rate != null && channels != null) {
+                    moduleOptions = moduleOptions + String.format(" record=true format=%s rate=%d channels=%d", format,
+                            rate.longValue(), channels.intValue());
+                }
+                sendRawCommand("load-module module-simple-protocol-tcp " + moduleOptions);
                 simpleTcpPortToTry = new Random().nextInt(64512) + 1024; // a random port above 1024
             }
             Thread.sleep(100);
+            update();
             currentTry++;
         } while (currentTry < 3);
 
@@ -424,14 +436,49 @@ public class PulseaudioClient {
      * @param item
      * @return
      */
-    private Optional<Integer> findSimpleProtocolTcpModule(AbstractAudioDeviceConfig item) {
-        update();
-
+    private Optional<Integer> findSimpleProtocolTcpModule(AbstractAudioDeviceConfig item, @Nullable String format,
+            @Nullable BigDecimal rate, @Nullable BigDecimal channels) {
+        String itemType = getItemCommandName(item);
+        if (itemType == null) {
+            return Optional.empty();
+        }
         List<Module> modulesCopy = new ArrayList<Module>(modules);
+        var isSource = item instanceof Source;
         return modulesCopy.stream() // iteration on modules
                 .filter(module -> MODULE_SIMPLE_PROTOCOL_TCP_NAME.equals(module.getPaName())) // filter on module name
-                .filter(module -> extractArgumentFromLine("sink", module.getArgument()) // extract sink in argument
-                        .map(sinkName -> sinkName.equals(item.getPaName())).orElse(false)) // filter on sink name
+                .filter(module -> {
+                    boolean nameMatch = extractArgumentFromLine(itemType, module.getArgument()) // extract sick|source
+                            .map(name -> name.equals(item.getPaName())).orElse(false);
+                    if (isSource && nameMatch) {
+                        boolean recordStream = extractArgumentFromLine("record", module.getArgument())
+                                .map("true"::equals).orElse(false);
+                        if (!recordStream) {
+                            return false;
+                        }
+                        if (format != null) {
+                            boolean rateMatch = extractArgumentFromLine("format", module.getArgument())
+                                    .map(format::equals).orElse(false);
+                            if (!rateMatch) {
+                                return false;
+                            }
+                        }
+                        if (rate != null) {
+                            boolean rateMatch = extractArgumentFromLine("rate", module.getArgument())
+                                    .map(value -> Long.parseLong(value) == rate.longValue()).orElse(false);
+                            if (!rateMatch) {
+                                return false;
+                            }
+                        }
+                        if (channels != null) {
+                            boolean channelsMatch = extractArgumentFromLine("channels", module.getArgument())
+                                    .map(value -> Integer.parseInt(value) == channels.intValue()).orElse(false);
+                            if (!channelsMatch) {
+                                return false;
+                            }
+                        }
+                    }
+                    return nameMatch;
+                }) // filter on sink name
                 .findAny() // get a corresponding module
                 .map(module -> extractArgumentFromLine("port", module.getArgument())
                         .orElse(Integer.toString(MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT))) // get port
@@ -458,7 +505,7 @@ public class PulseaudioClient {
      * @param item
      * @return
      */
-    private String getItemCommandName(AbstractAudioDeviceConfig item) {
+    private @Nullable String getItemCommandName(AbstractAudioDeviceConfig item) {
         if (item instanceof Sink) {
             return ITEM_SINK;
         } else if (item instanceof Source) {
@@ -478,7 +525,7 @@ public class PulseaudioClient {
      * @param vol the new volume percent value the {@link AbstractAudioDeviceConfig} should be changed to (possible
      *            values from 0 - 100)
      */
-    public void setVolumePercent(AbstractAudioDeviceConfig item, int vol) {
+    public void setVolumePercent(@Nullable AbstractAudioDeviceConfig item, int vol) {
         int volumeToSet = vol;
         if (item == null) {
             return;
@@ -505,7 +552,7 @@ public class PulseaudioClient {
      * @param combinedSink the combined sink which slaves should be changed
      * @param sinks the list of new slaves
      */
-    public void setCombinedSinkSlaves(Sink combinedSink, List<Sink> sinks) {
+    public void setCombinedSinkSlaves(@Nullable Sink combinedSink, List<Sink> sinks) {
         if (combinedSink == null || !combinedSink.isCombinedSink()) {
             return;
         }
@@ -528,7 +575,7 @@ public class PulseaudioClient {
      * @param sinkInput the sink-input to be rerouted
      * @param sink the new sink the sink-input should be routed to
      */
-    public void moveSinkInput(SinkInput sinkInput, Sink sink) {
+    public void moveSinkInput(@Nullable SinkInput sinkInput, @Nullable Sink sink) {
         if (sinkInput == null || sink == null) {
             return;
         }
@@ -542,7 +589,7 @@ public class PulseaudioClient {
      * @param sourceOutput the source-output to be rerouted
      * @param source the new source the source-output should be routed to
      */
-    public void moveSourceOutput(SourceOutput sourceOutput, Source source) {
+    public void moveSourceOutput(@Nullable SourceOutput sourceOutput, @Nullable Source source) {
         if (sourceOutput == null || source == null) {
             return;
         }
@@ -556,7 +603,7 @@ public class PulseaudioClient {
      * @param source the source which state should be changed
      * @param suspend suspend it or not
      */
-    public void suspendSource(Source source, boolean suspend) {
+    public void suspendSource(@Nullable Source source, boolean suspend) {
         if (source == null) {
             return;
         }
@@ -577,7 +624,7 @@ public class PulseaudioClient {
      * @param sink the sink which state should be changed
      * @param suspend suspend it or not
      */
-    public void suspendSink(Sink sink, boolean suspend) {
+    public void suspendSink(@Nullable Sink sink, boolean suspend) {
         if (sink == null) {
             return;
         }
@@ -613,9 +660,9 @@ public class PulseaudioClient {
         update();
     }
 
-    private void sendRawCommand(String command) {
+    private synchronized void sendRawCommand(String command) {
         checkConnection();
-        if (client != null) {
+        if (client != null && client.isConnected()) {
             try {
                 PrintStream out = new PrintStream(client.getOutputStream(), true);
                 logger.trace("sending command {} to pa-server {}", command, host);
@@ -632,7 +679,7 @@ public class PulseaudioClient {
         logger.trace("_sendRawRequest({})", command);
         checkConnection();
         String result = "";
-        if (client != null) {
+        if (client != null && client.isConnected()) {
             try {
                 PrintStream out = new PrintStream(client.getOutputStream(), true);
                 out.print(command + "\r\n");
diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioSimpleProtocolStream.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioSimpleProtocolStream.java
new file mode 100644 (file)
index 0000000..276ec38
--- /dev/null
@@ -0,0 +1,119 @@
+/**
+ * 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;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.Locale;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
+import org.openhab.core.library.types.PercentType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A connection to a pulseaudio Simple TCP Protocol
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ * @author Miguel Álvarez - Refactor some code from PulseAudioAudioSink here
+ *
+ */
+@NonNullByDefault
+public abstract class PulseaudioSimpleProtocolStream {
+
+    private final Logger logger = LoggerFactory.getLogger(PulseaudioSimpleProtocolStream.class);
+
+    protected PulseaudioHandler pulseaudioHandler;
+    protected ScheduledExecutorService scheduler;
+
+    protected @Nullable Socket clientSocket;
+
+    private boolean isIdle = true;
+
+    private @Nullable ScheduledFuture<?> scheduledDisconnection;
+
+    public PulseaudioSimpleProtocolStream(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) {
+        this.pulseaudioHandler = pulseaudioHandler;
+        this.scheduler = scheduler;
+    }
+
+    /**
+     * Connect to pulseaudio with the simple protocol
+     *
+     * @throws IOException
+     * @throws InterruptedException when interrupted during the loading module wait
+     */
+    public void connectIfNeeded() throws IOException, InterruptedException {
+        Socket clientSocketLocal = clientSocket;
+        if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) {
+            logger.debug("Simple TCP Stream connecting");
+            String host = pulseaudioHandler.getHost();
+            int port = pulseaudioHandler.getSimpleTcpPort();
+            clientSocket = new Socket(host, port);
+            clientSocket.setSoTimeout(pulseaudioHandler.getBasicProtocolSOTimeout());
+        }
+    }
+
+    /**
+     * Disconnect the socket to pulseaudio simple protocol
+     */
+    public void disconnect() {
+        final Socket clientSocketLocal = clientSocket;
+        if (clientSocketLocal != null && isIdle) {
+            logger.debug("Simple TCP Stream disconnecting");
+            try {
+                clientSocketLocal.close();
+            } catch (IOException ignored) {
+            }
+        } else {
+            logger.debug("Stream still running or socket not open");
+        }
+    }
+
+    public void scheduleDisconnect() {
+        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);
+        }
+    }
+
+    public PercentType getVolume() {
+        return new PercentType(pulseaudioHandler.getLastVolume());
+    }
+
+    public void setVolume(PercentType volume) {
+        pulseaudioHandler.setVolume(volume.intValue());
+    }
+
+    public String getId() {
+        return pulseaudioHandler.getThing().getUID().toString();
+    }
+
+    public String getLabel(@Nullable Locale locale) {
+        var label = pulseaudioHandler.getThing().getLabel();
+        return label != null ? label : pulseaudioHandler.getThing().getUID().getId();
+    }
+
+    public void setIdle(boolean idle) {
+        isIdle = idle;
+    }
+}
index 30f1fac3ad9f250aeb20d52f84ac8f3ca93d23f2..4df412417ec9fe466185407018d6a7272e106ef7 100644 (file)
@@ -12,6 +12,7 @@
  */
 package org.openhab.binding.pulseaudio.internal.handler;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
 import org.openhab.core.thing.Bridge;
 import org.openhab.core.thing.ThingUID;
@@ -23,6 +24,7 @@ import org.openhab.core.thing.ThingUID;
  * @author Tobias Bräutigam - Initial contribution
  *
  */
+@NonNullByDefault
 public interface DeviceStatusListener {
 
     /**
index c145dcaaefa727fdf1c3e543cc387445baaaef1c..df107b91b22f028c35a641d0a123a2a84a9637cc 100644 (file)
@@ -28,12 +28,18 @@ import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.pulseaudio.internal.PulseAudioAudioSink;
+import org.openhab.binding.pulseaudio.internal.PulseAudioAudioSource;
 import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
 import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
 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.core.audio.AudioFormat;
 import org.openhab.core.audio.AudioSink;
+import org.openhab.core.audio.AudioSource;
 import org.openhab.core.config.core.Configuration;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.IncreaseDecreaseType;
@@ -62,29 +68,28 @@ import org.slf4j.LoggerFactory;
  * sent to one of the channels.
  *
  * @author Tobias Bräutigam - Initial contribution
+ * @author Miguel Álvarez - Register audio source and refactor
  */
+@NonNullByDefault
 public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusListener {
 
     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
             .unmodifiableSet(Stream.of(SINK_THING_TYPE, COMBINED_SINK_THING_TYPE, SINK_INPUT_THING_TYPE,
                     SOURCE_THING_TYPE, SOURCE_OUTPUT_THING_TYPE).collect(Collectors.toSet()));
-
-    private int refresh = 60; // refresh every minute as default
-    private ScheduledFuture<?> refreshJob;
-
-    private PulseaudioBridgeHandler bridgeHandler;
-
     private final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class);
+    private final int refresh = 60; // refresh every minute as default
 
-    private String name;
-
-    private PulseAudioAudioSink audioSink;
-
-    private Integer savedVolume;
+    private @Nullable PulseaudioBridgeHandler bridgeHandler;
+    private @Nullable String name;
+    private @Nullable ScheduledFuture<?> refreshJob;
+    private @Nullable PulseAudioAudioSink audioSink;
+    private @Nullable PulseAudioAudioSource audioSource;
+    private @Nullable Integer savedVolume;
 
-    private Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
+    private final Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
+    private final Map<String, ServiceRegistration<AudioSource>> audioSourceRegistrations = new ConcurrentHashMap<>();
 
-    private BundleContext bundleContext;
+    private final BundleContext bundleContext;
 
     public PulseaudioHandler(Thing thing, BundleContext bundleContext) {
         super(thing);
@@ -109,6 +114,14 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
                 audioSinkSetup();
             }
         }
+        // if it's a SOURCE thing, then maybe we have to activate the audio source
+        if (SOURCE_THING_TYPE.equals(thing.getThingTypeUID())) {
+            // check the property to see if we it's enabled :
+            Boolean sourceActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_ACTIVATION);
+            if (sourceActivated != null && sourceActivated) {
+                audioSourceSetup();
+            }
+        }
     }
 
     private void audioSinkSetup() {
@@ -139,6 +152,34 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
         });
     }
 
+    private void audioSourceSetup() {
+        final PulseaudioHandler thisHandler = this;
+        scheduler.submit(new Runnable() {
+            @Override
+            public void run() {
+                // Register the source as an audio source in openhab
+                logger.trace("Registering an audio source for pulse audio source thing {}", thing.getUID());
+                PulseAudioAudioSource audioSource = new PulseAudioAudioSource(thisHandler, scheduler);
+                setAudioSource(audioSource);
+                try {
+                    audioSource.connectIfNeeded();
+                } catch (IOException e) {
+                    logger.warn("pulseaudio binding cannot connect to the module-simple-protocol-tcp on {} ({})",
+                            getHost(), e.getMessage());
+                } catch (InterruptedException i) {
+                    logger.info("Interrupted during source audio connection: {}", i.getMessage());
+                    return;
+                } finally {
+                    audioSource.scheduleDisconnect();
+                }
+                @SuppressWarnings("unchecked")
+                ServiceRegistration<AudioSource> reg = (ServiceRegistration<AudioSource>) bundleContext
+                        .registerService(AudioSource.class.getName(), audioSource, new Hashtable<>());
+                audioSourceRegistrations.put(thing.getUID().toString(), reg);
+            }
+        });
+    }
+
     @Override
     public void dispose() {
         if (refreshJob != null && !refreshJob.isCancelled()) {
@@ -152,16 +193,23 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
         }
         logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
         super.dispose();
-
         if (audioSink != null) {
             audioSink.disconnect();
         }
-
+        if (audioSource != null) {
+            audioSource.disconnect();
+        }
         // Unregister the potential pulse audio sink's audio sink
-        ServiceRegistration<AudioSink> reg = audioSinkRegistrations.remove(getThing().getUID().toString());
-        if (reg != null) {
+        ServiceRegistration<AudioSink> sinkReg = audioSinkRegistrations.remove(getThing().getUID().toString());
+        if (sinkReg != null) {
             logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID());
-            reg.unregister();
+            sinkReg.unregister();
+        }
+        // Unregister the potential pulse audio source's audio sources
+        ServiceRegistration<AudioSource> sourceReg = audioSourceRegistrations.remove(getThing().getUID().toString());
+        if (sourceReg != null) {
+            logger.trace("Unregistering the audio sync service for pulse audio source thing {}", getThing().getUID());
+            sourceReg.unregister();
         }
     }
 
@@ -172,7 +220,7 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
                 if (bridgeHandler != null) {
                     if (bridgeHandler.getDevice(name) == null) {
                         updateStatus(ThingStatus.OFFLINE);
-                        bridgeHandler = null;
+                        this.bridgeHandler = null;
                     } else {
                         updateStatus(ThingStatus.ONLINE);
                     }
@@ -182,14 +230,14 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
                 }
             } catch (Exception e) {
                 logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
-                bridgeHandler = null;
+                this.bridgeHandler = null;
             }
         };
 
         refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refresh, TimeUnit.SECONDS);
     }
 
-    private synchronized PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
+    private synchronized @Nullable PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
         if (this.bridgeHandler == null) {
             Bridge bridge = getBridge();
             if (bridge == null) {
@@ -367,18 +415,73 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
      * @throws InterruptedException when interrupted during the loading module wait
      */
     public int getSimpleTcpPort() throws InterruptedException {
-        Integer simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration()
-                .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_PORT)).intValue();
-
-        PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler();
+        var bridgeHandler = getPulseaudioBridgeHandler();
         AbstractAudioDeviceConfig device = bridgeHandler.getDevice(name);
-        return getPulseaudioBridgeHandler().getClient().loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPortPref)
-                .orElse(simpleTcpPortPref);
+        String simpleTcpPortPrefName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_PORT
+                : DEVICE_PARAMETER_AUDIO_SINK_PORT;
+        BigDecimal simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration().get(simpleTcpPortPrefName));
+        int simpleTcpPort = simpleTcpPortPref != null ? simpleTcpPortPref.intValue()
+                : MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT;
+        String simpleFormat = ((String) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_FORMAT));
+        BigDecimal simpleRate = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_RATE);
+        BigDecimal simpleChannels = (BigDecimal) getThing().getConfiguration()
+                .get(DEVICE_PARAMETER_AUDIO_SOURCE_CHANNELS);
+        return getPulseaudioBridgeHandler().getClient()
+                .loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPort, simpleFormat, simpleRate, simpleChannels)
+                .orElse(simpleTcpPort);
+    }
+
+    public @Nullable AudioFormat getSourceAudioFormat() {
+        String simpleFormat = ((String) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_FORMAT));
+        BigDecimal simpleRate = ((BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_RATE));
+        BigDecimal simpleChannels = ((BigDecimal) getThing().getConfiguration()
+                .get(DEVICE_PARAMETER_AUDIO_SOURCE_CHANNELS));
+        if (simpleFormat == null || simpleRate == null || simpleChannels == null) {
+            return null;
+        }
+        switch (simpleFormat) {
+            case "u8":
+                return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, null, 8, 1,
+                        simpleRate.longValue(), simpleChannels.intValue());
+            case "s16le":
+                return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 1,
+                        simpleRate.longValue(), simpleChannels.intValue());
+            case "s16be":
+                return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, true, 16, 1,
+                        simpleRate.longValue(), simpleChannels.intValue());
+            case "s24le":
+                return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 24, 1,
+                        simpleRate.longValue(), simpleChannels.intValue());
+            case "s24be":
+                return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, true, 24, 1,
+                        simpleRate.longValue(), simpleChannels.intValue());
+            case "s32le":
+                return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 32, 1,
+                        simpleRate.longValue(), simpleChannels.intValue());
+            case "s32be":
+                return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, true, 32, 1,
+                        simpleRate.longValue(), simpleChannels.intValue());
+            default:
+                logger.warn("unsupported format {}", simpleFormat);
+                return null;
+        }
     }
 
     public int getIdleTimeout() {
-        return ((BigDecimal) getThing().getConfiguration()
-                .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT)).intValue();
+        var handler = getPulseaudioBridgeHandler();
+        if (handler == null) {
+            return 30000;
+        }
+        AbstractAudioDeviceConfig device = handler.getDevice(name);
+        String idleTimeoutPropName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT
+                : DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT;
+        var idleTimeout = (BigDecimal) getThing().getConfiguration().get(idleTimeoutPropName);
+        return idleTimeout != null ? idleTimeout.intValue() : 30000;
+    }
+
+    public int getBasicProtocolSOTimeout() {
+        var soTimeout = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOCKET_SO_TIMEOUT);
+        return soTimeout != null ? soTimeout.intValue() : 500;
     }
 
     @Override
@@ -400,4 +503,8 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
     public void setAudioSink(PulseAudioAudioSink audioSink) {
         this.audioSink = audioSink;
     }
+
+    public void setAudioSource(PulseAudioAudioSource audioSource) {
+        this.audioSource = audioSource;
+    }
 }
index 15ea8c80a602882ccc08e2830c53062aeb616422..e95a7f89a4aa3d2a170575d67ed3e3e89ca4fa35 100644 (file)
@@ -20,7 +20,7 @@
                <parameter name="source" type="boolean">
                        <label>Import Sources</label>
                        <description>Activate the import of source elements.</description>
-                       <default>false</default>
+                       <default>true</default>
                </parameter>
                <parameter name="sourceOutput" type="boolean">
                        <label>Import Source Outputs</label>
index 62471edcad5e4b79baadd92c3eeb303265d47dc4..2a56feb6c7ddbbaf62137635ffb04eaa3796cbb8 100644 (file)
@@ -43,14 +43,37 @@ thing-type.config.pulseaudio.sink.activateSimpleProtocolSink.label = Create an A
 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.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
 thing-type.config.pulseaudio.sink.simpleProtocolSinkIdleTimeout.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. -1 for no disconnection.
 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.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.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
+thing-type.config.pulseaudio.source.simpleProtocolSourceChannels.description = The audio channel number to be used by module-simple-protocol-tcp on the pulseaudio server
+thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.label = Simple Protocol Format
+thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.description = The audio format to be used by module-simple-protocol-tcp on the pulseaudio server
+thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.u8 = PCM signed 8-bit
+thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s16le = PCM signed 16-bit little-endian
+thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s16be = PCM signed 16-bit big-endian
+thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s24le = PCM unsigned 24-bit little-endian
+thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s24be = PCM unsigned 24-bit big-endian
+thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s32le = PCM signed 32-bit little-endian
+thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s32be = PCM signed 32-bit big-endian
+thing-type.config.pulseaudio.source.simpleProtocolSourceIdleTimeout.label = Idle Timeout
+thing-type.config.pulseaudio.source.simpleProtocolSourceIdleTimeout.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 source can go to idle mode. -1 for no disconnection.
+thing-type.config.pulseaudio.source.simpleProtocolSourcePort.label = Simple Protocol Port
+thing-type.config.pulseaudio.source.simpleProtocolSourcePort.description = Default Port to allocate to be used by module-simple-protocol-tcp on the pulseaudio server
+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.
 
index 121faa7a5a92b9e023dc04f41429ba47b0a64443..6e4a44fe963992a431ba790c0ca27fde295cad38 100644 (file)
                                </description>
                                <default>30000</default>
                        </parameter>
+                       <parameter name="simpleProtocolSOTimeout" type="integer" min="250" max="2000">
+                               <label>Simple Protocol SO Timeout</label>
+                               <description>Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp. You can tune
+                                       this option if the socket disconnect frequently.</description>
+                               <default>500</default>
+                               <advanced>true</advanced>
+                       </parameter>
                </config-description>
        </thing-type>
 
index f783b3485b2911d0f60c31855021e86e60da1221..9383b62bcbb224e3bbb92f6783142858c5ff9369 100644 (file)
                                <label>Name</label>
                                <description>The name of one specific device.</description>
                        </parameter>
+                       <parameter name="activateSimpleProtocolSource" type="boolean" required="false">
+                               <label>Create an Audio Source with simple-protocol-tcp</label>
+                               <description>Activation of a corresponding source in OpenHAB (module-simple-protocol-tcp must be available on the
+                                       pulseaudio server)</description>
+                               <default>false</default>
+                       </parameter>
+                       <parameter name="simpleProtocolSourcePort" type="integer" required="false">
+                               <label>Simple Protocol Port</label>
+                               <description>Default Port to allocate to be used by module-simple-protocol-tcp on the pulseaudio server</description>
+                               <default>4710</default>
+                       </parameter>
+                       <parameter name="simpleProtocolSourceIdleTimeout" 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 source can go to idle mode. -1 for no disconnection.
+                               </description>
+                               <default>30000</default>
+                       </parameter>
+                       <parameter name="simpleProtocolSourceFormat" type="text">
+                               <label>Simple Protocol Format</label>
+                               <description>The audio format to be used by module-simple-protocol-tcp on the pulseaudio server</description>
+                               <default>s16le</default>
+                               <advanced>true</advanced>
+                               <options>
+                                       <option value="u8">PCM signed 8-bit</option>
+                                       <option value="s16le">PCM signed 16-bit little-endian</option>
+                                       <option value="s16be">PCM signed 16-bit big-endian</option>
+                                       <option value="s24le">PCM unsigned 24-bit little-endian</option>
+                                       <option value="s24be">PCM unsigned 24-bit big-endian</option>
+                                       <option value="s32le">PCM signed 32-bit little-endian</option>
+                                       <option value="s32be">PCM signed 32-bit big-endian</option>
+                               </options>
+                       </parameter>
+                       <parameter name="simpleProtocolSourceRate" type="integer" min="0">
+                               <label>Simple Protocol Rate</label>
+                               <description>The audio sample rate to be used by module-simple-protocol-tcp on the pulseaudio server</description>
+                               <default>16000</default>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="simpleProtocolSourceChannels" type="integer" min="1">
+                               <label>Simple Protocol Channels</label>
+                               <description>The audio channel number to be used by module-simple-protocol-tcp on the pulseaudio server</description>
+                               <default>1</default>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="simpleProtocolSOTimeout" type="integer" min="250" max="2000">
+                               <label>Simple Protocol SO Timeout</label>
+                               <description>Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp. You can tune
+                                       this option if the socket disconnect frequently.</description>
+                               <default>500</default>
+                               <advanced>true</advanced>
+                       </parameter>
                </config-description>
        </thing-type>