]> git.basschouten.com Git - openhab-addons.git/commitdiff
[pulseaudio] Small bugfixes and rewrite (#12581)
authorGwendal Roulleau <dalgwen@users.noreply.github.com>
Sat, 9 Apr 2022 22:25:45 +0000 (00:25 +0200)
committerGitHub <noreply@github.com>
Sat, 9 Apr 2022 22:25:45 +0000 (00:25 +0200)
* [pulseaudio] small fixes and rewrite

- All classes are now @NonNullByDefault
- all build warnings cleared
- no more need for a watchdog scheduled thread for every pulseaudio device : the bridge now handles sending information to child
- fix bug : exception at startup when child handler try to get information from the bridge too soon is now handled by waiting 2 seconds if necessary
- fix bug : playing MP3 with high bitrate is now OK with the replacement of the ResetableInputStream by a standard BufferedInputStream that handle mark/reset method better
- fix bug : ghost device listener no longer receive event after dispose
- fix bug : discovery doesn't show already added thing anymore
- Updating the status bridge to ONLINE only AFTER the update method is done.
- Use the bridgeStatusChanged method in the childhandler to get opportunity to test if the child could go ONLINE, (and by the way initialize the audiosink and audiosource, has they also need information from the bridge)

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
Co-authored-by: Laurent Garnier <lg.hc@free.fr>
19 files changed:
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/ConvertedInputStream.java
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
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/PulseaudioSimpleProtocolStream.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/discovery/PulseaudioDiscoveryParticipant.java
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/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/AbstractDeviceConfig.java
bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Module.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

index 5fd5a35d4847cd1ca656b5cacb0abf6f12f61ca3..6d7c7632743fcdfa7d7dcd8e25e05405ffbdcd67 100644 (file)
@@ -12,6 +12,7 @@
  */
 package org.openhab.binding.pulseaudio.internal;
 
+import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Map;
@@ -55,14 +56,13 @@ public class ConvertedInputStream extends InputStream {
 
     public ConvertedInputStream(AudioStream innerInputStream)
             throws UnsupportedAudioFormatException, UnsupportedAudioFileException, IOException {
-
         this.audioFormat = innerInputStream.getFormat();
 
         if (innerInputStream instanceof FixedLengthAudioStream) {
             length = ((FixedLengthAudioStream) innerInputStream).length();
         }
 
-        pcmNormalizedInputStream = getPCMStreamNormalized(getPCMStream(new ResetableInputStream(innerInputStream)));
+        pcmNormalizedInputStream = getPCMStreamNormalized(getPCMStream(new BufferedInputStream(innerInputStream)));
     }
 
     @Override
@@ -108,7 +108,6 @@ public class ConvertedInputStream extends InputStream {
      * @return A PCM normalized stream (2 channel, 44100hz, 16 bit signed)
      */
     private AudioInputStream getPCMStreamNormalized(AudioInputStream pcmInputStream) {
-
         javax.sound.sampled.AudioFormat format = pcmInputStream.getFormat();
         if (format.getChannels() != 2
                 || !format.getEncoding().equals(javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED)
@@ -138,7 +137,6 @@ public class ConvertedInputStream extends InputStream {
      */
     private AudioInputStream getPCMStream(InputStream resetableInnerInputStream)
             throws UnsupportedAudioFileException, IOException, UnsupportedAudioFormatException {
-
         if (AudioFormat.MP3.isCompatible(audioFormat)) {
             MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader();
 
@@ -170,7 +168,6 @@ public class ConvertedInputStream extends InputStream {
                     sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false);
 
             return mpegconverter.getAudioInputStream(convertFormat, sourceAIS);
-
         } else if (AudioFormat.WAV.isCompatible(audioFormat)) {
             // return the same input stream, but try to compute the duration first
             AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(resetableInnerInputStream);
@@ -187,71 +184,4 @@ public class ConvertedInputStream extends InputStream {
                     audioFormat);
         }
     }
-
-    /**
-     * This class add reset capability (on the first bytes only)
-     * to an AudioStream. This is necessary for the parsing / format detection.
-     *
-     */
-    public static class ResetableInputStream extends InputStream {
-
-        private static final int BUFFER_LENGTH = 10000;
-
-        private final InputStream originalInputStream;
-
-        private int position = -1;
-        private int markPosition = -1;
-        private int maxPreviousPosition = -2;
-
-        private byte[] startingBuffer = new byte[BUFFER_LENGTH + 1];
-
-        public ResetableInputStream(InputStream originalInputStream) {
-            this.originalInputStream = originalInputStream;
-        }
-
-        @Override
-        public void close() throws IOException {
-            originalInputStream.close();
-        }
-
-        @Override
-        public int read() throws IOException {
-            if (position >= BUFFER_LENGTH || originalInputStream.markSupported()) {
-                return originalInputStream.read();
-            } else {
-                position++;
-                if (position <= maxPreviousPosition) {
-                    return Byte.toUnsignedInt(startingBuffer[position]);
-                } else {
-                    int currentByte = originalInputStream.read();
-                    startingBuffer[position] = (byte) currentByte;
-                    maxPreviousPosition = position;
-                    return currentByte;
-                }
-            }
-        }
-
-        @Override
-        public synchronized void mark(int readlimit) {
-            if (originalInputStream.markSupported()) {
-                originalInputStream.mark(readlimit);
-            }
-            markPosition = position;
-        }
-
-        @Override
-        public boolean markSupported() {
-            return true;
-        }
-
-        @Override
-        public synchronized void reset() throws IOException {
-            if (originalInputStream.markSupported()) {
-                originalInputStream.reset();
-            } else if (position >= BUFFER_LENGTH) {
-                throw new IOException("mark/reset not supported above " + BUFFER_LENGTH + " bytes");
-            }
-            position = markPosition;
-        }
-    }
 }
index 7862a6d29e071a59dfad864e064b5248fc2cc240..8e42bb1c261e7ec13c0e43b3866a9cbd0519c6b1 100644 (file)
@@ -91,7 +91,9 @@ public class PulseAudioAudioSink extends PulseaudioSimpleProtocolStream implemen
                 } 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";
+                        final Socket clientSocketLocal = clientSocket;
+                        String port = clientSocketLocal != null ? Integer.toString(clientSocketLocal.getPort())
+                                : "unknown";
                         logger.warn(
                                 "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
                                 pulseaudioHandler.getHost(), port, e.getMessage());
index 04bc37636c28ea7532aed0bef7d7f541c51359ba..5369f600114c938749e84d9cd64af12e776560e0 100644 (file)
@@ -107,7 +107,9 @@ public class PulseAudioAudioSource extends PulseaudioSimpleProtocolStream implem
                 } catch (IOException e) {
                     disconnect(); // disconnect to force 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";
+                        final Socket clientSocketLocal = clientSocket;
+                        String port = clientSocketLocal != null ? Integer.toString(clientSocketLocal.getPort())
+                                : "unknown";
                         logger.warn(
                                 "Error while trying to get audio from pulseaudio audio source. Cannot connect to {}:{}, error: {}",
                                 pulseaudioHandler.getHost(), port, e.getMessage());
@@ -153,11 +155,14 @@ public class PulseAudioAudioSource extends PulseaudioSimpleProtocolStream implem
                                     if (pipeOutputs.contains(output)) {
                                         output.flush();
                                     }
-                                } catch (IOException e) {
-                                    if (e instanceof InterruptedIOException && pipeOutputs.isEmpty()) {
+                                } catch (InterruptedIOException e) {
+                                    if (pipeOutputs.isEmpty()) {
                                         // task has been ended while writing
                                         return;
                                     }
+                                    logger.warn("InterruptedIOException while writing to from pulse source pipe: {}",
+                                            getExceptionMessage(e));
+                                } catch (IOException e) {
                                     logger.warn("IOException while writing to from pulse source pipe: {}",
                                             getExceptionMessage(e));
                                 } catch (RuntimeException e) {
@@ -221,7 +226,8 @@ public class PulseAudioAudioSource extends PulseaudioSimpleProtocolStream implem
         } catch (IOException | InterruptedException ignored) {
         }
         try {
-            return (clientSocket != null) ? clientSocket.getInputStream() : null;
+            var clientSocketFinal = clientSocket;
+            return (clientSocketFinal != null) ? clientSocketFinal.getInputStream() : null;
         } catch (IOException ignored) {
             return null;
         }
@@ -264,11 +270,14 @@ public class PulseAudioAudioSource extends PulseaudioSimpleProtocolStream implem
 
         @Override
         public int read(byte @Nullable [] b) throws IOException {
-            return read(b, 0, b.length);
+            return read(b, 0, b == null ? 0 : b.length);
         }
 
         @Override
         public int read(byte @Nullable [] b, int off, int len) throws IOException {
+            if (b == null) {
+                throw new IOException("Buffer is null");
+            }
             logger.trace("reading from pulseaudio stream");
             if (closed) {
                 throw new IOException("Stream is closed");
index 4f7d6dc2c2be8fbb894f11734a74425a622d2d96..4cf99f0d4f17454a6e03a67c855d27c8ec3d5d91 100644 (file)
@@ -27,7 +27,6 @@ import java.util.List;
 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;
@@ -150,7 +149,6 @@ public class PulseaudioClient {
         modules = new ArrayList<Module>(Parser.parseModules(listModules()));
 
         List<AbstractAudioDeviceConfig> newItems = new ArrayList<>(); // prepare new list before assigning it
-        newItems.clear();
         if (configuration.sink) {
             logger.debug("reading sinks");
             newItems.addAll(Parser.parseSinks(listSinks(), this));
@@ -245,48 +243,6 @@ public class PulseaudioClient {
         return null;
     }
 
-    /**
-     * retrieves a {@link SinkInput} by its name
-     *
-     * @return the corresponding {@link SinkInput} to the given <code>name</code>
-     */
-    public @Nullable SinkInput getSinkInput(String name) {
-        for (AbstractAudioDeviceConfig item : items) {
-            if (item.getPaName().equalsIgnoreCase(name) && item instanceof SinkInput) {
-                return (SinkInput) item;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * retrieves a {@link SinkInput} by its id
-     *
-     * @return the corresponding {@link SinkInput} to the given <code>id</code>
-     */
-    public @Nullable SinkInput getSinkInput(int id) {
-        for (AbstractAudioDeviceConfig item : items) {
-            if (item.getId() == id && item instanceof SinkInput) {
-                return (SinkInput) item;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * retrieves a {@link Source} by its name
-     *
-     * @return the corresponding {@link Source} to the given <code>name</code>
-     */
-    public @Nullable Source getSource(String name) {
-        for (AbstractAudioDeviceConfig item : items) {
-            if (item.getPaName().equalsIgnoreCase(name) && item instanceof Source) {
-                return (Source) item;
-            }
-        }
-        return null;
-    }
-
     /**
      * retrieves a {@link Source} by its id
      *
@@ -301,34 +257,6 @@ public class PulseaudioClient {
         return null;
     }
 
-    /**
-     * retrieves a {@link SourceOutput} by its name
-     *
-     * @return the corresponding {@link SourceOutput} to the given <code>name</code>
-     */
-    public @Nullable SourceOutput getSourceOutput(String name) {
-        for (AbstractAudioDeviceConfig item : items) {
-            if (item.getPaName().equalsIgnoreCase(name) && item instanceof SourceOutput) {
-                return (SourceOutput) item;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * retrieves a {@link SourceOutput} by its id
-     *
-     * @return the corresponding {@link SourceOutput} to the given <code>id</code>
-     */
-    public @Nullable SourceOutput getSourceOutput(int id) {
-        for (AbstractAudioDeviceConfig item : items) {
-            if (item.getId() == id && item instanceof SourceOutput) {
-                return (SourceOutput) item;
-            }
-        }
-        return null;
-    }
-
     /**
      * retrieves a {@link AbstractAudioDeviceConfig} by its name
      *
@@ -343,6 +271,11 @@ public class PulseaudioClient {
         return null;
     }
 
+    /**
+     * Get all items previously parsed from the pulseaudio server.
+     *
+     * @return All items parsed from the pulseaudio server
+     */
     public List<AbstractAudioDeviceConfig> getItems() {
         return items;
     }
@@ -479,16 +412,18 @@ public class PulseaudioClient {
                 .map(portS -> Integer.parseInt(portS));
     }
 
-    private Optional<@NonNull String> extractArgumentFromLine(String argumentWanted, String argumentLine) {
+    private Optional<String> extractArgumentFromLine(String argumentWanted, @Nullable String argumentLine) {
         String argument = null;
-        int startPortIndex = argumentLine.indexOf(argumentWanted + "=");
-        if (startPortIndex != -1) {
-            startPortIndex = startPortIndex + argumentWanted.length() + 1;
-            int endPortIndex = argumentLine.indexOf(" ", startPortIndex);
-            if (endPortIndex == -1) {
-                endPortIndex = argumentLine.length();
+        if (argumentLine != null) {
+            int startPortIndex = argumentLine.indexOf(argumentWanted + "=");
+            if (startPortIndex != -1) {
+                startPortIndex = startPortIndex + argumentWanted.length() + 1;
+                int endPortIndex = argumentLine.indexOf(" ", startPortIndex);
+                if (endPortIndex == -1) {
+                    endPortIndex = argumentLine.length();
+                }
+                argument = argumentLine.substring(startPortIndex, endPortIndex);
             }
-            argument = argumentLine.substring(startPortIndex, endPortIndex);
         }
         return Optional.ofNullable(argument);
     }
@@ -552,7 +487,10 @@ public class PulseaudioClient {
             slaves.add(sink.getPaName());
         }
         // 1. delete old combined-sink
-        sendRawCommand(CMD_UNLOAD_MODULE + " " + combinedSink.getModule().getId());
+        Module lastModule = combinedSink.getModule();
+        if (lastModule != null) {
+            sendRawCommand(CMD_UNLOAD_MODULE + " " + lastModule.getId());
+        }
         // 2. add new combined-sink with same name and all slaves
         sendRawCommand(CMD_LOAD_MODULE + " " + MODULE_COMBINE_SINK + " sink_name=" + combinedSink.getPaName()
                 + " slaves=" + String.join(",", slaves));
@@ -731,8 +669,9 @@ public class PulseaudioClient {
         if (clientSocket == null || clientSocket.isClosed() || !clientSocket.isConnected()) {
             logger.trace("Try to connect...");
             try {
-                client = new Socket(host, port);
-                client.setSoTimeout(500);
+                var clientFinal = new Socket(host, port);
+                clientFinal.setSoTimeout(500);
+                client = clientFinal;
                 logger.trace("connected");
             } catch (UnknownHostException e) {
                 client = null;
index 59943dc790a1e5cbde215f0e1b07f417d1e797d3..eb91a1dd4275d3c90409f1db9e68770e18b75c52 100644 (file)
@@ -20,6 +20,8 @@ import java.util.Set;
 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.discovery.PulseaudioDeviceDiscoveryService;
 import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler;
 import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
@@ -47,6 +49,7 @@ import org.slf4j.LoggerFactory;
  * @author Tobias Bräutigam - Initial contribution
  */
 @Component(service = ThingHandlerFactory.class, configurationPid = "binding.pulseaudio")
+@NonNullByDefault
 public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
     private final Logger logger = LoggerFactory.getLogger(PulseaudioHandlerFactory.class);
 
@@ -64,8 +67,8 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
     }
 
     @Override
-    public Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration, ThingUID thingUID,
-            ThingUID bridgeUID) {
+    public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration,
+            @Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) {
         if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
             return super.createThing(thingTypeUID, configuration, thingUID, null);
         }
@@ -83,11 +86,11 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
                 bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
     }
 
-    private ThingUID getPulseaudioDeviceUID(ThingTypeUID thingTypeUID, ThingUID thingUID, Configuration configuration,
-            ThingUID bridgeUID) {
+    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);
-            return new ThingUID(thingTypeUID, name, bridgeUID.getId());
+            return new ThingUID(thingTypeUID, name, bridgeUID == null ? null : bridgeUID.getId());
         }
         return thingUID;
     }
@@ -106,8 +109,7 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
     }
 
     @Override
-    protected ThingHandler createHandler(Thing thing) {
-
+    protected @Nullable ThingHandler createHandler(Thing thing) {
         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
 
         if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
index 276ec38f7b2de6f30490bdf7957d74e470fbb328..98c8bfb3d78dcbd68f0d6d00ede98b8db4b4c35a 100644 (file)
@@ -63,9 +63,10 @@ public abstract class PulseaudioSimpleProtocolStream {
         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());
+            int port = pulseaudioHandler.getSimpleTcpPortAndLoadModuleIfNecessary();
+            var clientSocketFinal = new Socket(host, port);
+            clientSocketFinal.setSoTimeout(pulseaudioHandler.getBasicProtocolSOTimeout());
+            clientSocket = clientSocketFinal;
         }
     }
 
@@ -86,8 +87,9 @@ public abstract class PulseaudioSimpleProtocolStream {
     }
 
     public void scheduleDisconnect() {
-        if (scheduledDisconnection != null) {
-            scheduledDisconnection.cancel(true);
+        var scheduledDisconnectionFinal = scheduledDisconnection;
+        if (scheduledDisconnectionFinal != null) {
+            scheduledDisconnectionFinal.cancel(true);
         }
         int idleTimeout = pulseaudioHandler.getIdleTimeout();
         if (idleTimeout > -1) {
index 8dfb2f7787e1466d93bccd2b70ea55f8e129a2bc..47742c1796eed1c577003785133527e877fff9c7 100644 (file)
@@ -19,6 +19,8 @@ import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.pulseaudio.internal.PulseaudioClient;
 import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
 import org.openhab.binding.pulseaudio.internal.items.Module;
@@ -34,6 +36,7 @@ import org.slf4j.LoggerFactory;
  *
  * @author Tobias Bräutigam - Initial contribution
  */
+@NonNullByDefault
 public class Parser {
     private static final Logger LOGGER = LoggerFactory.getLogger(Parser.class);
 
@@ -143,10 +146,8 @@ public class Parser {
                 if (properties.containsKey("combine.slaves")) {
                     // this is a combined sink, the combined sink object should be
                     String sinkNames = properties.get("combine.slaves");
-                    if (sinkNames != null) {
-                        for (String sinkName : sinkNames.replace("\"", "").split(",")) {
-                            sink.addCombinedSinkName(sinkName);
-                        }
+                    for (String sinkName : sinkNames.replace("\"", "").split(",")) {
+                        sink.addCombinedSinkName(sinkName);
                     }
                     combinedSinks.add(sink);
                 }
@@ -270,8 +271,8 @@ public class Parser {
                 if (properties.containsKey("volume")) {
                     source.setVolume(parseVolume(properties.get("volume")));
                 }
-                String monitorOf = properties.get("monitor_of");
-                if (monitorOf != null) {
+                if (properties.containsKey("monitor_of")) {
+                    String monitorOf = properties.get("monitor_of");
                     source.setMonitorOf(client.getSink(Integer.valueOf(monitorOf)));
                 }
                 sources.add(source);
@@ -373,7 +374,7 @@ public class Parser {
      * @param raw
      * @return
      */
-    private static int getNumberValue(String raw) {
+    private static int getNumberValue(@Nullable String raw) {
         int id = -1;
         if (raw == null) {
             return 0;
index e39a56eba0874dece7738ae9d3ea65e0c7693e9d..b36e53472461d4d9a01fc86f0be6e5d421da4234 100644 (file)
@@ -15,7 +15,9 @@ package org.openhab.binding.pulseaudio.internal.discovery;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
 import org.openhab.binding.pulseaudio.internal.handler.DeviceStatusListener;
 import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler;
@@ -28,7 +30,7 @@ import org.openhab.binding.pulseaudio.internal.items.SourceOutput;
 import org.openhab.core.config.discovery.AbstractDiscoveryService;
 import org.openhab.core.config.discovery.DiscoveryResult;
 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
-import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingTypeUID;
 import org.openhab.core.thing.ThingUID;
 import org.slf4j.Logger;
@@ -40,6 +42,7 @@ import org.slf4j.LoggerFactory;
  *
  * @author Tobias Bräutigam - Initial contribution
  */
+@NonNullByDefault
 public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService implements DeviceStatusListener {
 
     private final Logger logger = LoggerFactory.getLogger(PulseaudioDeviceDiscoveryService.class);
@@ -66,7 +69,11 @@ public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService i
     }
 
     @Override
-    public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) {
+    public void onDeviceAdded(Thing bridge, AbstractAudioDeviceConfig device) {
+        if (getAlreadyConfiguredThings().contains(device.getPaName())) {
+            return;
+        }
+
         String uidName = device.getPaName();
         logger.debug("device {} found", device);
         ThingTypeUID thingType = null;
@@ -97,18 +104,14 @@ public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService i
         }
     }
 
-    @Override
-    protected void startScan() {
-        // this can be ignored here as we discover via the PulseaudioClient.update() mechanism
-    }
-
-    @Override
-    public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) {
-        // this can be ignored here
+    public Set<String> getAlreadyConfiguredThings() {
+        return pulseaudioBridgeHandler.getThing().getThings().stream().map(Thing::getConfiguration)
+                .map(conf -> (String) conf.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME))
+                .collect(Collectors.toSet());
     }
 
     @Override
-    public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) {
-        // this can be ignored here
+    protected void startScan() {
+        pulseaudioBridgeHandler.resetKnownActiveDevices();
     }
 }
index e76c996fdaf6a4a93125262b8cd85db6ff91e68a..3d2aa3b51ab48a13ab41deb8e40cbe1a42faddd3 100644 (file)
@@ -20,6 +20,8 @@ import java.util.Set;
 
 import javax.jmdns.ServiceInfo;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
 import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler;
 import org.openhab.core.config.discovery.DiscoveryResult;
@@ -38,6 +40,7 @@ import org.slf4j.LoggerFactory;
  * @author Tobias Bräutigam - Initial contribution
  */
 @Component
+@NonNullByDefault
 public class PulseaudioDiscoveryParticipant implements MDNSDiscoveryParticipant {
 
     private final Logger logger = LoggerFactory.getLogger(PulseaudioDiscoveryParticipant.class);
@@ -48,7 +51,7 @@ public class PulseaudioDiscoveryParticipant implements MDNSDiscoveryParticipant
     }
 
     @Override
-    public DiscoveryResult createResult(ServiceInfo info) {
+    public @Nullable DiscoveryResult createResult(ServiceInfo info) {
         DiscoveryResult result = null;
         ThingUID uid = getThingUID(info);
         if (uid != null) {
@@ -79,15 +82,12 @@ public class PulseaudioDiscoveryParticipant implements MDNSDiscoveryParticipant
     }
 
     @Override
-    public ThingUID getThingUID(ServiceInfo info) {
-        if (info != null) {
-            logger.debug("ServiceInfo: {}", info);
-            if (info.getType() != null) {
-                if (info.getType().equals(getServiceType())) {
-                    logger.trace("Discovered a pulseaudio server thing with name '{}'", info.getName());
-                    return new ThingUID(PulseaudioBindingConstants.BRIDGE_THING_TYPE,
-                            info.getName().replace("@", "_AT_"));
-                }
+    public @Nullable ThingUID getThingUID(ServiceInfo info) {
+        logger.debug("ServiceInfo: {}", info);
+        if (info.getType() != null) {
+            if (info.getType().equals(getServiceType())) {
+                logger.trace("Discovered a pulseaudio server thing with name '{}'", info.getName());
+                return new ThingUID(PulseaudioBindingConstants.BRIDGE_THING_TYPE, info.getName().replace("@", "_AT_"));
             }
         }
         return null;
index 4df412417ec9fe466185407018d6a7272e106ef7..bd5e23642e87e9549d12971cc1b6151c3e926548 100644 (file)
@@ -14,8 +14,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;
+import org.openhab.core.thing.Thing;
 
 /**
  * The {@link DeviceStatusListener} is notified when a device status has changed
@@ -26,28 +25,11 @@ import org.openhab.core.thing.ThingUID;
  */
 @NonNullByDefault
 public interface DeviceStatusListener {
-
-    /**
-     * This method is called whenever the state of the given device has changed.
-     *
-     * @param bridge The Pulseaudio bridge the changed device is connected to.
-     * @param device The device which received the state update.
-     */
-    public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device);
-
-    /**
-     * This method us called whenever a device is removed.
-     *
-     * @param bridge The Pulseaudio bridge the removed device was connected to.
-     * @param device The device which is removed.
-     */
-    public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device);
-
     /**
      * This method us called whenever a device is added.
      *
      * @param bridge The Pulseaudio bridge the added device was connected to.
      * @param device The device which is added.
      */
-    public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device);
+    public void onDeviceAdded(Thing bridge, AbstractAudioDeviceConfig device);
 }
index 133669d743de86cc8f1b4d9fdc080a61a473ab30..d6e0bacafa2e97c72b617193ed4984ebee400480 100644 (file)
@@ -24,6 +24,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
 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.PulseAudioBindingConfiguration;
 import org.openhab.binding.pulseaudio.internal.PulseAudioBindingConfigurationListener;
@@ -33,10 +34,12 @@ import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
 import org.openhab.core.config.core.Configuration;
 import org.openhab.core.thing.Bridge;
 import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.ThingTypeUID;
 import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandler;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.RefreshType;
 import org.slf4j.Logger;
@@ -47,8 +50,10 @@ import org.slf4j.LoggerFactory;
  * connects it to the framework.
  *
  * @author Tobias Bräutigam - Initial contribution
+ * @author Gwendal Roulleau - Rewrite for child handler notification
  *
  */
+@NonNullByDefault
 public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseAudioBindingConfigurationListener {
     private final Logger logger = LoggerFactory.getLogger(PulseaudioBridgeHandler.class);
 
@@ -60,22 +65,22 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
 
     public int refreshInterval = 30000;
 
+    @Nullable
     private PulseaudioClient client;
 
     private PulseAudioBindingConfiguration configuration;
 
     private List<DeviceStatusListener> deviceStatusListeners = new CopyOnWriteArrayList<>();
-    private HashSet<String> lastActiveDevices = new HashSet<>();
+    private Set<String> lastActiveDevices = new HashSet<>();
 
+    @Nullable
     private ScheduledFuture<?> pollingJob;
 
-    private synchronized void update() {
+    private Set<PulseaudioHandler> childHandlersInitialized = new HashSet<>();
+
+    public synchronized void update() {
         try {
-            client.connect();
-            if (getThing().getStatus() != ThingStatus.ONLINE) {
-                updateStatus(ThingStatus.ONLINE);
-                logger.debug("Established connection to Pulseaudio server on Host '{}':'{}'.", host, port);
-            }
+            getClient().connect();
         } catch (IOException e) {
             logger.debug("{}", e.getMessage(), e);
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
@@ -84,21 +89,23 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
             return;
         }
 
-        client.update();
-        for (AbstractAudioDeviceConfig device : client.getItems()) {
-            if (lastActiveDevices != null && lastActiveDevices.contains(device.getPaName())) {
-                for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
-                    try {
-                        deviceStatusListener.onDeviceStateChanged(getThing().getUID(), device);
-                    } catch (Exception e) {
-                        logger.warn("An exception occurred while calling the DeviceStatusListener", e);
-                    }
-                }
-            } else {
+        getClient().update();
+        if (getThing().getStatus() != ThingStatus.ONLINE) {
+            updateStatus(ThingStatus.ONLINE);
+            logger.debug("Established connection to Pulseaudio server on Host '{}':'{}'.", host, port);
+            // The framework will automatically notify the child handlers as the bridge status is changed
+        } 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()));
+            }
+        }
+        // browse query result to notify add event
+        for (AbstractAudioDeviceConfig device : getClient().getItems()) {
+            if (!lastActiveDevices.contains(device.getPaName())) {
                 for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
                     try {
                         deviceStatusListener.onDeviceAdded(getThing(), device);
-                        deviceStatusListener.onDeviceStateChanged(getThing().getUID(), device);
                     } catch (Exception e) {
                         logger.warn("An exception occurred while calling the DeviceStatusListener", e);
                     }
@@ -116,18 +123,22 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
         if (command instanceof RefreshType) {
-            client.update();
+            getClient().update();
         } else {
             logger.debug("received unexpected command for pulseaudio bridge '{}'.", host);
         }
     }
 
     public @Nullable AbstractAudioDeviceConfig getDevice(String name) {
-        return client.getGenericAudioItem(name);
+        return getClient().getGenericAudioItem(name);
     }
 
     public PulseaudioClient getClient() {
-        return client;
+        PulseaudioClient clientFinal = client;
+        if (clientFinal == null) {
+            throw new AssertionError("PulseaudioClient is null !");
+        }
+        return clientFinal;
     }
 
     @Override
@@ -145,10 +156,11 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
             this.refreshInterval = ((BigDecimal) conf.get(BRIDGE_PARAMETER_REFRESH_INTERVAL)).intValue();
         }
 
-        if (host != null && !host.isEmpty()) {
+        if (!host.isBlank()) {
             client = new PulseaudioClient(host, port, configuration);
             updateStatus(ThingStatus.UNKNOWN);
-            if (pollingJob == null || pollingJob.isCancelled()) {
+            final ScheduledFuture<?> pollingJobFinal = pollingJob;
+            if (pollingJobFinal == null || pollingJobFinal.isCancelled()) {
                 pollingJob = scheduler.scheduleWithFixedDelay(this::update, 0, refreshInterval, TimeUnit.MILLISECONDS);
             }
         } else {
@@ -168,16 +180,14 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
             job.cancel(true);
             pollingJob = null;
         }
-        if (client != null) {
-            client.disconnect();
+        var clientFinal = client;
+        if (clientFinal != null) {
+            clientFinal.disconnect();
         }
         super.dispose();
     }
 
     public boolean registerDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
-        if (deviceStatusListener == null) {
-            throw new IllegalArgumentException("It's not allowed to pass a null deviceStatusListener.");
-        }
         return deviceStatusListeners.add(deviceStatusListener);
     }
 
@@ -187,6 +197,33 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
 
     @Override
     public void bindingConfigurationChanged() {
-        update();
+        // If the bridge thing is not well setup, we do nothing
+        if (getThing().getStatus() != ThingStatus.OFFLINE
+                || getThing().getStatusInfo().getStatusDetail() != ThingStatusDetail.CONFIGURATION_ERROR) {
+            update();
+        }
+    }
+
+    public void resetKnownActiveDevices() {
+        // If the bridge thing is not well setup, we do nothing
+        if (getThing().getStatus() != ThingStatus.OFFLINE
+                || getThing().getStatusInfo().getStatusDetail() != ThingStatusDetail.CONFIGURATION_ERROR) {
+            lastActiveDevices = new HashSet<>();
+            update();
+        }
+    }
+
+    @Override
+    public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
+        if (childHandler instanceof PulseaudioHandler) {
+            this.childHandlersInitialized.add((PulseaudioHandler) childHandler);
+        } else {
+            logger.error("This bridge can only support PulseaudioHandler child");
+        }
+    }
+
+    @Override
+    public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
+        this.childHandlersInitialized.remove(childHandler);
     }
 }
index a357b6631886d0c64ab7c9efc5d98e5a8f20165d..b6821bd5ffa394a94f749fc59c7317f53a327544 100644 (file)
@@ -21,10 +21,9 @@ import java.util.Collections;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -53,7 +52,6 @@ import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.ThingStatusInfo;
 import org.openhab.core.thing.ThingTypeUID;
-import org.openhab.core.thing.ThingUID;
 import org.openhab.core.thing.binding.BaseThingHandler;
 import org.openhab.core.thing.binding.ThingHandler;
 import org.openhab.core.types.Command;
@@ -73,17 +71,14 @@ import org.slf4j.LoggerFactory;
  * @author Miguel Álvarez - Register audio source and refactor
  */
 @NonNullByDefault
-public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusListener {
+public class PulseaudioHandler extends BaseThingHandler {
 
     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 final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class);
-    private final int refresh = 60; // refresh every minute as default
 
-    private @Nullable PulseaudioBridgeHandler bridgeHandler;
-    private @Nullable String name;
-    private @Nullable ScheduledFuture<?> refreshJob;
+    private String name = "";
     private @Nullable PulseAudioAudioSink audioSink;
     private @Nullable PulseAudioAudioSource audioSource;
     private @Nullable Integer savedVolume;
@@ -102,37 +97,32 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
     public void initialize() {
         Configuration config = getThing().getConfiguration();
         name = (String) config.get(DEVICE_PARAMETER_NAME);
+        initializeWithTheBridge();
+    }
 
-        updateStatus(ThingStatus.UNKNOWN);
-        deviceOnlineWatchdog();
-
-        // if it's a SINK thing, then maybe we have to activate the audio sink
-        if (SINK_THING_TYPE.equals(thing.getThingTypeUID())) {
-            // check the property to see if we it's enabled :
-            Boolean sinkActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION);
-            if (sinkActivated != null && sinkActivated) {
-                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();
-            }
-        }
+    public String getName() {
+        return name;
     }
 
     private void audioSinkSetup() {
+        if (audioSink != null) {
+            // Audio sink is already setup
+            return;
+        }
+        if (!SINK_THING_TYPE.equals(thing.getThingTypeUID())) {
+            return;
+        }
+        // check the property to see if it's enabled :
+        Boolean sinkActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION);
+        if (sinkActivated == null || !sinkActivated.booleanValue()) {
+            return;
+        }
         final PulseaudioHandler thisHandler = this;
+        PulseAudioAudioSink audioSink = new PulseAudioAudioSink(thisHandler, scheduler);
         scheduler.submit(new Runnable() {
             @Override
             public void run() {
-                // Register the sink as an audio sink in openhab
-                logger.trace("Registering an audio sink for pulse audio sink thing {}", thing.getUID());
-                PulseAudioAudioSink audioSink = new PulseAudioAudioSink(thisHandler, scheduler);
-                setAudioSink(audioSink);
+                PulseaudioHandler.this.audioSink = audioSink;
                 try {
                     audioSink.connectIfNeeded();
                 } catch (IOException e) {
@@ -144,23 +134,49 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
                 } finally {
                     audioSink.scheduleDisconnect();
                 }
-                @SuppressWarnings("unchecked")
-                ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
-                        .registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
-                audioSinkRegistrations.put(thing.getUID().toString(), reg);
             }
         });
+        // Register the sink as an audio sink in openhab
+        logger.trace("Registering an audio sink for pulse audio sink thing {}", thing.getUID());
+        @SuppressWarnings("unchecked")
+        ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
+                .registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
+        audioSinkRegistrations.put(thing.getUID().toString(), reg);
+    }
+
+    private void audioSinkUnsetup() {
+        PulseAudioAudioSink sink = audioSink;
+        if (sink != null) {
+            sink.disconnect();
+            audioSink = null;
+        }
+        // Unregister the potential pulse audio sink's audio sink
+        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());
+            sinkReg.unregister();
+        }
     }
 
     private void audioSourceSetup() {
+        if (audioSource != null) {
+            // Audio source is already setup
+            return;
+        }
+        if (!SOURCE_THING_TYPE.equals(thing.getThingTypeUID())) {
+            return;
+        }
+        // check the property to see if it's enabled :
+        Boolean sourceActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_ACTIVATION);
+        if (sourceActivated == null || !sourceActivated.booleanValue()) {
+            return;
+        }
         final PulseaudioHandler thisHandler = this;
+        PulseAudioAudioSource audioSource = new PulseAudioAudioSource(thisHandler, scheduler);
         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);
+                PulseaudioHandler.this.audioSource = audioSource;
                 try {
                     audioSource.connectIfNeeded();
                 } catch (IOException e) {
@@ -172,41 +188,21 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
                 } finally {
                     audioSource.scheduleDisconnect();
                 }
-                @SuppressWarnings("unchecked")
-                ServiceRegistration<AudioSource> reg = (ServiceRegistration<AudioSource>) bundleContext
-                        .registerService(AudioSource.class.getName(), audioSource, new Hashtable<>());
-                audioSourceRegistrations.put(thing.getUID().toString(), reg);
             }
         });
+        // Register the source as an audio source in openhab
+        logger.trace("Registering an audio source for pulse audio source thing {}", thing.getUID());
+        @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() {
-        ScheduledFuture<?> job = refreshJob;
-        if (job != null && !job.isCancelled()) {
-            job.cancel(true);
-            refreshJob = null;
-        }
-        PulseaudioBridgeHandler briHandler = bridgeHandler;
-        if (briHandler != null) {
-            briHandler.unregisterDeviceStatusListener(this);
-            bridgeHandler = null;
-        }
-        logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
-        super.dispose();
-        PulseAudioAudioSink sink = audioSink;
-        if (sink != null) {
-            sink.disconnect();
-        }
+    private void audioSourceUnsetup() {
         PulseAudioAudioSource source = audioSource;
         if (source != null) {
             source.disconnect();
-        }
-        // Unregister the potential pulse audio sink's audio sink
-        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());
-            sinkReg.unregister();
+            audioSource = null;
         }
         // Unregister the potential pulse audio source's audio sources
         ServiceRegistration<AudioSource> sourceReg = audioSourceRegistrations.remove(getThing().getUID().toString());
@@ -217,68 +213,42 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
     }
 
     @Override
-    public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
-        if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
-                && getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE) {
-            // Bridge is now ONLINE, restart the refresh job to get an update of the thing status without waiting
-            // its next planned run
-            ScheduledFuture<?> job = refreshJob;
-            if (job != null && !job.isCancelled()) {
-                job.cancel(true);
-                refreshJob = null;
-            }
-            deviceOnlineWatchdog();
-        } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE
-                || bridgeStatusInfo.getStatus() == ThingStatus.UNKNOWN) {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
-        }
+    public void dispose() {
+        logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
+        super.dispose();
+        audioSinkUnsetup();
+        audioSourceUnsetup();
     }
 
-    private void deviceOnlineWatchdog() {
-        Runnable runnable = () -> {
-            try {
-                PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler();
-                if (bridgeHandler != null) {
-                    if (bridgeHandler.getThing().getStatus() == ThingStatus.ONLINE) {
-                        if (bridgeHandler.getDevice(name) == null) {
-                            updateStatus(ThingStatus.OFFLINE);
-                            this.bridgeHandler = null;
-                        } else {
-                            updateStatus(ThingStatus.ONLINE);
-                        }
-                    } else {
-                        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
-                    }
-                } else {
-                    logger.debug("Bridge for pulseaudio device {} not found.", name);
-                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
-                }
-            } catch (Exception e) {
-                logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
-                this.bridgeHandler = null;
-            }
-        };
+    @Override
+    public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
+        initializeWithTheBridge();
+    }
 
-        refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refresh, TimeUnit.SECONDS);
+    private void initializeWithTheBridge() {
+        PulseaudioBridgeHandler pulseaudioBridgeHandler = getPulseaudioBridgeHandler();
+        if (pulseaudioBridgeHandler == null) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
+        } else if (pulseaudioBridgeHandler.getThing().getStatus() != ThingStatus.ONLINE) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+        } else {
+            deviceUpdate(pulseaudioBridgeHandler.getDevice(name));
+        }
     }
 
     private synchronized @Nullable PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
-        if (this.bridgeHandler == null) {
-            Bridge bridge = getBridge();
-            if (bridge == null) {
-                logger.debug("Required bridge not defined for device {}.", name);
-                return null;
-            }
-            ThingHandler handler = bridge.getHandler();
-            if (handler instanceof PulseaudioBridgeHandler) {
-                this.bridgeHandler = (PulseaudioBridgeHandler) handler;
-                this.bridgeHandler.registerDeviceStatusListener(this);
-            } else {
-                logger.debug("No available bridge handler found for device {} bridge {} .", name, bridge.getUID());
-                return null;
-            }
+        Bridge bridge = getBridge();
+        if (bridge == null) {
+            logger.debug("Required bridge not defined for device {}.", name);
+            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());
+            return null;
         }
-        return this.bridgeHandler;
     }
 
     @Override
@@ -296,8 +266,7 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
         AbstractAudioDeviceConfig device = briHandler.getDevice(name);
         if (device == null) {
             logger.warn("device {} not found", name);
-            updateStatus(ThingStatus.OFFLINE);
-            bridgeHandler = null;
+            deviceUpdate(null);
             return;
         } else {
             State updateState = UnDefType.UNDEF;
@@ -384,19 +353,20 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
      *
      * @return
      */
-    public int getLastVolume() {
-        if (savedVolume == null) {
+    public Integer getLastVolume() {
+        Integer savedVolumeFinal = savedVolume;
+        if (savedVolumeFinal == null) {
             PulseaudioBridgeHandler briHandler = getPulseaudioBridgeHandler();
             if (briHandler != null) {
                 // refresh to get the current volume level
                 briHandler.getClient().update();
                 AbstractAudioDeviceConfig device = briHandler.getDevice(name);
                 if (device != null) {
-                    savedVolume = device.getVolume();
+                    savedVolume = savedVolumeFinal = device.getVolume();
                 }
             }
         }
-        return savedVolume == null ? 50 : savedVolume;
+        return savedVolumeFinal == null ? 50 : savedVolumeFinal;
     }
 
     public void setVolume(int volume) {
@@ -415,25 +385,38 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
         savedVolume = volume;
     }
 
-    @Override
-    public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) {
-        if (device.getPaName().equals(name)) {
-            updateStatus(ThingStatus.ONLINE);
+    public void deviceUpdate(@Nullable AbstractAudioDeviceConfig device) {
+        if (device != null && device.getPaName().equals(name)) {
+            updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
             logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL);
-            savedVolume = device.getVolume();
-            updateState(VOLUME_CHANNEL, new PercentType(savedVolume));
-            updateState(MUTE_CHANNEL, device.isMuted() ? OnOffType.ON : OnOffType.OFF);
-            updateState(STATE_CHANNEL,
-                    device.getState() != null ? new StringType(device.getState().toString()) : new StringType("-"));
+            int actualVolume = device.getVolume();
+            savedVolume = actualVolume;
+            updateState(VOLUME_CHANNEL, new PercentType(actualVolume));
+            updateState(MUTE_CHANNEL, OnOffType.from(device.isMuted()));
+            org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig.State state = device.getState();
+            updateState(STATE_CHANNEL, state != null ? new StringType(state.toString()) : new StringType("-"));
             if (device instanceof SinkInput) {
-                updateState(ROUTE_TO_SINK_CHANNEL,
-                        ((SinkInput) device).getSink() != null
-                                ? new StringType(((SinkInput) device).getSink().getPaName())
-                                : new StringType("-"));
+                updateState(ROUTE_TO_SINK_CHANNEL, new StringType(
+                        Optional.ofNullable(((SinkInput) device).getSink()).map(Sink::getPaName).orElse("-")));
             }
             if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
                 updateState(SLAVES_CHANNEL, new StringType(String.join(",", ((Sink) device).getCombinedSinkNames())));
             }
+            audioSinkSetup();
+            audioSourceSetup();
+        } else if (device == null) {
+            updateState(VOLUME_CHANNEL, UnDefType.UNDEF);
+            updateState(MUTE_CHANNEL, UnDefType.UNDEF);
+            updateState(STATE_CHANNEL, UnDefType.UNDEF);
+            if (SINK_INPUT_THING_TYPE.equals(thing.getThingTypeUID())) {
+                updateState(ROUTE_TO_SINK_CHANNEL, UnDefType.UNDEF);
+            }
+            if (COMBINED_SINK_THING_TYPE.equals(thing.getThingTypeUID())) {
+                updateState(SLAVES_CHANNEL, UnDefType.UNDEF);
+            }
+            audioSinkUnsetup();
+            audioSourceUnsetup();
+            updateStatus(ThingStatus.OFFLINE);
         }
     }
 
@@ -448,14 +431,14 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
     }
 
     /**
-     * This method will scan the pulseaudio server to find the port on which the module/sink is listening
+     * This method will scan the pulseaudio server to find the port on which the module/sink/source is listening
      * If no module is listening, then it will command the module to load on the pulse audio server,
      *
-     * @return the port on which the pulseaudio server is listening for this sink
+     * @return the port on which the pulseaudio server is listening for this sink/source
      * @throws IOException when device info is not available
      * @throws InterruptedException when interrupted during the loading module wait
      */
-    public int getSimpleTcpPort() throws IOException, InterruptedException {
+    public int getSimpleTcpPortAndLoadModuleIfNecessary() throws IOException, InterruptedException {
         var briHandler = getPulseaudioBridgeHandler();
         if (briHandler == null) {
             throw new IOException("bridge is not ready");
@@ -515,43 +498,22 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
     }
 
     public int getIdleTimeout() {
+        var idleTimeout = 3000;
         var handler = getPulseaudioBridgeHandler();
-        if (handler == null) {
-            return 30000;
+        if (handler != null) {
+            AbstractAudioDeviceConfig device = handler.getDevice(name);
+            String idleTimeoutPropName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT
+                    : DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT;
+            var idleTimeoutB = (BigDecimal) getThing().getConfiguration().get(idleTimeoutPropName);
+            if (idleTimeoutB != null) {
+                idleTimeout = idleTimeoutB.intValue();
+            }
         }
-        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;
+        return idleTimeout;
     }
 
     public int getBasicProtocolSOTimeout() {
         var soTimeout = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOCKET_SO_TIMEOUT);
         return soTimeout != null ? soTimeout.intValue() : 500;
     }
-
-    @Override
-    public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) {
-        if (device.getPaName().equals(name)) {
-            bridgeHandler.unregisterDeviceStatusListener(this);
-            bridgeHandler = null;
-            audioSink.disconnect();
-            audioSink = null;
-            updateStatus(ThingStatus.OFFLINE);
-        }
-    }
-
-    @Override
-    public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) {
-        logger.trace("new device discovered {} by {}", device, bridge);
-    }
-
-    public void setAudioSink(PulseAudioAudioSink audioSink) {
-        this.audioSink = audioSink;
-    }
-
-    public void setAudioSource(PulseAudioAudioSource audioSource) {
-        this.audioSource = audioSource;
-    }
 }
index ae1f0d03b0d59b5e1f669ab9fceaea29f279d557..68a842f2fe1367efc519c5296c2a136b6f9d1268 100644 (file)
  */
 package org.openhab.binding.pulseaudio.internal.items;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
 /**
  * GenericAudioItems are any kind of items that deal with audio data and can be
  * muted or their volume can be changed.
  *
  * @author Tobias Bräutigam - Initial contribution
  */
+@NonNullByDefault
 public abstract class AbstractAudioDeviceConfig extends AbstractDeviceConfig {
 
     public enum State {
@@ -28,25 +32,21 @@ public abstract class AbstractAudioDeviceConfig extends AbstractDeviceConfig {
         DRAINED
     }
 
-    protected State state;
+    protected @Nullable State state;
     protected boolean muted;
     protected int volume;
-    protected Module module;
+    protected @Nullable Module module;
 
-    public AbstractAudioDeviceConfig(int id, String name, Module module) {
+    public AbstractAudioDeviceConfig(int id, String name, @Nullable Module module) {
         super(id, name);
         this.module = module;
     }
 
-    public Module getModule() {
+    public @Nullable Module getModule() {
         return module;
     }
 
-    public void setModule(Module module) {
-        this.module = module;
-    }
-
-    public State getState() {
+    public @Nullable State getState() {
         return state;
     }
 
index 2539a5fbfc111ee92d197ff4396735892163a9a4..493293147ae2861b122e6ea30d67d4cb5c9ba8d2 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.pulseaudio.internal.items;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
 /**
  * Abstract root class for all items in an pulseaudio server. Every item in a
  * pulseaudio server has a name and a unique id which can be inherited by this
@@ -19,6 +21,7 @@ package org.openhab.binding.pulseaudio.internal.items;
  *
  * @author Tobias Bräutigam - Initial contribution
  */
+@NonNullByDefault
 public abstract class AbstractDeviceConfig {
 
     protected int id;
index f46ce95d2c2914444a917eeb4c59da82ad378969..cbe482136fa78865e16a923a10298cdced00bfca 100644 (file)
@@ -12,6 +12,9 @@
  */
 package org.openhab.binding.pulseaudio.internal.items;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
 /**
  * In order to add a {@link Sink} to the pulseaudio server you have to
  * load a corresponding module. Current Module objects are needed to
@@ -19,15 +22,16 @@ package org.openhab.binding.pulseaudio.internal.items;
  *
  * @author Tobias Bräutigam - Initial contribution
  */
+@NonNullByDefault
 public class Module extends AbstractDeviceConfig {
 
-    private String argument;
+    private @Nullable String argument;
 
     public Module(int id, String name) {
         super(id, name);
     }
 
-    public String getArgument() {
+    public @Nullable String getArgument() {
         return argument;
     }
 
index ce4771bc02fc1bce61204a8634cf8a772b107316..0f31498277125282cf22818a3bb540663abafd28 100644 (file)
@@ -15,6 +15,9 @@ package org.openhab.binding.pulseaudio.internal.items;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
 /**
  * On a Pulseaudio server Sinks are the devices the audio streams are routed to
  * (playback devices) it can be a single item or a group of other Sinks that are
@@ -22,12 +25,13 @@ import java.util.List;
  *
  * @author Tobias Bräutigam - Initial contribution
  */
+@NonNullByDefault
 public class Sink extends AbstractAudioDeviceConfig {
 
     protected List<String> combinedSinkNames;
     protected List<Sink> combinedSinks;
 
-    public Sink(int id, String name, Module module) {
+    public Sink(int id, String name, @Nullable Module module) {
         super(id, name, module);
         combinedSinkNames = new ArrayList<>();
         combinedSinks = new ArrayList<>();
@@ -53,7 +57,7 @@ public class Sink extends AbstractAudioDeviceConfig {
         this.combinedSinks = combinedSinks;
     }
 
-    public void addCombinedSink(Sink sink) {
+    public void addCombinedSink(@Nullable Sink sink) {
         if (sink != null) {
             this.combinedSinks.add(sink);
         }
index d138cddd88393d1a9a2010aafa39abf7723af095..fa9cb5046a98c6ec3f649c2e6f67a8a7558eb790 100644 (file)
  */
 package org.openhab.binding.pulseaudio.internal.items;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
 /**
  * A SinkInput is an audio stream which can be routed to a {@link Sink}
  *
  * @author Tobias Bräutigam - Initial contribution
  */
+@NonNullByDefault
 public class SinkInput extends AbstractAudioDeviceConfig {
 
+    @Nullable
     private Sink sink;
 
-    public SinkInput(int id, String name, Module module) {
+    public SinkInput(int id, String name, @Nullable Module module) {
         super(id, name, module);
     }
 
-    public Sink getSink() {
+    public @Nullable Sink getSink() {
         return sink;
     }
 
-    public void setSink(Sink sink) {
+    public void setSink(@Nullable Sink sink) {
         this.sink = sink;
     }
 }
index 53e4220aee032e5eeab0d0721aee616c3d4ca04c..c5162b3a51dc69e74b24eb132b8eec351e65d753 100644 (file)
  */
 package org.openhab.binding.pulseaudio.internal.items;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
 /**
  * A Source is a device which is the source of an audio stream (recording
  * device) For example microphones or line-in jacks.
  *
  * @author Tobias Bräutigam - Initial contribution
  */
+@NonNullByDefault
 public class Source extends AbstractAudioDeviceConfig {
 
+    @Nullable
     protected Sink monitorOf;
 
-    public Source(int id, String name, Module module) {
+    public Source(int id, String name, @Nullable Module module) {
         super(id, name, module);
     }
 
-    public Sink getMonitorOf() {
+    public @Nullable Sink getMonitorOf() {
         return monitorOf;
     }
 
-    public void setMonitorOf(Sink sink) {
+    public void setMonitorOf(@Nullable Sink sink) {
         this.monitorOf = sink;
     }
 }
index 60edda6f8340bf2cf10fee103454fbab3343162c..0bda3854553c5edcb6953a7c3a05f4e5b620bddc 100644 (file)
  */
 package org.openhab.binding.pulseaudio.internal.items;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
 /**
  * A SourceOutput is the audio stream which is produced by a (@link Source}
  *
  * @author Tobias Bräutigam - Initial contribution
  */
+@NonNullByDefault
 public class SourceOutput extends AbstractAudioDeviceConfig {
 
+    @Nullable
     private Source source;
 
-    public SourceOutput(int id, String name, Module module) {
+    public SourceOutput(int id, String name, @Nullable Module module) {
         super(id, name, module);
     }
 
-    public Source getSource() {
+    public @Nullable Source getSource() {
         return source;
     }
 
-    public void setSource(Source source) {
+    public void setSource(@Nullable Source source) {
         this.source = source;
     }
 }