From 2be9a658d56bb810623f3ddb9cc41bdd9efce18a Mon Sep 17 00:00:00 2001 From: Gwendal Roulleau Date: Sun, 10 Apr 2022 00:25:45 +0200 Subject: [PATCH] [pulseaudio] Small bugfixes and rewrite (#12581) * [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 Co-authored-by: Laurent Garnier --- .../internal/ConvertedInputStream.java | 74 +---- .../internal/PulseAudioAudioSink.java | 4 +- .../internal/PulseAudioAudioSource.java | 19 +- .../pulseaudio/internal/PulseaudioClient.java | 105 ++---- .../internal/PulseaudioHandlerFactory.java | 16 +- .../PulseaudioSimpleProtocolStream.java | 12 +- .../pulseaudio/internal/cli/Parser.java | 15 +- .../PulseaudioDeviceDiscoveryService.java | 27 +- .../PulseaudioDiscoveryParticipant.java | 20 +- .../handler/DeviceStatusListener.java | 22 +- .../handler/PulseaudioBridgeHandler.java | 97 ++++-- .../internal/handler/PulseaudioHandler.java | 312 ++++++++---------- .../items/AbstractAudioDeviceConfig.java | 18 +- .../internal/items/AbstractDeviceConfig.java | 3 + .../pulseaudio/internal/items/Module.java | 8 +- .../pulseaudio/internal/items/Sink.java | 8 +- .../pulseaudio/internal/items/SinkInput.java | 11 +- .../pulseaudio/internal/items/Source.java | 11 +- .../internal/items/SourceOutput.java | 11 +- 19 files changed, 344 insertions(+), 449 deletions(-) diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/ConvertedInputStream.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/ConvertedInputStream.java index 5fd5a35d4847c..6d7c7632743fc 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/ConvertedInputStream.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/ConvertedInputStream.java @@ -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 void close() throws IOException { * @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 long getDuration() { */ private AudioInputStream getPCMStream(InputStream resetableInnerInputStream) throws UnsupportedAudioFileException, IOException, UnsupportedAudioFormatException { - if (AudioFormat.MP3.isCompatible(audioFormat)) { MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader(); @@ -170,7 +168,6 @@ private AudioInputStream getPCMStream(InputStream resetableInnerInputStream) 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 @@ private AudioInputStream getPCMStream(InputStream resetableInnerInputStream) 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; - } - } } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java index 7862a6d29e071..8e42bb1c261e7 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseAudioAudioSink.java @@ -91,7 +91,9 @@ public void process(@Nullable AudioStream audioStream) } 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()); 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 index 04bc37636c28e..5369f600114c9 100644 --- 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 @@ -107,7 +107,9 @@ public void close() throws IOException { } 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 @@ private synchronized void startPipeWrite() { 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 @@ private synchronized void stopPipeWriteTask() { } 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 int read() throws IOException { @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"); diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java index 4f7d6dc2c2be8..4cf99f0d4f174 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java @@ -27,7 +27,6 @@ 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 synchronized void update() { modules = new ArrayList(Parser.parseModules(listModules())); List 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 void sendCommand(String command) { return null; } - /** - * retrieves a {@link SinkInput} by its name - * - * @return the corresponding {@link SinkInput} to the given name - */ - 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 id - */ - 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 name - */ - 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 void sendCommand(String command) { return null; } - /** - * retrieves a {@link SourceOutput} by its name - * - * @return the corresponding {@link SourceOutput} to the given name - */ - 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 id - */ - 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 void sendCommand(String command) { return null; } + /** + * Get all items previously parsed from the pulseaudio server. + * + * @return All items parsed from the pulseaudio server + */ public List getItems() { return items; } @@ -479,16 +412,18 @@ private Optional findSimpleProtocolTcpModule(AbstractAudioDeviceConfig .map(portS -> Integer.parseInt(portS)); } - private Optional<@NonNull String> extractArgumentFromLine(String argumentWanted, String argumentLine) { + private Optional 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 void setCombinedSinkSlaves(@Nullable Sink combinedSink, List sinks) 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 void connect() throws IOException { 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; diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java index 59943dc790a1e..eb91a1dd4275d 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java @@ -20,6 +20,8 @@ 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 @@ * @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 boolean supportsThingType(ThingTypeUID thingTypeUID) { } @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 @@ private void registerDeviceDiscoveryService(PulseaudioBridgeHandler paBridgeHand 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 @@ protected void removeHandler(ThingHandler thingHandler) { } @Override - protected ThingHandler createHandler(Thing thing) { - + protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { 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 index 276ec38f7b2de..98c8bfb3d78dc 100644 --- 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 @@ -63,9 +63,10 @@ public void connectIfNeeded() throws IOException, InterruptedException { 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 void disconnect() { } 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) { diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java index 8dfb2f7787e14..47742c1796eed 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java @@ -19,6 +19,8 @@ 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 @@ * * @author Tobias Bräutigam - Initial contribution */ +@NonNullByDefault public class Parser { private static final Logger LOGGER = LoggerFactory.getLogger(Parser.class); @@ -143,10 +146,8 @@ public static Collection parseSinks(String raw, PulseaudioClient client) { 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 static List parseSources(String raw, PulseaudioClient client) { 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 @@ private static int parseVolume(String vol) { * @param raw * @return */ - private static int getNumberValue(String raw) { + private static int getNumberValue(@Nullable String raw) { int id = -1; if (raw == null) { return 0; diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDeviceDiscoveryService.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDeviceDiscoveryService.java index e39a56eba0874..b36e53472461d 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDeviceDiscoveryService.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDeviceDiscoveryService.java @@ -15,7 +15,9 @@ 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.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 @@ * * @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 Set getSupportedThingTypes() { } @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 void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) { } } - @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 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(); } } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDiscoveryParticipant.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDiscoveryParticipant.java index e76c996fdaf6a..3d2aa3b51ab48 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDiscoveryParticipant.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDiscoveryParticipant.java @@ -20,6 +20,8 @@ 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 @@ * @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 Set getSupportedThingTypeUIDs() { } @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 DiscoveryResult createResult(ServiceInfo info) { } @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; diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceStatusListener.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceStatusListener.java index 4df412417ec9f..bd5e23642e87e 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceStatusListener.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceStatusListener.java @@ -14,8 +14,7 @@ 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 @@ */ @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); } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java index 133669d743de8..d6e0bacafa2e9 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java @@ -24,6 +24,7 @@ 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.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 @@ * 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 deviceStatusListeners = new CopyOnWriteArrayList<>(); - private HashSet lastActiveDevices = new HashSet<>(); + private Set lastActiveDevices = new HashSet<>(); + @Nullable private ScheduledFuture pollingJob; - private synchronized void update() { + private Set 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 @@ private synchronized void update() { 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 PulseaudioBridgeHandler(Bridge bridge, PulseAudioBindingConfiguration con @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 void initialize() { 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 void dispose() { 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 boolean unregisterDeviceStatusListener(DeviceStatusListener deviceStatusL @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); } } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java index a357b6631886d..b6821bd5ffa39 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java @@ -21,10 +21,9 @@ 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.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 @@ * @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 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 PulseaudioHandler(Thing thing, BundleContext bundleContext) { 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 void run() { } finally { audioSink.scheduleDisconnect(); } - @SuppressWarnings("unchecked") - ServiceRegistration reg = (ServiceRegistration) 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 reg = (ServiceRegistration) 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 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 void run() { } finally { audioSource.scheduleDisconnect(); } - @SuppressWarnings("unchecked") - ServiceRegistration reg = (ServiceRegistration) 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 reg = (ServiceRegistration) 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 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 sourceReg = audioSourceRegistrations.remove(getThing().getUID().toString()); @@ -217,68 +213,42 @@ public void dispose() { } @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 void handleCommand(ChannelUID channelUID, Command command) { 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 void handleCommand(ChannelUID channelUID, Command command) { * * @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 void setVolume(int volume) { 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 String getHost() { } /** - * 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 int getSimpleTcpPort() throws IOException, InterruptedException { } 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; - } } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/AbstractAudioDeviceConfig.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/AbstractAudioDeviceConfig.java index ae1f0d03b0d59..68a842f2fe136 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/AbstractAudioDeviceConfig.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/AbstractAudioDeviceConfig.java @@ -12,12 +12,16 @@ */ 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 enum State { 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; } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/AbstractDeviceConfig.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/AbstractDeviceConfig.java index 2539a5fbfc111..493293147ae28 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/AbstractDeviceConfig.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/AbstractDeviceConfig.java @@ -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 @@ * * @author Tobias Bräutigam - Initial contribution */ +@NonNullByDefault public abstract class AbstractDeviceConfig { protected int id; diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Module.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Module.java index f46ce95d2c291..cbe482136fa78 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Module.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Module.java @@ -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 @@ * * @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; } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Sink.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Sink.java index ce4771bc02fc1..0f31498277125 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Sink.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Sink.java @@ -15,6 +15,9 @@ 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 @@ * * @author Tobias Bräutigam - Initial contribution */ +@NonNullByDefault public class Sink extends AbstractAudioDeviceConfig { protected List combinedSinkNames; protected List 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 void setCombinedSinks(List combinedSinks) { this.combinedSinks = combinedSinks; } - public void addCombinedSink(Sink sink) { + public void addCombinedSink(@Nullable Sink sink) { if (sink != null) { this.combinedSinks.add(sink); } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SinkInput.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SinkInput.java index d138cddd88393..fa9cb5046a98c 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SinkInput.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SinkInput.java @@ -12,24 +12,29 @@ */ 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; } } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Source.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Source.java index 53e4220aee032..c5162b3a51dc6 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Source.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Source.java @@ -12,25 +12,30 @@ */ 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; } } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SourceOutput.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SourceOutput.java index 60edda6f8340b..0bda3854553c5 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SourceOutput.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SourceOutput.java @@ -12,24 +12,29 @@ */ 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; } }