[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;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
@ -55,14 +56,13 @@ public class ConvertedInputStream extends InputStream {
public ConvertedInputStream(AudioStream innerInputStream)
throws UnsupportedAudioFormatException, UnsupportedAudioFileException, IOException {
this.audioFormat = innerInputStream.getFormat();
if (innerInputStream instanceof FixedLengthAudioStream) {
length = ((FixedLengthAudioStream) innerInputStream).length();
}
pcmNormalizedInputStream = getPCMStreamNormalized(getPCMStream(new ResetableInputStream(innerInputStream)));
pcmNormalizedInputStream = getPCMStreamNormalized(getPCMStream(new BufferedInputStream(innerInputStream)));
}
@Override
@ -108,7 +108,6 @@ public class ConvertedInputStream extends InputStream {
* @return A PCM normalized stream (2 channel, 44100hz, 16 bit signed)
*/
private AudioInputStream getPCMStreamNormalized(AudioInputStream pcmInputStream) {
javax.sound.sampled.AudioFormat format = pcmInputStream.getFormat();
if (format.getChannels() != 2
|| !format.getEncoding().equals(javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED)
@ -138,7 +137,6 @@ public class ConvertedInputStream extends InputStream {
*/
private AudioInputStream getPCMStream(InputStream resetableInnerInputStream)
throws UnsupportedAudioFileException, IOException, UnsupportedAudioFormatException {
if (AudioFormat.MP3.isCompatible(audioFormat)) {
MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader();
@ -170,7 +168,6 @@ public class ConvertedInputStream extends InputStream {
sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false);
return mpegconverter.getAudioInputStream(convertFormat, sourceAIS);
} else if (AudioFormat.WAV.isCompatible(audioFormat)) {
// return the same input stream, but try to compute the duration first
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(resetableInnerInputStream);
@ -187,71 +184,4 @@ public class ConvertedInputStream extends InputStream {
audioFormat);
}
}
/**
* This class add reset capability (on the first bytes only)
* to an AudioStream. This is necessary for the parsing / format detection.
*
*/
public static class ResetableInputStream extends InputStream {
private static final int BUFFER_LENGTH = 10000;
private final InputStream originalInputStream;
private int position = -1;
private int markPosition = -1;
private int maxPreviousPosition = -2;
private byte[] startingBuffer = new byte[BUFFER_LENGTH + 1];
public ResetableInputStream(InputStream originalInputStream) {
this.originalInputStream = originalInputStream;
}
@Override
public void close() throws IOException {
originalInputStream.close();
}
@Override
public int read() throws IOException {
if (position >= BUFFER_LENGTH || originalInputStream.markSupported()) {
return originalInputStream.read();
} else {
position++;
if (position <= maxPreviousPosition) {
return Byte.toUnsignedInt(startingBuffer[position]);
} else {
int currentByte = originalInputStream.read();
startingBuffer[position] = (byte) currentByte;
maxPreviousPosition = position;
return currentByte;
}
}
}
@Override
public synchronized void mark(int readlimit) {
if (originalInputStream.markSupported()) {
originalInputStream.mark(readlimit);
}
markPosition = position;
}
@Override
public boolean markSupported() {
return true;
}
@Override
public synchronized void reset() throws IOException {
if (originalInputStream.markSupported()) {
originalInputStream.reset();
} else if (position >= BUFFER_LENGTH) {
throw new IOException("mark/reset not supported above " + BUFFER_LENGTH + " bytes");
}
position = markPosition;
}
}
}

View File

@ -91,7 +91,9 @@ public class PulseAudioAudioSink extends PulseaudioSimpleProtocolStream implemen
} catch (IOException e) {
disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown
if (countAttempt == 2) { // we won't retry : log and quit
String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown";
final Socket clientSocketLocal = clientSocket;
String port = clientSocketLocal != null ? Integer.toString(clientSocketLocal.getPort())
: "unknown";
logger.warn(
"Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
pulseaudioHandler.getHost(), port, e.getMessage());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,9 @@ package org.openhab.binding.pulseaudio.internal.discovery;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
import org.openhab.binding.pulseaudio.internal.handler.DeviceStatusListener;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler;
@ -28,7 +30,7 @@ import org.openhab.binding.pulseaudio.internal.items.SourceOutput;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
@ -40,6 +42,7 @@ import org.slf4j.LoggerFactory;
*
* @author Tobias Bräutigam - Initial contribution
*/
@NonNullByDefault
public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService implements DeviceStatusListener {
private final Logger logger = LoggerFactory.getLogger(PulseaudioDeviceDiscoveryService.class);
@ -66,7 +69,11 @@ public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService i
}
@Override
public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) {
public void onDeviceAdded(Thing bridge, AbstractAudioDeviceConfig device) {
if (getAlreadyConfiguredThings().contains(device.getPaName())) {
return;
}
String uidName = device.getPaName();
logger.debug("device {} found", device);
ThingTypeUID thingType = null;
@ -97,18 +104,14 @@ public class PulseaudioDeviceDiscoveryService extends AbstractDiscoveryService i
}
}
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
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
}
@Override
public void onDeviceRemoved(PulseaudioBridgeHandler bridge, AbstractAudioDeviceConfig device) {
// this can be ignored here
pulseaudioBridgeHandler.resetKnownActiveDevices();
}
}

View File

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

View File

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

View File

@ -24,6 +24,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.pulseaudio.internal.PulseAudioBindingConfiguration;
import org.openhab.binding.pulseaudio.internal.PulseAudioBindingConfigurationListener;
@ -33,10 +34,12 @@ import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
@ -47,8 +50,10 @@ import org.slf4j.LoggerFactory;
* connects it to the framework.
*
* @author Tobias Bräutigam - Initial contribution
* @author Gwendal Roulleau - Rewrite for child handler notification
*
*/
@NonNullByDefault
public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseAudioBindingConfigurationListener {
private final Logger logger = LoggerFactory.getLogger(PulseaudioBridgeHandler.class);
@ -60,22 +65,22 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
public int refreshInterval = 30000;
@Nullable
private PulseaudioClient client;
private PulseAudioBindingConfiguration configuration;
private List<DeviceStatusListener> deviceStatusListeners = new CopyOnWriteArrayList<>();
private HashSet<String> lastActiveDevices = new HashSet<>();
private Set<String> lastActiveDevices = new HashSet<>();
@Nullable
private ScheduledFuture<?> pollingJob;
private synchronized void update() {
private Set<PulseaudioHandler> childHandlersInitialized = new HashSet<>();
public synchronized void update() {
try {
client.connect();
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
logger.debug("Established connection to Pulseaudio server on Host '{}':'{}'.", host, port);
}
getClient().connect();
} catch (IOException e) {
logger.debug("{}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
@ -84,21 +89,23 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
return;
}
client.update();
for (AbstractAudioDeviceConfig device : client.getItems()) {
if (lastActiveDevices != null && lastActiveDevices.contains(device.getPaName())) {
for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
try {
deviceStatusListener.onDeviceStateChanged(getThing().getUID(), device);
} catch (Exception e) {
logger.warn("An exception occurred while calling the DeviceStatusListener", e);
}
}
getClient().update();
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
logger.debug("Established connection to Pulseaudio server on Host '{}':'{}'.", host, port);
// The framework will automatically notify the child handlers as the bridge status is changed
} else {
// browse all child handlers to update status according to the result of the query to the pulse audio server
for (PulseaudioHandler pulseaudioHandler : childHandlersInitialized) {
pulseaudioHandler.deviceUpdate(getDevice(pulseaudioHandler.getName()));
}
}
// browse query result to notify add event
for (AbstractAudioDeviceConfig device : getClient().getItems()) {
if (!lastActiveDevices.contains(device.getPaName())) {
for (DeviceStatusListener deviceStatusListener : deviceStatusListeners) {
try {
deviceStatusListener.onDeviceAdded(getThing(), device);
deviceStatusListener.onDeviceStateChanged(getThing().getUID(), device);
} catch (Exception e) {
logger.warn("An exception occurred while calling the DeviceStatusListener", e);
}
@ -116,18 +123,22 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
client.update();
getClient().update();
} else {
logger.debug("received unexpected command for pulseaudio bridge '{}'.", host);
}
}
public @Nullable AbstractAudioDeviceConfig getDevice(String name) {
return client.getGenericAudioItem(name);
return getClient().getGenericAudioItem(name);
}
public PulseaudioClient getClient() {
return client;
PulseaudioClient clientFinal = client;
if (clientFinal == null) {
throw new AssertionError("PulseaudioClient is null !");
}
return clientFinal;
}
@Override
@ -145,10 +156,11 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
this.refreshInterval = ((BigDecimal) conf.get(BRIDGE_PARAMETER_REFRESH_INTERVAL)).intValue();
}
if (host != null && !host.isEmpty()) {
if (!host.isBlank()) {
client = new PulseaudioClient(host, port, configuration);
updateStatus(ThingStatus.UNKNOWN);
if (pollingJob == null || pollingJob.isCancelled()) {
final ScheduledFuture<?> pollingJobFinal = pollingJob;
if (pollingJobFinal == null || pollingJobFinal.isCancelled()) {
pollingJob = scheduler.scheduleWithFixedDelay(this::update, 0, refreshInterval, TimeUnit.MILLISECONDS);
}
} else {
@ -168,16 +180,14 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
job.cancel(true);
pollingJob = null;
}
if (client != null) {
client.disconnect();
var clientFinal = client;
if (clientFinal != null) {
clientFinal.disconnect();
}
super.dispose();
}
public boolean registerDeviceStatusListener(DeviceStatusListener deviceStatusListener) {
if (deviceStatusListener == null) {
throw new IllegalArgumentException("It's not allowed to pass a null deviceStatusListener.");
}
return deviceStatusListeners.add(deviceStatusListener);
}
@ -187,6 +197,33 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
@Override
public void bindingConfigurationChanged() {
// 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.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -53,7 +52,6 @@ import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
@ -73,17 +71,14 @@ import org.slf4j.LoggerFactory;
* @author Miguel Álvarez - Register audio source and refactor
*/
@NonNullByDefault
public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusListener {
public class PulseaudioHandler extends BaseThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(SINK_THING_TYPE, COMBINED_SINK_THING_TYPE, SINK_INPUT_THING_TYPE,
SOURCE_THING_TYPE, SOURCE_OUTPUT_THING_TYPE).collect(Collectors.toSet()));
private final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class);
private final int refresh = 60; // refresh every minute as default
private @Nullable PulseaudioBridgeHandler bridgeHandler;
private @Nullable String name;
private @Nullable ScheduledFuture<?> refreshJob;
private String name = "";
private @Nullable PulseAudioAudioSink audioSink;
private @Nullable PulseAudioAudioSource audioSource;
private @Nullable Integer savedVolume;
@ -102,37 +97,32 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
public void initialize() {
Configuration config = getThing().getConfiguration();
name = (String) config.get(DEVICE_PARAMETER_NAME);
initializeWithTheBridge();
}
updateStatus(ThingStatus.UNKNOWN);
deviceOnlineWatchdog();
// if it's a SINK thing, then maybe we have to activate the audio sink
if (SINK_THING_TYPE.equals(thing.getThingTypeUID())) {
// check the property to see if we it's enabled :
Boolean sinkActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION);
if (sinkActivated != null && sinkActivated) {
audioSinkSetup();
}
}
// if it's a SOURCE thing, then maybe we have to activate the audio source
if (SOURCE_THING_TYPE.equals(thing.getThingTypeUID())) {
// check the property to see if we it's enabled :
Boolean sourceActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_ACTIVATION);
if (sourceActivated != null && sourceActivated) {
audioSourceSetup();
}
}
public String getName() {
return name;
}
private void audioSinkSetup() {
if (audioSink != null) {
// Audio sink is already setup
return;
}
if (!SINK_THING_TYPE.equals(thing.getThingTypeUID())) {
return;
}
// check the property to see if it's enabled :
Boolean sinkActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION);
if (sinkActivated == null || !sinkActivated.booleanValue()) {
return;
}
final PulseaudioHandler thisHandler = this;
PulseAudioAudioSink audioSink = new PulseAudioAudioSink(thisHandler, scheduler);
scheduler.submit(new Runnable() {
@Override
public void run() {
// Register the sink as an audio sink in openhab
logger.trace("Registering an audio sink for pulse audio sink thing {}", thing.getUID());
PulseAudioAudioSink audioSink = new PulseAudioAudioSink(thisHandler, scheduler);
setAudioSink(audioSink);
PulseaudioHandler.this.audioSink = audioSink;
try {
audioSink.connectIfNeeded();
} catch (IOException e) {
@ -144,23 +134,49 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
} finally {
audioSink.scheduleDisconnect();
}
}
});
// Register the sink as an audio sink in openhab
logger.trace("Registering an audio sink for pulse audio sink thing {}", thing.getUID());
@SuppressWarnings("unchecked")
ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
.registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
audioSinkRegistrations.put(thing.getUID().toString(), reg);
}
});
private void audioSinkUnsetup() {
PulseAudioAudioSink sink = audioSink;
if (sink != null) {
sink.disconnect();
audioSink = null;
}
// Unregister the potential pulse audio sink's audio sink
ServiceRegistration<AudioSink> sinkReg = audioSinkRegistrations.remove(getThing().getUID().toString());
if (sinkReg != null) {
logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID());
sinkReg.unregister();
}
}
private void audioSourceSetup() {
if (audioSource != null) {
// Audio source is already setup
return;
}
if (!SOURCE_THING_TYPE.equals(thing.getThingTypeUID())) {
return;
}
// check the property to see if it's enabled :
Boolean sourceActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_ACTIVATION);
if (sourceActivated == null || !sourceActivated.booleanValue()) {
return;
}
final PulseaudioHandler thisHandler = this;
PulseAudioAudioSource audioSource = new PulseAudioAudioSource(thisHandler, scheduler);
scheduler.submit(new Runnable() {
@Override
public void run() {
// Register the source as an audio source in openhab
logger.trace("Registering an audio source for pulse audio source thing {}", thing.getUID());
PulseAudioAudioSource audioSource = new PulseAudioAudioSource(thisHandler, scheduler);
setAudioSource(audioSource);
PulseaudioHandler.this.audioSource = audioSource;
try {
audioSource.connectIfNeeded();
} catch (IOException e) {
@ -172,41 +188,21 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
} finally {
audioSource.scheduleDisconnect();
}
}
});
// Register the source as an audio source in openhab
logger.trace("Registering an audio source for pulse audio source thing {}", thing.getUID());
@SuppressWarnings("unchecked")
ServiceRegistration<AudioSource> reg = (ServiceRegistration<AudioSource>) bundleContext
.registerService(AudioSource.class.getName(), audioSource, new Hashtable<>());
audioSourceRegistrations.put(thing.getUID().toString(), reg);
}
});
}
@Override
public void dispose() {
ScheduledFuture<?> job = refreshJob;
if (job != null && !job.isCancelled()) {
job.cancel(true);
refreshJob = null;
}
PulseaudioBridgeHandler briHandler = bridgeHandler;
if (briHandler != null) {
briHandler.unregisterDeviceStatusListener(this);
bridgeHandler = null;
}
logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
super.dispose();
PulseAudioAudioSink sink = audioSink;
if (sink != null) {
sink.disconnect();
}
private void audioSourceUnsetup() {
PulseAudioAudioSource source = audioSource;
if (source != null) {
source.disconnect();
}
// Unregister the potential pulse audio sink's audio sink
ServiceRegistration<AudioSink> sinkReg = audioSinkRegistrations.remove(getThing().getUID().toString());
if (sinkReg != null) {
logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID());
sinkReg.unregister();
audioSource = null;
}
// Unregister the potential pulse audio source's audio sources
ServiceRegistration<AudioSource> sourceReg = audioSourceRegistrations.remove(getThing().getUID().toString());
@ -216,54 +212,31 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
}
}
@Override
public void dispose() {
logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
super.dispose();
audioSinkUnsetup();
audioSourceUnsetup();
}
@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);
}
initializeWithTheBridge();
}
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);
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));
}
} catch (Exception e) {
logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
this.bridgeHandler = null;
}
};
refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refresh, TimeUnit.SECONDS);
}
private synchronized @Nullable PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
if (this.bridgeHandler == null) {
Bridge bridge = getBridge();
if (bridge == null) {
logger.debug("Required bridge not defined for device {}.", name);
@ -271,15 +244,12 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
}
ThingHandler handler = bridge.getHandler();
if (handler instanceof PulseaudioBridgeHandler) {
this.bridgeHandler = (PulseaudioBridgeHandler) handler;
this.bridgeHandler.registerDeviceStatusListener(this);
return (PulseaudioBridgeHandler) handler;
} else {
logger.debug("No available bridge handler found for device {} bridge {} .", name, bridge.getUID());
return null;
}
}
return this.bridgeHandler;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
@ -296,8 +266,7 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
AbstractAudioDeviceConfig device = briHandler.getDevice(name);
if (device == null) {
logger.warn("device {} not found", name);
updateStatus(ThingStatus.OFFLINE);
bridgeHandler = null;
deviceUpdate(null);
return;
} else {
State updateState = UnDefType.UNDEF;
@ -384,19 +353,20 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
*
* @return
*/
public int getLastVolume() {
if (savedVolume == null) {
public Integer getLastVolume() {
Integer savedVolumeFinal = savedVolume;
if (savedVolumeFinal == null) {
PulseaudioBridgeHandler briHandler = getPulseaudioBridgeHandler();
if (briHandler != null) {
// refresh to get the current volume level
briHandler.getClient().update();
AbstractAudioDeviceConfig device = briHandler.getDevice(name);
if (device != null) {
savedVolume = device.getVolume();
savedVolume = savedVolumeFinal = device.getVolume();
}
}
}
return savedVolume == null ? 50 : savedVolume;
return savedVolumeFinal == null ? 50 : savedVolumeFinal;
}
public void setVolume(int volume) {
@ -415,25 +385,38 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
savedVolume = volume;
}
@Override
public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) {
if (device.getPaName().equals(name)) {
updateStatus(ThingStatus.ONLINE);
public void deviceUpdate(@Nullable AbstractAudioDeviceConfig device) {
if (device != null && device.getPaName().equals(name)) {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL);
savedVolume = device.getVolume();
updateState(VOLUME_CHANNEL, new PercentType(savedVolume));
updateState(MUTE_CHANNEL, device.isMuted() ? OnOffType.ON : OnOffType.OFF);
updateState(STATE_CHANNEL,
device.getState() != null ? new StringType(device.getState().toString()) : new StringType("-"));
int actualVolume = device.getVolume();
savedVolume = actualVolume;
updateState(VOLUME_CHANNEL, new PercentType(actualVolume));
updateState(MUTE_CHANNEL, OnOffType.from(device.isMuted()));
org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig.State state = device.getState();
updateState(STATE_CHANNEL, state != null ? new StringType(state.toString()) : new StringType("-"));
if (device instanceof SinkInput) {
updateState(ROUTE_TO_SINK_CHANNEL,
((SinkInput) device).getSink() != null
? new StringType(((SinkInput) device).getSink().getPaName())
: new StringType("-"));
updateState(ROUTE_TO_SINK_CHANNEL, new StringType(
Optional.ofNullable(((SinkInput) device).getSink()).map(Sink::getPaName).orElse("-")));
}
if (device instanceof Sink && ((Sink) device).isCombinedSink()) {
updateState(SLAVES_CHANNEL, new StringType(String.join(",", ((Sink) device).getCombinedSinkNames())));
}
audioSinkSetup();
audioSourceSetup();
} else if (device == null) {
updateState(VOLUME_CHANNEL, UnDefType.UNDEF);
updateState(MUTE_CHANNEL, UnDefType.UNDEF);
updateState(STATE_CHANNEL, UnDefType.UNDEF);
if (SINK_INPUT_THING_TYPE.equals(thing.getThingTypeUID())) {
updateState(ROUTE_TO_SINK_CHANNEL, UnDefType.UNDEF);
}
if (COMBINED_SINK_THING_TYPE.equals(thing.getThingTypeUID())) {
updateState(SLAVES_CHANNEL, UnDefType.UNDEF);
}
audioSinkUnsetup();
audioSourceUnsetup();
updateStatus(ThingStatus.OFFLINE);
}
}
@ -448,14 +431,14 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
}
/**
* This method will scan the pulseaudio server to find the port on which the module/sink is listening
* This method will scan the pulseaudio server to find the port on which the module/sink/source is listening
* If no module is listening, then it will command the module to load on the pulse audio server,
*
* @return the port on which the pulseaudio server is listening for this sink
* @return the port on which the pulseaudio server is listening for this sink/source
* @throws IOException when device info is not available
* @throws InterruptedException when interrupted during the loading module wait
*/
public int getSimpleTcpPort() throws IOException, InterruptedException {
public int getSimpleTcpPortAndLoadModuleIfNecessary() throws IOException, InterruptedException {
var briHandler = getPulseaudioBridgeHandler();
if (briHandler == null) {
throw new IOException("bridge is not ready");
@ -515,43 +498,22 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
}
public int getIdleTimeout() {
var idleTimeout = 3000;
var handler = getPulseaudioBridgeHandler();
if (handler == null) {
return 30000;
}
if (handler != null) {
AbstractAudioDeviceConfig device = handler.getDevice(name);
String idleTimeoutPropName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT
: DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT;
var idleTimeout = (BigDecimal) getThing().getConfiguration().get(idleTimeoutPropName);
return idleTimeout != null ? idleTimeout.intValue() : 30000;
var idleTimeoutB = (BigDecimal) getThing().getConfiguration().get(idleTimeoutPropName);
if (idleTimeoutB != null) {
idleTimeout = idleTimeoutB.intValue();
}
}
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;
}
}

View File

@ -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 abstract class AbstractAudioDeviceConfig extends AbstractDeviceConfig {
DRAINED
}
protected State state;
protected @Nullable State state;
protected boolean muted;
protected int volume;
protected Module module;
protected @Nullable Module module;
public AbstractAudioDeviceConfig(int id, String name, Module module) {
public AbstractAudioDeviceConfig(int id, String name, @Nullable Module module) {
super(id, name);
this.module = module;
}
public Module getModule() {
public @Nullable Module getModule() {
return module;
}
public void setModule(Module module) {
this.module = module;
}
public State getState() {
public @Nullable State getState() {
return state;
}

View File

@ -12,6 +12,8 @@
*/
package org.openhab.binding.pulseaudio.internal.items;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Abstract root class for all items in an pulseaudio server. Every item in a
* pulseaudio server has a name and a unique id which can be inherited by this
@ -19,6 +21,7 @@ package org.openhab.binding.pulseaudio.internal.items;
*
* @author Tobias Bräutigam - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractDeviceConfig {
protected int id;

View File

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

View File

@ -15,6 +15,9 @@ package org.openhab.binding.pulseaudio.internal.items;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* On a Pulseaudio server Sinks are the devices the audio streams are routed to
* (playback devices) it can be a single item or a group of other Sinks that are
@ -22,12 +25,13 @@ import java.util.List;
*
* @author Tobias Bräutigam - Initial contribution
*/
@NonNullByDefault
public class Sink extends AbstractAudioDeviceConfig {
protected List<String> combinedSinkNames;
protected List<Sink> combinedSinks;
public Sink(int id, String name, Module module) {
public Sink(int id, String name, @Nullable Module module) {
super(id, name, module);
combinedSinkNames = new ArrayList<>();
combinedSinks = new ArrayList<>();
@ -53,7 +57,7 @@ public class Sink extends AbstractAudioDeviceConfig {
this.combinedSinks = combinedSinks;
}
public void addCombinedSink(Sink sink) {
public void addCombinedSink(@Nullable Sink sink) {
if (sink != null) {
this.combinedSinks.add(sink);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}