[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 <gwendal.roulleau@gmail.com>
Co-authored-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
Gwendal Roulleau 2022-04-10 00:25:45 +02:00 committed by GitHub
parent f65ee6c3a8
commit 2be9a658d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 345 additions and 450 deletions

View File

@ -12,6 +12,7 @@
*/ */
package org.openhab.binding.pulseaudio.internal; package org.openhab.binding.pulseaudio.internal;
import java.io.BufferedInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Map; import java.util.Map;
@ -55,14 +56,13 @@ public class ConvertedInputStream extends InputStream {
public ConvertedInputStream(AudioStream innerInputStream) public ConvertedInputStream(AudioStream innerInputStream)
throws UnsupportedAudioFormatException, UnsupportedAudioFileException, IOException { throws UnsupportedAudioFormatException, UnsupportedAudioFileException, IOException {
this.audioFormat = innerInputStream.getFormat(); this.audioFormat = innerInputStream.getFormat();
if (innerInputStream instanceof FixedLengthAudioStream) { if (innerInputStream instanceof FixedLengthAudioStream) {
length = ((FixedLengthAudioStream) innerInputStream).length(); length = ((FixedLengthAudioStream) innerInputStream).length();
} }
pcmNormalizedInputStream = getPCMStreamNormalized(getPCMStream(new ResetableInputStream(innerInputStream))); pcmNormalizedInputStream = getPCMStreamNormalized(getPCMStream(new BufferedInputStream(innerInputStream)));
} }
@Override @Override
@ -108,7 +108,6 @@ public class ConvertedInputStream extends InputStream {
* @return A PCM normalized stream (2 channel, 44100hz, 16 bit signed) * @return A PCM normalized stream (2 channel, 44100hz, 16 bit signed)
*/ */
private AudioInputStream getPCMStreamNormalized(AudioInputStream pcmInputStream) { private AudioInputStream getPCMStreamNormalized(AudioInputStream pcmInputStream) {
javax.sound.sampled.AudioFormat format = pcmInputStream.getFormat(); javax.sound.sampled.AudioFormat format = pcmInputStream.getFormat();
if (format.getChannels() != 2 if (format.getChannels() != 2
|| !format.getEncoding().equals(javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED) || !format.getEncoding().equals(javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED)
@ -138,7 +137,6 @@ public class ConvertedInputStream extends InputStream {
*/ */
private AudioInputStream getPCMStream(InputStream resetableInnerInputStream) private AudioInputStream getPCMStream(InputStream resetableInnerInputStream)
throws UnsupportedAudioFileException, IOException, UnsupportedAudioFormatException { throws UnsupportedAudioFileException, IOException, UnsupportedAudioFormatException {
if (AudioFormat.MP3.isCompatible(audioFormat)) { if (AudioFormat.MP3.isCompatible(audioFormat)) {
MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader(); MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader();
@ -170,7 +168,6 @@ public class ConvertedInputStream extends InputStream {
sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false); sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false);
return mpegconverter.getAudioInputStream(convertFormat, sourceAIS); return mpegconverter.getAudioInputStream(convertFormat, sourceAIS);
} else if (AudioFormat.WAV.isCompatible(audioFormat)) { } else if (AudioFormat.WAV.isCompatible(audioFormat)) {
// return the same input stream, but try to compute the duration first // return the same input stream, but try to compute the duration first
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(resetableInnerInputStream); AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(resetableInnerInputStream);
@ -187,71 +184,4 @@ public class ConvertedInputStream extends InputStream {
audioFormat); 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;
}
}
} }

View File

@ -91,7 +91,9 @@ public class PulseAudioAudioSink extends PulseaudioSimpleProtocolStream implemen
} catch (IOException e) { } catch (IOException e) {
disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown
if (countAttempt == 2) { // we won't retry : log and quit 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( logger.warn(
"Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}", "Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
pulseaudioHandler.getHost(), port, e.getMessage()); pulseaudioHandler.getHost(), port, e.getMessage());

View File

@ -107,7 +107,9 @@ public class PulseAudioAudioSource extends PulseaudioSimpleProtocolStream implem
} catch (IOException e) { } catch (IOException e) {
disconnect(); // disconnect to force clear connection in case of socket not cleanly shutdown disconnect(); // disconnect to force clear connection in case of socket not cleanly shutdown
if (countAttempt == 2) { // we won't retry : log and quit 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( logger.warn(
"Error while trying to get audio from pulseaudio audio source. Cannot connect to {}:{}, error: {}", "Error while trying to get audio from pulseaudio audio source. Cannot connect to {}:{}, error: {}",
pulseaudioHandler.getHost(), port, e.getMessage()); pulseaudioHandler.getHost(), port, e.getMessage());
@ -153,11 +155,14 @@ public class PulseAudioAudioSource extends PulseaudioSimpleProtocolStream implem
if (pipeOutputs.contains(output)) { if (pipeOutputs.contains(output)) {
output.flush(); output.flush();
} }
} catch (IOException e) { } catch (InterruptedIOException e) {
if (e instanceof InterruptedIOException && pipeOutputs.isEmpty()) { if (pipeOutputs.isEmpty()) {
// task has been ended while writing // task has been ended while writing
return; 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: {}", logger.warn("IOException while writing to from pulse source pipe: {}",
getExceptionMessage(e)); getExceptionMessage(e));
} catch (RuntimeException e) { } catch (RuntimeException e) {
@ -221,7 +226,8 @@ public class PulseAudioAudioSource extends PulseaudioSimpleProtocolStream implem
} catch (IOException | InterruptedException ignored) { } catch (IOException | InterruptedException ignored) {
} }
try { try {
return (clientSocket != null) ? clientSocket.getInputStream() : null; var clientSocketFinal = clientSocket;
return (clientSocketFinal != null) ? clientSocketFinal.getInputStream() : null;
} catch (IOException ignored) { } catch (IOException ignored) {
return null; return null;
} }
@ -264,11 +270,14 @@ public class PulseAudioAudioSource extends PulseaudioSimpleProtocolStream implem
@Override @Override
public int read(byte @Nullable [] b) throws IOException { public int read(byte @Nullable [] b) throws IOException {
return read(b, 0, b.length); return read(b, 0, b == null ? 0 : b.length);
} }
@Override @Override
public int read(byte @Nullable [] b, int off, int len) throws IOException { 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"); logger.trace("reading from pulseaudio stream");
if (closed) { if (closed) {
throw new IOException("Stream is closed"); throw new IOException("Stream is closed");

View File

@ -27,7 +27,6 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Random; import java.util.Random;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.pulseaudio.internal.cli.Parser; import org.openhab.binding.pulseaudio.internal.cli.Parser;
@ -150,7 +149,6 @@ public class PulseaudioClient {
modules = new ArrayList<Module>(Parser.parseModules(listModules())); modules = new ArrayList<Module>(Parser.parseModules(listModules()));
List<AbstractAudioDeviceConfig> newItems = new ArrayList<>(); // prepare new list before assigning it List<AbstractAudioDeviceConfig> newItems = new ArrayList<>(); // prepare new list before assigning it
newItems.clear();
if (configuration.sink) { if (configuration.sink) {
logger.debug("reading sinks"); logger.debug("reading sinks");
newItems.addAll(Parser.parseSinks(listSinks(), this)); newItems.addAll(Parser.parseSinks(listSinks(), this));
@ -245,48 +243,6 @@ public class PulseaudioClient {
return null; 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 * retrieves a {@link Source} by its id
* *
@ -301,34 +257,6 @@ public class PulseaudioClient {
return null; 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 * retrieves a {@link AbstractAudioDeviceConfig} by its name
* *
@ -343,6 +271,11 @@ public class PulseaudioClient {
return null; return null;
} }
/**
* Get all items previously parsed from the pulseaudio server.
*
* @return All items parsed from the pulseaudio server
*/
public List<AbstractAudioDeviceConfig> getItems() { public List<AbstractAudioDeviceConfig> getItems() {
return items; return items;
} }
@ -479,16 +412,18 @@ public class PulseaudioClient {
.map(portS -> Integer.parseInt(portS)); .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; String argument = null;
int startPortIndex = argumentLine.indexOf(argumentWanted + "="); if (argumentLine != null) {
if (startPortIndex != -1) { int startPortIndex = argumentLine.indexOf(argumentWanted + "=");
startPortIndex = startPortIndex + argumentWanted.length() + 1; if (startPortIndex != -1) {
int endPortIndex = argumentLine.indexOf(" ", startPortIndex); startPortIndex = startPortIndex + argumentWanted.length() + 1;
if (endPortIndex == -1) { int endPortIndex = argumentLine.indexOf(" ", startPortIndex);
endPortIndex = argumentLine.length(); if (endPortIndex == -1) {
endPortIndex = argumentLine.length();
}
argument = argumentLine.substring(startPortIndex, endPortIndex);
} }
argument = argumentLine.substring(startPortIndex, endPortIndex);
} }
return Optional.ofNullable(argument); return Optional.ofNullable(argument);
} }
@ -552,7 +487,10 @@ public class PulseaudioClient {
slaves.add(sink.getPaName()); slaves.add(sink.getPaName());
} }
// 1. delete old combined-sink // 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 // 2. add new combined-sink with same name and all slaves
sendRawCommand(CMD_LOAD_MODULE + " " + MODULE_COMBINE_SINK + " sink_name=" + combinedSink.getPaName() sendRawCommand(CMD_LOAD_MODULE + " " + MODULE_COMBINE_SINK + " sink_name=" + combinedSink.getPaName()
+ " slaves=" + String.join(",", slaves)); + " slaves=" + String.join(",", slaves));
@ -731,8 +669,9 @@ public class PulseaudioClient {
if (clientSocket == null || clientSocket.isClosed() || !clientSocket.isConnected()) { if (clientSocket == null || clientSocket.isClosed() || !clientSocket.isConnected()) {
logger.trace("Try to connect..."); logger.trace("Try to connect...");
try { try {
client = new Socket(host, port); var clientFinal = new Socket(host, port);
client.setSoTimeout(500); clientFinal.setSoTimeout(500);
client = clientFinal;
logger.trace("connected"); logger.trace("connected");
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
client = null; client = null;

View File

@ -20,6 +20,8 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; 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.discovery.PulseaudioDeviceDiscoveryService;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler; import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler; import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
@ -47,6 +49,7 @@ import org.slf4j.LoggerFactory;
* @author Tobias Bräutigam - Initial contribution * @author Tobias Bräutigam - Initial contribution
*/ */
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.pulseaudio") @Component(service = ThingHandlerFactory.class, configurationPid = "binding.pulseaudio")
@NonNullByDefault
public class PulseaudioHandlerFactory extends BaseThingHandlerFactory { public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(PulseaudioHandlerFactory.class); private final Logger logger = LoggerFactory.getLogger(PulseaudioHandlerFactory.class);
@ -64,8 +67,8 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
} }
@Override @Override
public Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration, ThingUID thingUID, public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration,
ThingUID bridgeUID) { @Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) {
if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
return super.createThing(thingTypeUID, configuration, thingUID, null); return super.createThing(thingTypeUID, configuration, thingUID, null);
} }
@ -83,11 +86,11 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>())); bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
} }
private ThingUID getPulseaudioDeviceUID(ThingTypeUID thingTypeUID, ThingUID thingUID, Configuration configuration, private ThingUID getPulseaudioDeviceUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID,
ThingUID bridgeUID) { Configuration configuration, @Nullable ThingUID bridgeUID) {
if (thingUID == null) { if (thingUID == null) {
String name = (String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME); 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; return thingUID;
} }
@ -106,8 +109,7 @@ public class PulseaudioHandlerFactory extends BaseThingHandlerFactory {
} }
@Override @Override
protected ThingHandler createHandler(Thing thing) { protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID(); ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {

View File

@ -63,9 +63,10 @@ public abstract class PulseaudioSimpleProtocolStream {
if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) { if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) {
logger.debug("Simple TCP Stream connecting"); logger.debug("Simple TCP Stream connecting");
String host = pulseaudioHandler.getHost(); String host = pulseaudioHandler.getHost();
int port = pulseaudioHandler.getSimpleTcpPort(); int port = pulseaudioHandler.getSimpleTcpPortAndLoadModuleIfNecessary();
clientSocket = new Socket(host, port); var clientSocketFinal = new Socket(host, port);
clientSocket.setSoTimeout(pulseaudioHandler.getBasicProtocolSOTimeout()); clientSocketFinal.setSoTimeout(pulseaudioHandler.getBasicProtocolSOTimeout());
clientSocket = clientSocketFinal;
} }
} }
@ -86,8 +87,9 @@ public abstract class PulseaudioSimpleProtocolStream {
} }
public void scheduleDisconnect() { public void scheduleDisconnect() {
if (scheduledDisconnection != null) { var scheduledDisconnectionFinal = scheduledDisconnection;
scheduledDisconnection.cancel(true); if (scheduledDisconnectionFinal != null) {
scheduledDisconnectionFinal.cancel(true);
} }
int idleTimeout = pulseaudioHandler.getIdleTimeout(); int idleTimeout = pulseaudioHandler.getIdleTimeout();
if (idleTimeout > -1) { if (idleTimeout > -1) {

View File

@ -19,6 +19,8 @@ import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; 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.PulseaudioClient;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig; import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
import org.openhab.binding.pulseaudio.internal.items.Module; import org.openhab.binding.pulseaudio.internal.items.Module;
@ -34,6 +36,7 @@ import org.slf4j.LoggerFactory;
* *
* @author Tobias Bräutigam - Initial contribution * @author Tobias Bräutigam - Initial contribution
*/ */
@NonNullByDefault
public class Parser { public class Parser {
private static final Logger LOGGER = LoggerFactory.getLogger(Parser.class); private static final Logger LOGGER = LoggerFactory.getLogger(Parser.class);
@ -143,10 +146,8 @@ public class Parser {
if (properties.containsKey("combine.slaves")) { if (properties.containsKey("combine.slaves")) {
// this is a combined sink, the combined sink object should be // this is a combined sink, the combined sink object should be
String sinkNames = properties.get("combine.slaves"); String sinkNames = properties.get("combine.slaves");
if (sinkNames != null) { for (String sinkName : sinkNames.replace("\"", "").split(",")) {
for (String sinkName : sinkNames.replace("\"", "").split(",")) { sink.addCombinedSinkName(sinkName);
sink.addCombinedSinkName(sinkName);
}
} }
combinedSinks.add(sink); combinedSinks.add(sink);
} }
@ -270,8 +271,8 @@ public class Parser {
if (properties.containsKey("volume")) { if (properties.containsKey("volume")) {
source.setVolume(parseVolume(properties.get("volume"))); source.setVolume(parseVolume(properties.get("volume")));
} }
String monitorOf = properties.get("monitor_of"); if (properties.containsKey("monitor_of")) {
if (monitorOf != null) { String monitorOf = properties.get("monitor_of");
source.setMonitorOf(client.getSink(Integer.valueOf(monitorOf))); source.setMonitorOf(client.getSink(Integer.valueOf(monitorOf)));
} }
sources.add(source); sources.add(source);
@ -373,7 +374,7 @@ public class Parser {
* @param raw * @param raw
* @return * @return
*/ */
private static int getNumberValue(String raw) { private static int getNumberValue(@Nullable String raw) {
int id = -1; int id = -1;
if (raw == null) { if (raw == null) {
return 0; return 0;

View File

@ -15,7 +15,9 @@ package org.openhab.binding.pulseaudio.internal.discovery;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; 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.PulseaudioBindingConstants;
import org.openhab.binding.pulseaudio.internal.handler.DeviceStatusListener; import org.openhab.binding.pulseaudio.internal.handler.DeviceStatusListener;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler; 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.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder; 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.ThingTypeUID;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -40,6 +42,7 @@ import org.slf4j.LoggerFactory;
* *
* @author Tobias Bräutigam - Initial contribution * @author Tobias Bräutigam - Initial contribution
*/ */
@NonNullByDefault
public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService implements DeviceStatusListener { public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService implements DeviceStatusListener {
private final Logger logger = LoggerFactory.getLogger(PulseaudioDeviceDiscoveryService.class); private final Logger logger = LoggerFactory.getLogger(PulseaudioDeviceDiscoveryService.class);
@ -66,7 +69,11 @@ public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService i
} }
@Override @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(); String uidName = device.getPaName();
logger.debug("device {} found", device); logger.debug("device {} found", device);
ThingTypeUID thingType = null; ThingTypeUID thingType = null;
@ -97,18 +104,14 @@ public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService i
} }
} }
public Set<String> getAlreadyConfiguredThings() {
return pulseaudioBridgeHandler.getThing().getThings().stream().map(Thing::getConfiguration)
.map(conf -> (String) conf.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME))
.collect(Collectors.toSet());
}
@Override @Override
protected void startScan() { protected void startScan() {
// this can be ignored here as we discover via the PulseaudioClient.update() mechanism pulseaudioBridgeHandler.resetKnownActiveDevices();
}
@Override
public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) {
// this can be ignored here
}
@Override
public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) {
// this can be ignored here
} }
} }

View File

@ -20,6 +20,8 @@ import java.util.Set;
import javax.jmdns.ServiceInfo; 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.PulseaudioBindingConstants;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler; import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler;
import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResult;
@ -38,6 +40,7 @@ import org.slf4j.LoggerFactory;
* @author Tobias Bräutigam - Initial contribution * @author Tobias Bräutigam - Initial contribution
*/ */
@Component @Component
@NonNullByDefault
public class PulseaudioDiscoveryParticipant implements MDNSDiscoveryParticipant { public class PulseaudioDiscoveryParticipant implements MDNSDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(PulseaudioDiscoveryParticipant.class); private final Logger logger = LoggerFactory.getLogger(PulseaudioDiscoveryParticipant.class);
@ -48,7 +51,7 @@ public class PulseaudioDiscoveryParticipant implements MDNSDiscoveryParticipant
} }
@Override @Override
public DiscoveryResult createResult(ServiceInfo info) { public @Nullable DiscoveryResult createResult(ServiceInfo info) {
DiscoveryResult result = null; DiscoveryResult result = null;
ThingUID uid = getThingUID(info); ThingUID uid = getThingUID(info);
if (uid != null) { if (uid != null) {
@ -79,15 +82,12 @@ public class PulseaudioDiscoveryParticipant implements MDNSDiscoveryParticipant
} }
@Override @Override
public ThingUID getThingUID(ServiceInfo info) { public @Nullable ThingUID getThingUID(ServiceInfo info) {
if (info != null) { logger.debug("ServiceInfo: {}", info);
logger.debug("ServiceInfo: {}", info); if (info.getType() != null) {
if (info.getType() != null) { if (info.getType().equals(getServiceType())) {
if (info.getType().equals(getServiceType())) { logger.trace("Discovered a pulseaudio server thing with name '{}'", info.getName());
logger.trace("Discovered a pulseaudio server thing with name '{}'", info.getName()); return new ThingUID(PulseaudioBindingConstants.BRIDGE_THING_TYPE, info.getName().replace("@", "_AT_"));
return new ThingUID(PulseaudioBindingConstants.BRIDGE_THING_TYPE,
info.getName().replace("@", "_AT_"));
}
} }
} }
return null; return null;

View File

@ -14,8 +14,7 @@ package org.openhab.binding.pulseaudio.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig; import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
/** /**
* The {@link DeviceStatusListener} is notified when a device status has changed * The {@link DeviceStatusListener} is notified when a device status has changed
@ -26,28 +25,11 @@ import org.openhab.core.thing.ThingUID;
*/ */
@NonNullByDefault @NonNullByDefault
public interface DeviceStatusListener { 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. * This method us called whenever a device is added.
* *
* @param bridge The Pulseaudio bridge the added device was connected to. * @param bridge The Pulseaudio bridge the added device was connected to.
* @param device The device which is added. * @param device The device which is added.
*/ */
public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device); public void onDeviceAdded(Thing bridge, AbstractAudioDeviceConfig device);
} }

View File

@ -24,6 +24,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.pulseaudio.internal.PulseAudioBindingConfiguration; import org.openhab.binding.pulseaudio.internal.PulseAudioBindingConfiguration;
import org.openhab.binding.pulseaudio.internal.PulseAudioBindingConfigurationListener; 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.config.core.Configuration;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseBridgeHandler; 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.Command;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -47,8 +50,10 @@ import org.slf4j.LoggerFactory;
* connects it to the framework. * connects it to the framework.
* *
* @author Tobias Bräutigam - Initial contribution * @author Tobias Bräutigam - Initial contribution
* @author Gwendal Roulleau - Rewrite for child handler notification
* *
*/ */
@NonNullByDefault
public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseAudioBindingConfigurationListener { public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseAudioBindingConfigurationListener {
private final Logger logger = LoggerFactory.getLogger(PulseaudioBridgeHandler.class); private final Logger logger = LoggerFactory.getLogger(PulseaudioBridgeHandler.class);
@ -60,22 +65,22 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
public int refreshInterval = 30000; public int refreshInterval = 30000;
@Nullable
private PulseaudioClient client; private PulseaudioClient client;
private PulseAudioBindingConfiguration configuration; private PulseAudioBindingConfiguration configuration;
private List<DeviceStatusListener> deviceStatusListeners = new CopyOnWriteArrayList<>(); private List<DeviceStatusListener> deviceStatusListeners = new CopyOnWriteArrayList<>();
private HashSet<String> lastActiveDevices = new HashSet<>(); private Set<String> lastActiveDevices = new HashSet<>();
@Nullable
private ScheduledFuture<?> pollingJob; private ScheduledFuture<?> pollingJob;
private synchronized void update() { private Set<PulseaudioHandler> childHandlersInitialized = new HashSet<>();
public synchronized void update() {
try { try {
client.connect(); getClient().connect();
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
logger.debug("Established connection to Pulseaudio server on Host '{}':'{}'.", host, port);
}
} catch (IOException e) { } catch (IOException e) {
logger.debug("{}", e.getMessage(), e); logger.debug("{}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
@ -84,21 +89,23 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
return; return;
} }
client.update(); getClient().update();
for (AbstractAudioDeviceConfig device : client.getItems()) { if (getThing().getStatus() != ThingStatus.ONLINE) {
if (lastActiveDevices != null && lastActiveDevices.contains(device.getPaName())) { updateStatus(ThingStatus.ONLINE);
for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) { logger.debug("Established connection to Pulseaudio server on Host '{}':'{}'.", host, port);
try { // The framework will automatically notify the child handlers as the bridge status is changed
deviceStatusListener.onDeviceStateChanged(getThing().getUID(), device); } else {
} catch (Exception e) { // browse all child handlers to update status according to the result of the query to the pulse audio server
logger.warn("An exception occurred while calling the DeviceStatusListener", e); for (PulseaudioHandler pulseaudioHandler : childHandlersInitialized) {
} pulseaudioHandler.deviceUpdate(getDevice(pulseaudioHandler.getName()));
} }
} else { }
// browse query result to notify add event
for (AbstractAudioDeviceConfig device : getClient().getItems()) {
if (!lastActiveDevices.contains(device.getPaName())) {
for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) { for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
try { try {
deviceStatusListener.onDeviceAdded(getThing(), device); deviceStatusListener.onDeviceAdded(getThing(), device);
deviceStatusListener.onDeviceStateChanged(getThing().getUID(), device);
} catch (Exception e) { } catch (Exception e) {
logger.warn("An exception occurred while calling the DeviceStatusListener", e); logger.warn("An exception occurred while calling the DeviceStatusListener", e);
} }
@ -116,18 +123,22 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
@Override @Override
public void handleCommand(ChannelUID channelUID, Command command) { public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) { if (command instanceof RefreshType) {
client.update(); getClient().update();
} else { } else {
logger.debug("received unexpected command for pulseaudio bridge '{}'.", host); logger.debug("received unexpected command for pulseaudio bridge '{}'.", host);
} }
} }
public @Nullable AbstractAudioDeviceConfig getDevice(String name) { public @Nullable AbstractAudioDeviceConfig getDevice(String name) {
return client.getGenericAudioItem(name); return getClient().getGenericAudioItem(name);
} }
public PulseaudioClient getClient() { public PulseaudioClient getClient() {
return client; PulseaudioClient clientFinal = client;
if (clientFinal == null) {
throw new AssertionError("PulseaudioClient is null !");
}
return clientFinal;
} }
@Override @Override
@ -145,10 +156,11 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
this.refreshInterval = ((BigDecimal) conf.get(BRIDGE_PARAMETER_REFRESH_INTERVAL)).intValue(); this.refreshInterval = ((BigDecimal) conf.get(BRIDGE_PARAMETER_REFRESH_INTERVAL)).intValue();
} }
if (host != null && !host.isEmpty()) { if (!host.isBlank()) {
client = new PulseaudioClient(host, port, configuration); client = new PulseaudioClient(host, port, configuration);
updateStatus(ThingStatus.UNKNOWN); 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); pollingJob = scheduler.scheduleWithFixedDelay(this::update, 0, refreshInterval, TimeUnit.MILLISECONDS);
} }
} else { } else {
@ -168,16 +180,14 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
job.cancel(true); job.cancel(true);
pollingJob = null; pollingJob = null;
} }
if (client != null) { var clientFinal = client;
client.disconnect(); if (clientFinal != null) {
clientFinal.disconnect();
} }
super.dispose(); super.dispose();
} }
public boolean registerDeviceStatusListener(DeviceStatusListener deviceStatusListener) { public boolean registerDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
if (deviceStatusListener == null) {
throw new IllegalArgumentException("It's not allowed to pass a null deviceStatusListener.");
}
return deviceStatusListeners.add(deviceStatusListener); return deviceStatusListeners.add(deviceStatusListener);
} }
@ -187,6 +197,33 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
@Override @Override
public void bindingConfigurationChanged() { 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);
} }
} }

View File

@ -21,10 +21,9 @@ import java.util.Collections;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; 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.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo; import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.ThingTypeUID; 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.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
@ -73,17 +71,14 @@ import org.slf4j.LoggerFactory;
* @author Miguel Álvarez - Register audio source and refactor * @author Miguel Álvarez - Register audio source and refactor
*/ */
@NonNullByDefault @NonNullByDefault
public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusListener { public class PulseaudioHandler extends BaseThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(SINK_THING_TYPE, COMBINED_SINK_THING_TYPE, SINK_INPUT_THING_TYPE, .unmodifiableSet(Stream.of(SINK_THING_TYPE, COMBINED_SINK_THING_TYPE, SINK_INPUT_THING_TYPE,
SOURCE_THING_TYPE, SOURCE_OUTPUT_THING_TYPE).collect(Collectors.toSet())); SOURCE_THING_TYPE, SOURCE_OUTPUT_THING_TYPE).collect(Collectors.toSet()));
private final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class); private final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class);
private final int refresh = 60; // refresh every minute as default
private @Nullable PulseaudioBridgeHandler bridgeHandler; private String name = "";
private @Nullable String name;
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable PulseAudioAudioSink audioSink; private @Nullable PulseAudioAudioSink audioSink;
private @Nullable PulseAudioAudioSource audioSource; private @Nullable PulseAudioAudioSource audioSource;
private @Nullable Integer savedVolume; private @Nullable Integer savedVolume;
@ -102,37 +97,32 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
public void initialize() { public void initialize() {
Configuration config = getThing().getConfiguration(); Configuration config = getThing().getConfiguration();
name = (String) config.get(DEVICE_PARAMETER_NAME); name = (String) config.get(DEVICE_PARAMETER_NAME);
initializeWithTheBridge();
}
updateStatus(ThingStatus.UNKNOWN); public String getName() {
deviceOnlineWatchdog(); return name;
// 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();
}
}
} }
private void audioSinkSetup() { 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; final PulseaudioHandler thisHandler = this;
PulseAudioAudioSink audioSink = new PulseAudioAudioSink(thisHandler, scheduler);
scheduler.submit(new Runnable() { scheduler.submit(new Runnable() {
@Override @Override
public void run() { public void run() {
// Register the sink as an audio sink in openhab PulseaudioHandler.this.audioSink = audioSink;
logger.trace("Registering an audio sink for pulse audio sink thing {}", thing.getUID());
PulseAudioAudioSink audioSink = new PulseAudioAudioSink(thisHandler, scheduler);
setAudioSink(audioSink);
try { try {
audioSink.connectIfNeeded(); audioSink.connectIfNeeded();
} catch (IOException e) { } catch (IOException e) {
@ -144,23 +134,49 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
} finally { } finally {
audioSink.scheduleDisconnect(); 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() { 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; final PulseaudioHandler thisHandler = this;
PulseAudioAudioSource audioSource = new PulseAudioAudioSource(thisHandler, scheduler);
scheduler.submit(new Runnable() { scheduler.submit(new Runnable() {
@Override @Override
public void run() { public void run() {
// Register the source as an audio source in openhab PulseaudioHandler.this.audioSource = audioSource;
logger.trace("Registering an audio source for pulse audio source thing {}", thing.getUID());
PulseAudioAudioSource audioSource = new PulseAudioAudioSource(thisHandler, scheduler);
setAudioSource(audioSource);
try { try {
audioSource.connectIfNeeded(); audioSource.connectIfNeeded();
} catch (IOException e) { } catch (IOException e) {
@ -172,41 +188,21 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
} finally { } finally {
audioSource.scheduleDisconnect(); 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 private void audioSourceUnsetup() {
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();
}
PulseAudioAudioSource source = audioSource; PulseAudioAudioSource source = audioSource;
if (source != null) { if (source != null) {
source.disconnect(); source.disconnect();
} audioSource = 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();
} }
// Unregister the potential pulse audio source's audio sources // Unregister the potential pulse audio source's audio sources
ServiceRegistration<AudioSource> sourceReg = audioSourceRegistrations.remove(getThing().getUID().toString()); ServiceRegistration<AudioSource> sourceReg = audioSourceRegistrations.remove(getThing().getUID().toString());
@ -217,68 +213,42 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
} }
@Override @Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { public void dispose() {
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
&& getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE) { super.dispose();
// Bridge is now ONLINE, restart the refresh job to get an update of the thing status without waiting audioSinkUnsetup();
// its next planned run audioSourceUnsetup();
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);
}
} }
private void deviceOnlineWatchdog() { @Override
Runnable runnable = () -> { public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
try { initializeWithTheBridge();
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;
}
};
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() { private synchronized @Nullable PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
if (this.bridgeHandler == null) { Bridge bridge = getBridge();
Bridge bridge = getBridge(); if (bridge == null) {
if (bridge == null) { logger.debug("Required bridge not defined for device {}.", name);
logger.debug("Required bridge not defined for device {}.", name); return null;
return null; }
} ThingHandler handler = bridge.getHandler();
ThingHandler handler = bridge.getHandler(); if (handler instanceof PulseaudioBridgeHandler) {
if (handler instanceof PulseaudioBridgeHandler) { return (PulseaudioBridgeHandler) handler;
this.bridgeHandler = (PulseaudioBridgeHandler) handler; } else {
this.bridgeHandler.registerDeviceStatusListener(this); logger.debug("No available bridge handler found for device {} bridge {} .", name, bridge.getUID());
} else { return null;
logger.debug("No available bridge handler found for device {} bridge {} .", name, bridge.getUID());
return null;
}
} }
return this.bridgeHandler;
} }
@Override @Override
@ -296,8 +266,7 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
AbstractAudioDeviceConfig device = briHandler.getDevice(name); AbstractAudioDeviceConfig device = briHandler.getDevice(name);
if (device == null) { if (device == null) {
logger.warn("device {} not found", name); logger.warn("device {} not found", name);
updateStatus(ThingStatus.OFFLINE); deviceUpdate(null);
bridgeHandler = null;
return; return;
} else { } else {
State updateState = UnDefType.UNDEF; State updateState = UnDefType.UNDEF;
@ -384,19 +353,20 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
* *
* @return * @return
*/ */
public int getLastVolume() { public Integer getLastVolume() {
if (savedVolume == null) { Integer savedVolumeFinal = savedVolume;
if (savedVolumeFinal == null) {
PulseaudioBridgeHandler briHandler = getPulseaudioBridgeHandler(); PulseaudioBridgeHandler briHandler = getPulseaudioBridgeHandler();
if (briHandler != null) { if (briHandler != null) {
// refresh to get the current volume level // refresh to get the current volume level
briHandler.getClient().update(); briHandler.getClient().update();
AbstractAudioDeviceConfig device = briHandler.getDevice(name); AbstractAudioDeviceConfig device = briHandler.getDevice(name);
if (device != null) { 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) { public void setVolume(int volume) {
@ -415,25 +385,38 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
savedVolume = volume; savedVolume = volume;
} }
@Override public void deviceUpdate(@Nullable AbstractAudioDeviceConfig device) {
public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) { if (device != null && device.getPaName().equals(name)) {
if (device.getPaName().equals(name)) { updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
updateStatus(ThingStatus.ONLINE);
logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL); logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL);
savedVolume = device.getVolume(); int actualVolume = device.getVolume();
updateState(VOLUME_CHANNEL, new PercentType(savedVolume)); savedVolume = actualVolume;
updateState(MUTE_CHANNEL, device.isMuted() ? OnOffType.ON : OnOffType.OFF); updateState(VOLUME_CHANNEL, new PercentType(actualVolume));
updateState(STATE_CHANNEL, updateState(MUTE_CHANNEL, OnOffType.from(device.isMuted()));
device.getState() != null ? new StringType(device.getState().toString()) : new StringType("-")); 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) { if (device instanceof SinkInput) {
updateState(ROUTE_TO_SINK_CHANNEL, updateState(ROUTE_TO_SINK_CHANNEL, new StringType(
((SinkInput) device).getSink() != null Optional.ofNullable(((SinkInput) device).getSink()).map(Sink::getPaName).orElse("-")));
? new StringType(((SinkInput) device).getSink().getPaName())
: new StringType("-"));
} }
if (device instanceof Sink && ((Sink) device).isCombinedSink()) { if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
updateState(SLAVES_CHANNEL, new StringType(String.join(",", ((Sink) device).getCombinedSinkNames()))); 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, * 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 IOException when device info is not available
* @throws InterruptedException when interrupted during the loading module wait * @throws InterruptedException when interrupted during the loading module wait
*/ */
public int getSimpleTcpPort() throws IOException, InterruptedException { public int getSimpleTcpPortAndLoadModuleIfNecessary() throws IOException, InterruptedException {
var briHandler = getPulseaudioBridgeHandler(); var briHandler = getPulseaudioBridgeHandler();
if (briHandler == null) { if (briHandler == null) {
throw new IOException("bridge is not ready"); throw new IOException("bridge is not ready");
@ -515,43 +498,22 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
} }
public int getIdleTimeout() { public int getIdleTimeout() {
var idleTimeout = 3000;
var handler = getPulseaudioBridgeHandler(); var handler = getPulseaudioBridgeHandler();
if (handler == null) { if (handler != null) {
return 30000; AbstractAudioDeviceConfig device = handler.getDevice(name);
String idleTimeoutPropName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT
: DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT;
var idleTimeoutB = (BigDecimal) getThing().getConfiguration().get(idleTimeoutPropName);
if (idleTimeoutB != null) {
idleTimeout = idleTimeoutB.intValue();
}
} }
AbstractAudioDeviceConfig device = handler.getDevice(name); return idleTimeout;
String idleTimeoutPropName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT
: DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT;
var idleTimeout = (BigDecimal) getThing().getConfiguration().get(idleTimeoutPropName);
return idleTimeout != null ? idleTimeout.intValue() : 30000;
} }
public int getBasicProtocolSOTimeout() { public int getBasicProtocolSOTimeout() {
var soTimeout = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOCKET_SO_TIMEOUT); var soTimeout = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOCKET_SO_TIMEOUT);
return soTimeout != null ? soTimeout.intValue() : 500; 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;
}
} }

View File

@ -12,12 +12,16 @@
*/ */
package org.openhab.binding.pulseaudio.internal.items; 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 * GenericAudioItems are any kind of items that deal with audio data and can be
* muted or their volume can be changed. * muted or their volume can be changed.
* *
* @author Tobias Bräutigam - Initial contribution * @author Tobias Bräutigam - Initial contribution
*/ */
@NonNullByDefault
public abstract class AbstractAudioDeviceConfig extends AbstractDeviceConfig { public abstract class AbstractAudioDeviceConfig extends AbstractDeviceConfig {
public enum State { public enum State {
@ -28,25 +32,21 @@ public abstract class AbstractAudioDeviceConfig extends AbstractDeviceConfig {
DRAINED DRAINED
} }
protected State state; protected @Nullable State state;
protected boolean muted; protected boolean muted;
protected int volume; 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); super(id, name);
this.module = module; this.module = module;
} }
public Module getModule() { public @Nullable Module getModule() {
return module; return module;
} }
public void setModule(Module module) { public @Nullable State getState() {
this.module = module;
}
public State getState() {
return state; return state;
} }

View File

@ -12,6 +12,8 @@
*/ */
package org.openhab.binding.pulseaudio.internal.items; 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 * 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 * 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 * @author Tobias Bräutigam - Initial contribution
*/ */
@NonNullByDefault
public abstract class AbstractDeviceConfig { public abstract class AbstractDeviceConfig {
protected int id; protected int id;

View File

@ -12,6 +12,9 @@
*/ */
package org.openhab.binding.pulseaudio.internal.items; 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 * In order to add a {@link Sink} to the pulseaudio server you have to
* load a corresponding module. Current Module objects are needed 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 * @author Tobias Bräutigam - Initial contribution
*/ */
@NonNullByDefault
public class Module extends AbstractDeviceConfig { public class Module extends AbstractDeviceConfig {
private String argument; private @Nullable String argument;
public Module(int id, String name) { public Module(int id, String name) {
super(id, name); super(id, name);
} }
public String getArgument() { public @Nullable String getArgument() {
return argument; return argument;
} }

View File

@ -15,6 +15,9 @@ package org.openhab.binding.pulseaudio.internal.items;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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 * 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 * (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 * @author Tobias Bräutigam - Initial contribution
*/ */
@NonNullByDefault
public class Sink extends AbstractAudioDeviceConfig { public class Sink extends AbstractAudioDeviceConfig {
protected List<String> combinedSinkNames; protected List<String> combinedSinkNames;
protected List<Sink> combinedSinks; 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); super(id, name, module);
combinedSinkNames = new ArrayList<>(); combinedSinkNames = new ArrayList<>();
combinedSinks = new ArrayList<>(); combinedSinks = new ArrayList<>();
@ -53,7 +57,7 @@ public class Sink extends AbstractAudioDeviceConfig {
this.combinedSinks = combinedSinks; this.combinedSinks = combinedSinks;
} }
public void addCombinedSink(Sink sink) { public void addCombinedSink(@Nullable Sink sink) {
if (sink != null) { if (sink != null) {
this.combinedSinks.add(sink); this.combinedSinks.add(sink);
} }

View File

@ -12,24 +12,29 @@
*/ */
package org.openhab.binding.pulseaudio.internal.items; 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} * A SinkInput is an audio stream which can be routed to a {@link Sink}
* *
* @author Tobias Bräutigam - Initial contribution * @author Tobias Bräutigam - Initial contribution
*/ */
@NonNullByDefault
public class SinkInput extends AbstractAudioDeviceConfig { public class SinkInput extends AbstractAudioDeviceConfig {
@Nullable
private Sink sink; private Sink sink;
public SinkInput(int id, String name, Module module) { public SinkInput(int id, String name, @Nullable Module module) {
super(id, name, module); super(id, name, module);
} }
public Sink getSink() { public @Nullable Sink getSink() {
return sink; return sink;
} }
public void setSink(Sink sink) { public void setSink(@Nullable Sink sink) {
this.sink = sink; this.sink = sink;
} }
} }

View File

@ -12,25 +12,30 @@
*/ */
package org.openhab.binding.pulseaudio.internal.items; 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 * A Source is a device which is the source of an audio stream (recording
* device) For example microphones or line-in jacks. * device) For example microphones or line-in jacks.
* *
* @author Tobias Bräutigam - Initial contribution * @author Tobias Bräutigam - Initial contribution
*/ */
@NonNullByDefault
public class Source extends AbstractAudioDeviceConfig { public class Source extends AbstractAudioDeviceConfig {
@Nullable
protected Sink monitorOf; protected Sink monitorOf;
public Source(int id, String name, Module module) { public Source(int id, String name, @Nullable Module module) {
super(id, name, module); super(id, name, module);
} }
public Sink getMonitorOf() { public @Nullable Sink getMonitorOf() {
return monitorOf; return monitorOf;
} }
public void setMonitorOf(Sink sink) { public void setMonitorOf(@Nullable Sink sink) {
this.monitorOf = sink; this.monitorOf = sink;
} }
} }

View File

@ -12,24 +12,29 @@
*/ */
package org.openhab.binding.pulseaudio.internal.items; 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} * A SourceOutput is the audio stream which is produced by a (@link Source}
* *
* @author Tobias Bräutigam - Initial contribution * @author Tobias Bräutigam - Initial contribution
*/ */
@NonNullByDefault
public class SourceOutput extends AbstractAudioDeviceConfig { public class SourceOutput extends AbstractAudioDeviceConfig {
@Nullable
private Source source; private Source source;
public SourceOutput(int id, String name, Module module) { public SourceOutput(int id, String name, @Nullable Module module) {
super(id, name, module); super(id, name, module);
} }
public Source getSource() { public @Nullable Source getSource() {
return source; return source;
} }
public void setSource(Source source) { public void setSource(@Nullable Source source) {
this.source = source; this.source = source;
} }
} }