[pulseaudio] register audio sources in openhab (#12376)

* [pulseaudio] register audio sources in openha
* [pulseaudio] fix audio source reconnection
* [pulseaudio] audio source check record property and customize SOTimeout
* [pulseaudio] use pipe streams
* [pulseaudio] synchronize commands and update after module load

Signed-off-by: Miguel Álvarez Díez <miguelwork92@gmail.com>
This commit is contained in:
GiviMAD 2022-03-03 13:01:07 +01:00 committed by GitHub
parent c5a2a1fbdf
commit 094cb7f12d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 696 additions and 153 deletions

View File

@ -20,7 +20,7 @@ The Pulseaudio bridge is discovered through mDNS in the local network.
## Binding Configuration (optional)
The Pulseaudio binding can be customized to handle different devices. The Sink support is activated by default and you need no further action to use it. If you want to use another type of device, or disable the Sink type, you have to switch the corresponding binding property.
The Pulseaudio binding can be customized to handle different devices. The Sink and Source support is activated by default and you need no further action to use it. If you want to use another type of device, or disable the Sink/Source type, you have to switch the corresponding binding property.
- **sink:** Allow the binding to parse sink devices from the pulseaudio server
- **source:** Allow the binding to parse source devices from the pulseaudio server
@ -31,7 +31,7 @@ You can use the GUI on the bindings page (click on the pulseaudio binding then "
```
binding.pulseaudio:sink=true
binding.pulseaudio:source=false
binding.pulseaudio:source=true
binding.pulseaudio:sinkInput=false
binding.pulseaudio:sourceOutput=false
```
@ -59,6 +59,14 @@ Sink things can register themselves as audio sink in openHAB. MP3 and WAV files
Use the appropriate parameter in the sink thing to activate this possibility (activateSimpleProtocolSink).
This requires the module **module-simple-protocol-tcp** to be present on the server which runs your openHAB instance. The binding will try to command (if not discovered first) the load of this module on the pulseaudio server.
## Audio source
Source things can register themselves as audio source in openHAB.
WAV input format, rate and channels can be configured on the thing configuration. (defaults to pcm_signed,16000,1)
Use the appropriate parameter in the source thing to activate this possibility (activateSimpleProtocolSource).
This requires the module **module-simple-protocol-tcp** to be present on the target pulseaudio server. The binding will load this module on the pulseaudio server.
## Full Example
### pulseaudio.things

View File

@ -17,11 +17,8 @@ import java.net.Socket;
import java.time.Duration;
import java.time.Instant;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.sound.sampled.UnsupportedAudioFileException;
@ -34,7 +31,6 @@ import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.FixedLengthAudioStream;
import org.openhab.core.audio.UnsupportedAudioFormatException;
import org.openhab.core.audio.UnsupportedAudioStreamException;
import org.openhab.core.library.types.PercentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -42,25 +38,18 @@ import org.slf4j.LoggerFactory;
* The audio sink for openhab, implemented by a connection to a pulseaudio sink
*
* @author Gwendal Roulleau - Initial contribution
* @author Miguel Álvarez - move some code to the PulseaudioSimpleProtocolStream class so sink and source can extend
* from it.
*
*/
@NonNullByDefault
public class PulseAudioAudioSink implements AudioSink {
public class PulseAudioAudioSink extends PulseaudioSimpleProtocolStream implements AudioSink {
private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSink.class);
private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
private PulseaudioHandler pulseaudioHandler;
private ScheduledExecutorService scheduler;
private @Nullable Socket clientSocket;
private boolean isIdle = true;
private @Nullable ScheduledFuture<?> scheduledDisconnection;
static {
SUPPORTED_FORMATS.add(AudioFormat.WAV);
SUPPORTED_FORMATS.add(AudioFormat.MP3);
@ -68,60 +57,15 @@ public class PulseAudioAudioSink implements AudioSink {
}
public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) {
this.pulseaudioHandler = pulseaudioHandler;
this.scheduler = scheduler;
}
@Override
public String getId() {
return pulseaudioHandler.getThing().getUID().toString();
}
@Override
public @Nullable String getLabel(@Nullable Locale locale) {
return pulseaudioHandler.getThing().getLabel();
}
/**
* Connect to pulseaudio with the simple protocol
*
* @throws IOException
* @throws InterruptedException when interrupted during the loading module wait
*/
public void connectIfNeeded() throws IOException, InterruptedException {
Socket clientSocketLocal = clientSocket;
if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) {
String host = pulseaudioHandler.getHost();
int port = pulseaudioHandler.getSimpleTcpPort();
clientSocket = new Socket(host, port);
clientSocket.setSoTimeout(500);
}
}
/**
* Disconnect the socket to pulseaudio simple protocol
*/
public void disconnect() {
final Socket clientSocketLocal = clientSocket;
if (clientSocketLocal != null && isIdle) {
logger.debug("Disconnecting");
try {
clientSocketLocal.close();
} catch (IOException e) {
}
} else {
logger.debug("Stream still running or socket not open");
}
super(pulseaudioHandler, scheduler);
}
@Override
public void process(@Nullable AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
if (audioStream == null) {
return;
}
try (ConvertedInputStream normalizedPCMStream = new ConvertedInputStream(audioStream)) {
for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
try {
@ -129,7 +73,7 @@ public class PulseAudioAudioSink implements AudioSink {
final Socket clientSocketLocal = clientSocket;
if (clientSocketLocal != null) {
// send raw audio to the socket and to pulse audio
isIdle = false;
setIdle(false);
Instant start = Instant.now();
normalizedPCMStream.transferTo(clientSocketLocal.getOutputStream());
if (normalizedPCMStream.getDuration() != -1) { // ensure, if the sound has a duration
@ -147,12 +91,10 @@ public class PulseAudioAudioSink implements AudioSink {
} catch (IOException e) {
disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown
if (countAttempt == 2) { // we won't retry : log and quit
if (logger.isWarnEnabled()) {
String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown";
logger.warn(
"Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
pulseaudioHandler.getHost(), port, e.getMessage());
}
String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown";
logger.warn(
"Error while trying to send audio to pulseaudio audio sink. Cannot connect to {}:{}, error: {}",
pulseaudioHandler.getHost(), port, e.getMessage());
break;
}
} catch (InterruptedException ie) {
@ -166,18 +108,7 @@ public class PulseAudioAudioSink implements AudioSink {
} finally {
scheduleDisconnect();
}
isIdle = true;
}
public void scheduleDisconnect() {
if (scheduledDisconnection != null) {
scheduledDisconnection.cancel(true);
}
int idleTimeout = pulseaudioHandler.getIdleTimeout();
if (idleTimeout > -1) {
logger.debug("Scheduling disconnect");
scheduledDisconnection = scheduler.schedule(this::disconnect, idleTimeout, TimeUnit.MILLISECONDS);
}
setIdle(true);
}
@Override
@ -189,14 +120,4 @@ public class PulseAudioAudioSink implements AudioSink {
public Set<Class<? extends AudioStream>> getSupportedStreams() {
return SUPPORTED_STREAMS;
}
@Override
public PercentType getVolume() {
return new PercentType(pulseaudioHandler.getLastVolume());
}
@Override
public void setVolume(PercentType volume) {
pulseaudioHandler.setVolume(volume.intValue());
}
}

View File

@ -0,0 +1,249 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal;
import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.Socket;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
import org.openhab.core.audio.AudioException;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioSource;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.common.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The audio source for openhab, implemented by a connection to a pulseaudio source using Simple TCP protocol
*
* @author Miguel Álvarez - Initial contribution
*
*/
@NonNullByDefault
public class PulseAudioAudioSource extends PulseaudioSimpleProtocolStream implements AudioSource {
private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSource.class);
private final Set<PipedOutputStream> pipeOutputs = new HashSet<>();
private final ScheduledExecutorService executor;
private @Nullable Future<?> pipeWriteTask;
public PulseAudioAudioSource(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) {
super(pulseaudioHandler, scheduler);
executor = ThreadPoolManager
.getScheduledPool("OH-binding-" + pulseaudioHandler.getThing().getUID() + "-source");
}
@Override
public Set<AudioFormat> getSupportedFormats() {
var supportedFormats = new HashSet<AudioFormat>();
var audioFormat = pulseaudioHandler.getSourceAudioFormat();
if (audioFormat != null) {
supportedFormats.add(audioFormat);
}
return supportedFormats;
}
@Override
public AudioStream getInputStream(AudioFormat audioFormat) throws AudioException {
try {
for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
try {
connectIfNeeded();
final Socket clientSocketLocal = clientSocket;
if (clientSocketLocal == null) {
break;
}
var sourceFormat = pulseaudioHandler.getSourceAudioFormat();
if (sourceFormat == null) {
throw new AudioException("Unable to get source audio format");
}
if (!audioFormat.isCompatible(sourceFormat)) {
throw new AudioException("Incompatible audio format requested");
}
setIdle(true);
var pipeOutput = new PipedOutputStream();
registerPipe(pipeOutput);
var pipeInput = new PipedInputStream(pipeOutput, 1024 * 20) {
@Override
public void close() throws IOException {
unregisterPipe(pipeOutput);
super.close();
}
};
// get raw audio from the pulse audio socket
return new PulseAudioStream(sourceFormat, pipeInput, (idle) -> {
setIdle(idle);
if (idle) {
scheduleDisconnect();
} else {
// ensure pipe is writing
startPipeWrite();
}
});
} catch (IOException e) {
disconnect(); // disconnect force to clear connection in case of socket not cleanly shutdown
if (countAttempt == 2) { // we won't retry : log and quit
String port = clientSocket != null ? Integer.toString(clientSocket.getPort()) : "unknown";
logger.warn(
"Error while trying to get audio from pulseaudio audio source. Cannot connect to {}:{}, error: {}",
pulseaudioHandler.getHost(), port, e.getMessage());
setIdle(true);
throw e;
}
} catch (InterruptedException ie) {
logger.info("Interrupted during source audio connection: {}", ie.getMessage());
setIdle(true);
throw new AudioException(ie);
}
countAttempt++;
}
} catch (IOException e) {
throw new AudioException(e);
} finally {
scheduleDisconnect();
}
setIdle(true);
throw new AudioException("Unable to create input stream");
}
private synchronized void registerPipe(PipedOutputStream pipeOutput) {
this.pipeOutputs.add(pipeOutput);
startPipeWrite();
}
private void startPipeWrite() {
if (pipeWriteTask == null) {
this.pipeWriteTask = executor.submit(() -> {
int lengthRead;
byte[] buffer = new byte[1024];
while (true) {
var stream = getSourceInputStream();
if (stream != null) {
try {
lengthRead = stream.read(buffer);
for (var output : pipeOutputs) {
output.write(buffer, 0, lengthRead);
output.flush();
}
} catch (IOException e) {
logger.warn("IOException while reading from pulse source: {}", e.getMessage());
} catch (RuntimeException e) {
logger.warn("RuntimeException while reading from pulse source: {}", e.getMessage());
}
} else {
logger.warn("Unable to get source input stream");
}
}
});
}
}
private synchronized void unregisterPipe(PipedOutputStream pipeOutput) {
this.pipeOutputs.remove(pipeOutput);
stopPipeWriteTask();
try {
pipeOutput.close();
} catch (IOException ignored) {
}
}
private void stopPipeWriteTask() {
var pipeWriteTask = this.pipeWriteTask;
if (pipeOutputs.isEmpty() && pipeWriteTask != null) {
pipeWriteTask.cancel(true);
this.pipeWriteTask = null;
}
}
private @Nullable InputStream getSourceInputStream() {
try {
connectIfNeeded();
} catch (IOException | InterruptedException ignored) {
}
try {
return (clientSocket != null) ? clientSocket.getInputStream() : null;
} catch (IOException ignored) {
return null;
}
}
@Override
public void disconnect() {
stopPipeWriteTask();
super.disconnect();
}
static class PulseAudioStream extends AudioStream {
private final Logger logger = LoggerFactory.getLogger(PulseAudioAudioSource.class);
private final AudioFormat format;
private final InputStream input;
private final Consumer<Boolean> setIdle;
private boolean closed = false;
public PulseAudioStream(AudioFormat format, InputStream input, Consumer<Boolean> setIdle) {
this.input = input;
this.format = format;
this.setIdle = setIdle;
}
@Override
public AudioFormat getFormat() {
return format;
}
@Override
public int read() throws IOException {
byte[] b = new byte[1];
int bytesRead = read(b);
if (-1 == bytesRead) {
return bytesRead;
}
Byte bb = Byte.valueOf(b[0]);
return bb.intValue();
}
@Override
public int read(byte @Nullable [] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public int read(byte @Nullable [] b, int off, int len) throws IOException {
logger.trace("reading from pulseaudio stream");
if (closed) {
throw new IOException("Stream is closed");
}
setIdle.accept(false);
return input.read(b, off, len);
}
@Override
public void close() throws IOException {
closed = true;
setIdle.accept(true);
input.close();
}
};
}

View File

@ -28,7 +28,7 @@ public class PulseAudioBindingConfiguration {
public boolean sink = true;
public boolean source = false;
public boolean source = true;
public boolean sinkInput = false;

View File

@ -20,6 +20,7 @@ import org.openhab.core.thing.ThingTypeUID;
* used across the whole binding.
*
* @author Tobias Bräutigam - Initial contribution
* @author Miguel Álvarez - Add new configuration options to sink and source
*/
@NonNullByDefault
public class PulseaudioBindingConstants {
@ -51,6 +52,13 @@ public class PulseaudioBindingConstants {
public static final String DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION = "activateSimpleProtocolSink";
public static final String DEVICE_PARAMETER_AUDIO_SINK_PORT = "simpleProtocolSinkPort";
public static final String DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT = "simpleProtocolSinkIdleTimeout";
public static final String DEVICE_PARAMETER_AUDIO_SOURCE_ACTIVATION = "activateSimpleProtocolSource";
public static final String DEVICE_PARAMETER_AUDIO_SOURCE_PORT = "simpleProtocolSourcePort";
public static final String DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT = "simpleProtocolSourceIdleTimeout";
public static final String DEVICE_PARAMETER_AUDIO_SOURCE_RATE = "simpleProtocolSourceRate";
public static final String DEVICE_PARAMETER_AUDIO_SOURCE_FORMAT = "simpleProtocolSourceFormat";
public static final String DEVICE_PARAMETER_AUDIO_SOURCE_CHANNELS = "simpleProtocolSourceChannels";
public static final String DEVICE_PARAMETER_AUDIO_SOCKET_SO_TIMEOUT = "simpleProtocolSOTimeout";
public static final String MODULE_SIMPLE_PROTOCOL_TCP_NAME = "module-simple-protocol-tcp";
public static final int MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT = 4711;

View File

@ -17,6 +17,7 @@ import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.math.BigDecimal;
import java.net.NoRouteToHostException;
import java.net.Socket;
import java.net.SocketException;
@ -28,6 +29,8 @@ import java.util.Optional;
import java.util.Random;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.pulseaudio.internal.cli.Parser;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig.State;
@ -47,14 +50,16 @@ import org.slf4j.LoggerFactory;
* On the pulseaudio server the module-cli-protocol-tcp has to be loaded.
*
* @author Tobias Bräutigam - Initial contribution
* @author Miguel Álvarez - changes for loading audio source module and nullability annotations
*/
@NonNullByDefault
public class PulseaudioClient {
private final Logger logger = LoggerFactory.getLogger(PulseaudioClient.class);
private String host;
private int port;
private Socket client;
private @Nullable Socket client;
private List<AbstractAudioDeviceConfig> items;
private List<Module> modules;
@ -195,7 +200,7 @@ public class PulseaudioClient {
* @param id
* @return the corresponding {@link Module} to the given <code>id</code>
*/
public Module getModule(int id) {
public @Nullable Module getModule(int id) {
for (Module module : modules) {
if (module.getId() == id) {
return module;
@ -220,7 +225,7 @@ public class PulseaudioClient {
*
* @return the corresponding {@link Sink} to the given <code>name</code>
*/
public Sink getSink(String name) {
public @Nullable Sink getSink(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getPaName().equalsIgnoreCase(name) && item instanceof Sink) {
return (Sink) item;
@ -234,7 +239,7 @@ public class PulseaudioClient {
*
* @return the corresponding {@link Sink} to the given <code>id</code>
*/
public Sink getSink(int id) {
public @Nullable Sink getSink(int id) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getId() == id && item instanceof Sink) {
return (Sink) item;
@ -248,7 +253,7 @@ public class PulseaudioClient {
*
* @return the corresponding {@link SinkInput} to the given <code>name</code>
*/
public SinkInput getSinkInput(String name) {
public @Nullable SinkInput getSinkInput(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getPaName().equalsIgnoreCase(name) && item instanceof SinkInput) {
return (SinkInput) item;
@ -262,7 +267,7 @@ public class PulseaudioClient {
*
* @return the corresponding {@link SinkInput} to the given <code>id</code>
*/
public SinkInput getSinkInput(int id) {
public @Nullable SinkInput getSinkInput(int id) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getId() == id && item instanceof SinkInput) {
return (SinkInput) item;
@ -276,7 +281,7 @@ public class PulseaudioClient {
*
* @return the corresponding {@link Source} to the given <code>name</code>
*/
public Source getSource(String name) {
public @Nullable Source getSource(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getPaName().equalsIgnoreCase(name) && item instanceof Source) {
return (Source) item;
@ -290,7 +295,7 @@ public class PulseaudioClient {
*
* @return the corresponding {@link Source} to the given <code>id</code>
*/
public Source getSource(int id) {
public @Nullable Source getSource(int id) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getId() == id && item instanceof Source) {
return (Source) item;
@ -304,7 +309,7 @@ public class PulseaudioClient {
*
* @return the corresponding {@link SourceOutput} to the given <code>name</code>
*/
public SourceOutput getSourceOutput(String name) {
public @Nullable SourceOutput getSourceOutput(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getPaName().equalsIgnoreCase(name) && item instanceof SourceOutput) {
return (SourceOutput) item;
@ -318,7 +323,7 @@ public class PulseaudioClient {
*
* @return the corresponding {@link SourceOutput} to the given <code>id</code>
*/
public SourceOutput getSourceOutput(int id) {
public @Nullable SourceOutput getSourceOutput(int id) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getId() == id && item instanceof SourceOutput) {
return (SourceOutput) item;
@ -332,7 +337,7 @@ public class PulseaudioClient {
*
* @return the corresponding {@link AbstractAudioDeviceConfig} to the given <code>name</code>
*/
public AbstractAudioDeviceConfig getGenericAudioItem(String name) {
public @Nullable AbstractAudioDeviceConfig getGenericAudioItem(String name) {
for (AbstractAudioDeviceConfig item : items) {
if (item.getPaName().equalsIgnoreCase(name)) {
return item;
@ -351,7 +356,7 @@ public class PulseaudioClient {
* @param item the {@link Sink} to handle
* @param mute mutes the sink if true, unmutes if false
*/
public void setMute(AbstractAudioDeviceConfig item, boolean mute) {
public void setMute(@Nullable AbstractAudioDeviceConfig item, boolean mute) {
if (item == null) {
return;
}
@ -395,20 +400,27 @@ public class PulseaudioClient {
* @throws InterruptedException
*/
public Optional<Integer> loadModuleSimpleProtocolTcpIfNeeded(AbstractAudioDeviceConfig item,
Integer simpleTcpPortPref) throws InterruptedException {
Integer simpleTcpPortPref, @Nullable String format, @Nullable BigDecimal rate,
@Nullable BigDecimal channels) throws InterruptedException {
int currentTry = 0;
int simpleTcpPortToTry = simpleTcpPortPref;
String itemType = getItemCommandName(item);
do {
Optional<Integer> simplePort = findSimpleProtocolTcpModule(item);
Optional<Integer> simplePort = findSimpleProtocolTcpModule(item, format, rate, channels);
if (simplePort.isPresent()) {
return simplePort;
} else {
sendRawCommand("load-module module-simple-protocol-tcp sink=" + item.getPaName() + " port="
+ simpleTcpPortToTry);
String moduleOptions = itemType + "=" + item.getPaName() + " port=" + simpleTcpPortToTry;
if (item instanceof Source && format != null && rate != null && channels != null) {
moduleOptions = moduleOptions + String.format(" record=true format=%s rate=%d channels=%d", format,
rate.longValue(), channels.intValue());
}
sendRawCommand("load-module module-simple-protocol-tcp " + moduleOptions);
simpleTcpPortToTry = new Random().nextInt(64512) + 1024; // a random port above 1024
}
Thread.sleep(100);
update();
currentTry++;
} while (currentTry < 3);
@ -424,14 +436,49 @@ public class PulseaudioClient {
* @param item
* @return
*/
private Optional<Integer> findSimpleProtocolTcpModule(AbstractAudioDeviceConfig item) {
update();
private Optional<Integer> findSimpleProtocolTcpModule(AbstractAudioDeviceConfig item, @Nullable String format,
@Nullable BigDecimal rate, @Nullable BigDecimal channels) {
String itemType = getItemCommandName(item);
if (itemType == null) {
return Optional.empty();
}
List<Module> modulesCopy = new ArrayList<Module>(modules);
var isSource = item instanceof Source;
return modulesCopy.stream() // iteration on modules
.filter(module -> MODULE_SIMPLE_PROTOCOL_TCP_NAME.equals(module.getPaName())) // filter on module name
.filter(module -> extractArgumentFromLine("sink", module.getArgument()) // extract sink in argument
.map(sinkName -> sinkName.equals(item.getPaName())).orElse(false)) // filter on sink name
.filter(module -> {
boolean nameMatch = extractArgumentFromLine(itemType, module.getArgument()) // extract sick|source
.map(name -> name.equals(item.getPaName())).orElse(false);
if (isSource && nameMatch) {
boolean recordStream = extractArgumentFromLine("record", module.getArgument())
.map("true"::equals).orElse(false);
if (!recordStream) {
return false;
}
if (format != null) {
boolean rateMatch = extractArgumentFromLine("format", module.getArgument())
.map(format::equals).orElse(false);
if (!rateMatch) {
return false;
}
}
if (rate != null) {
boolean rateMatch = extractArgumentFromLine("rate", module.getArgument())
.map(value -> Long.parseLong(value) == rate.longValue()).orElse(false);
if (!rateMatch) {
return false;
}
}
if (channels != null) {
boolean channelsMatch = extractArgumentFromLine("channels", module.getArgument())
.map(value -> Integer.parseInt(value) == channels.intValue()).orElse(false);
if (!channelsMatch) {
return false;
}
}
}
return nameMatch;
}) // filter on sink name
.findAny() // get a corresponding module
.map(module -> extractArgumentFromLine("port", module.getArgument())
.orElse(Integer.toString(MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT))) // get port
@ -458,7 +505,7 @@ public class PulseaudioClient {
* @param item
* @return
*/
private String getItemCommandName(AbstractAudioDeviceConfig item) {
private @Nullable String getItemCommandName(AbstractAudioDeviceConfig item) {
if (item instanceof Sink) {
return ITEM_SINK;
} else if (item instanceof Source) {
@ -478,7 +525,7 @@ public class PulseaudioClient {
* @param vol the new volume percent value the {@link AbstractAudioDeviceConfig} should be changed to (possible
* values from 0 - 100)
*/
public void setVolumePercent(AbstractAudioDeviceConfig item, int vol) {
public void setVolumePercent(@Nullable AbstractAudioDeviceConfig item, int vol) {
int volumeToSet = vol;
if (item == null) {
return;
@ -505,7 +552,7 @@ public class PulseaudioClient {
* @param combinedSink the combined sink which slaves should be changed
* @param sinks the list of new slaves
*/
public void setCombinedSinkSlaves(Sink combinedSink, List<Sink> sinks) {
public void setCombinedSinkSlaves(@Nullable Sink combinedSink, List<Sink> sinks) {
if (combinedSink == null || !combinedSink.isCombinedSink()) {
return;
}
@ -528,7 +575,7 @@ public class PulseaudioClient {
* @param sinkInput the sink-input to be rerouted
* @param sink the new sink the sink-input should be routed to
*/
public void moveSinkInput(SinkInput sinkInput, Sink sink) {
public void moveSinkInput(@Nullable SinkInput sinkInput, @Nullable Sink sink) {
if (sinkInput == null || sink == null) {
return;
}
@ -542,7 +589,7 @@ public class PulseaudioClient {
* @param sourceOutput the source-output to be rerouted
* @param source the new source the source-output should be routed to
*/
public void moveSourceOutput(SourceOutput sourceOutput, Source source) {
public void moveSourceOutput(@Nullable SourceOutput sourceOutput, @Nullable Source source) {
if (sourceOutput == null || source == null) {
return;
}
@ -556,7 +603,7 @@ public class PulseaudioClient {
* @param source the source which state should be changed
* @param suspend suspend it or not
*/
public void suspendSource(Source source, boolean suspend) {
public void suspendSource(@Nullable Source source, boolean suspend) {
if (source == null) {
return;
}
@ -577,7 +624,7 @@ public class PulseaudioClient {
* @param sink the sink which state should be changed
* @param suspend suspend it or not
*/
public void suspendSink(Sink sink, boolean suspend) {
public void suspendSink(@Nullable Sink sink, boolean suspend) {
if (sink == null) {
return;
}
@ -613,9 +660,9 @@ public class PulseaudioClient {
update();
}
private void sendRawCommand(String command) {
private synchronized void sendRawCommand(String command) {
checkConnection();
if (client != null) {
if (client != null && client.isConnected()) {
try {
PrintStream out = new PrintStream(client.getOutputStream(), true);
logger.trace("sending command {} to pa-server {}", command, host);
@ -632,7 +679,7 @@ public class PulseaudioClient {
logger.trace("_sendRawRequest({})", command);
checkConnection();
String result = "";
if (client != null) {
if (client != null && client.isConnected()) {
try {
PrintStream out = new PrintStream(client.getOutputStream(), true);
out.print(command + "\r\n");

View File

@ -0,0 +1,119 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pulseaudio.internal;
import java.io.IOException;
import java.net.Socket;
import java.util.Locale;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler;
import org.openhab.core.library.types.PercentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A connection to a pulseaudio Simple TCP Protocol
*
* @author Gwendal Roulleau - Initial contribution
* @author Miguel Álvarez - Refactor some code from PulseAudioAudioSink here
*
*/
@NonNullByDefault
public abstract class PulseaudioSimpleProtocolStream {
private final Logger logger = LoggerFactory.getLogger(PulseaudioSimpleProtocolStream.class);
protected PulseaudioHandler pulseaudioHandler;
protected ScheduledExecutorService scheduler;
protected @Nullable Socket clientSocket;
private boolean isIdle = true;
private @Nullable ScheduledFuture<?> scheduledDisconnection;
public PulseaudioSimpleProtocolStream(PulseaudioHandler pulseaudioHandler, ScheduledExecutorService scheduler) {
this.pulseaudioHandler = pulseaudioHandler;
this.scheduler = scheduler;
}
/**
* Connect to pulseaudio with the simple protocol
*
* @throws IOException
* @throws InterruptedException when interrupted during the loading module wait
*/
public void connectIfNeeded() throws IOException, InterruptedException {
Socket clientSocketLocal = clientSocket;
if (clientSocketLocal == null || !clientSocketLocal.isConnected() || clientSocketLocal.isClosed()) {
logger.debug("Simple TCP Stream connecting");
String host = pulseaudioHandler.getHost();
int port = pulseaudioHandler.getSimpleTcpPort();
clientSocket = new Socket(host, port);
clientSocket.setSoTimeout(pulseaudioHandler.getBasicProtocolSOTimeout());
}
}
/**
* Disconnect the socket to pulseaudio simple protocol
*/
public void disconnect() {
final Socket clientSocketLocal = clientSocket;
if (clientSocketLocal != null && isIdle) {
logger.debug("Simple TCP Stream disconnecting");
try {
clientSocketLocal.close();
} catch (IOException ignored) {
}
} else {
logger.debug("Stream still running or socket not open");
}
}
public void scheduleDisconnect() {
if (scheduledDisconnection != null) {
scheduledDisconnection.cancel(true);
}
int idleTimeout = pulseaudioHandler.getIdleTimeout();
if (idleTimeout > -1) {
logger.debug("Scheduling disconnect");
scheduledDisconnection = scheduler.schedule(this::disconnect, idleTimeout, TimeUnit.MILLISECONDS);
}
}
public PercentType getVolume() {
return new PercentType(pulseaudioHandler.getLastVolume());
}
public void setVolume(PercentType volume) {
pulseaudioHandler.setVolume(volume.intValue());
}
public String getId() {
return pulseaudioHandler.getThing().getUID().toString();
}
public String getLabel(@Nullable Locale locale) {
var label = pulseaudioHandler.getThing().getLabel();
return label != null ? label : pulseaudioHandler.getThing().getUID().getId();
}
public void setIdle(boolean idle) {
isIdle = idle;
}
}

View File

@ -12,6 +12,7 @@
*/
package org.openhab.binding.pulseaudio.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingUID;
@ -23,6 +24,7 @@ import org.openhab.core.thing.ThingUID;
* @author Tobias Bräutigam - Initial contribution
*
*/
@NonNullByDefault
public interface DeviceStatusListener {
/**

View File

@ -28,12 +28,18 @@ import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.pulseaudio.internal.PulseAudioAudioSink;
import org.openhab.binding.pulseaudio.internal.PulseAudioAudioSource;
import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants;
import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig;
import org.openhab.binding.pulseaudio.internal.items.Sink;
import org.openhab.binding.pulseaudio.internal.items.SinkInput;
import org.openhab.binding.pulseaudio.internal.items.Source;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioSink;
import org.openhab.core.audio.AudioSource;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
@ -62,29 +68,28 @@ import org.slf4j.LoggerFactory;
* sent to one of the channels.
*
* @author Tobias Bräutigam - Initial contribution
* @author Miguel Álvarez - Register audio source and refactor
*/
@NonNullByDefault
public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusListener {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(SINK_THING_TYPE, COMBINED_SINK_THING_TYPE, SINK_INPUT_THING_TYPE,
SOURCE_THING_TYPE, SOURCE_OUTPUT_THING_TYPE).collect(Collectors.toSet()));
private int refresh = 60; // refresh every minute as default
private ScheduledFuture<?> refreshJob;
private PulseaudioBridgeHandler bridgeHandler;
private final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class);
private final int refresh = 60; // refresh every minute as default
private String name;
private @Nullable PulseaudioBridgeHandler bridgeHandler;
private @Nullable String name;
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable PulseAudioAudioSink audioSink;
private @Nullable PulseAudioAudioSource audioSource;
private @Nullable Integer savedVolume;
private PulseAudioAudioSink audioSink;
private final Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
private final Map<String, ServiceRegistration<AudioSource>> audioSourceRegistrations = new ConcurrentHashMap<>();
private Integer savedVolume;
private Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
private BundleContext bundleContext;
private final BundleContext bundleContext;
public PulseaudioHandler(Thing thing, BundleContext bundleContext) {
super(thing);
@ -109,6 +114,14 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
audioSinkSetup();
}
}
// if it's a SOURCE thing, then maybe we have to activate the audio source
if (SOURCE_THING_TYPE.equals(thing.getThingTypeUID())) {
// check the property to see if we it's enabled :
Boolean sourceActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_ACTIVATION);
if (sourceActivated != null && sourceActivated) {
audioSourceSetup();
}
}
}
private void audioSinkSetup() {
@ -139,6 +152,34 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
});
}
private void audioSourceSetup() {
final PulseaudioHandler thisHandler = this;
scheduler.submit(new Runnable() {
@Override
public void run() {
// Register the source as an audio source in openhab
logger.trace("Registering an audio source for pulse audio source thing {}", thing.getUID());
PulseAudioAudioSource audioSource = new PulseAudioAudioSource(thisHandler, scheduler);
setAudioSource(audioSource);
try {
audioSource.connectIfNeeded();
} catch (IOException e) {
logger.warn("pulseaudio binding cannot connect to the module-simple-protocol-tcp on {} ({})",
getHost(), e.getMessage());
} catch (InterruptedException i) {
logger.info("Interrupted during source audio connection: {}", i.getMessage());
return;
} finally {
audioSource.scheduleDisconnect();
}
@SuppressWarnings("unchecked")
ServiceRegistration<AudioSource> reg = (ServiceRegistration<AudioSource>) bundleContext
.registerService(AudioSource.class.getName(), audioSource, new Hashtable<>());
audioSourceRegistrations.put(thing.getUID().toString(), reg);
}
});
}
@Override
public void dispose() {
if (refreshJob != null && !refreshJob.isCancelled()) {
@ -152,16 +193,23 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
}
logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
super.dispose();
if (audioSink != null) {
audioSink.disconnect();
}
if (audioSource != null) {
audioSource.disconnect();
}
// Unregister the potential pulse audio sink's audio sink
ServiceRegistration<AudioSink> reg = audioSinkRegistrations.remove(getThing().getUID().toString());
if (reg != null) {
ServiceRegistration<AudioSink> sinkReg = audioSinkRegistrations.remove(getThing().getUID().toString());
if (sinkReg != null) {
logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID());
reg.unregister();
sinkReg.unregister();
}
// Unregister the potential pulse audio source's audio sources
ServiceRegistration<AudioSource> sourceReg = audioSourceRegistrations.remove(getThing().getUID().toString());
if (sourceReg != null) {
logger.trace("Unregistering the audio sync service for pulse audio source thing {}", getThing().getUID());
sourceReg.unregister();
}
}
@ -172,7 +220,7 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
if (bridgeHandler != null) {
if (bridgeHandler.getDevice(name) == null) {
updateStatus(ThingStatus.OFFLINE);
bridgeHandler = null;
this.bridgeHandler = null;
} else {
updateStatus(ThingStatus.ONLINE);
}
@ -182,14 +230,14 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
}
} catch (Exception e) {
logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
bridgeHandler = null;
this.bridgeHandler = null;
}
};
refreshJob = scheduler.scheduleWithFixedDelay(runnable, 0, refresh, TimeUnit.SECONDS);
}
private synchronized PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
private synchronized @Nullable PulseaudioBridgeHandler getPulseaudioBridgeHandler() {
if (this.bridgeHandler == null) {
Bridge bridge = getBridge();
if (bridge == null) {
@ -367,18 +415,73 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
* @throws InterruptedException when interrupted during the loading module wait
*/
public int getSimpleTcpPort() throws InterruptedException {
Integer simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration()
.get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_PORT)).intValue();
PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler();
var bridgeHandler = getPulseaudioBridgeHandler();
AbstractAudioDeviceConfig device = bridgeHandler.getDevice(name);
return getPulseaudioBridgeHandler().getClient().loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPortPref)
.orElse(simpleTcpPortPref);
String simpleTcpPortPrefName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_PORT
: DEVICE_PARAMETER_AUDIO_SINK_PORT;
BigDecimal simpleTcpPortPref = ((BigDecimal) getThing().getConfiguration().get(simpleTcpPortPrefName));
int simpleTcpPort = simpleTcpPortPref != null ? simpleTcpPortPref.intValue()
: MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT;
String simpleFormat = ((String) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_FORMAT));
BigDecimal simpleRate = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_RATE);
BigDecimal simpleChannels = (BigDecimal) getThing().getConfiguration()
.get(DEVICE_PARAMETER_AUDIO_SOURCE_CHANNELS);
return getPulseaudioBridgeHandler().getClient()
.loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPort, simpleFormat, simpleRate, simpleChannels)
.orElse(simpleTcpPort);
}
public @Nullable AudioFormat getSourceAudioFormat() {
String simpleFormat = ((String) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_FORMAT));
BigDecimal simpleRate = ((BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOURCE_RATE));
BigDecimal simpleChannels = ((BigDecimal) getThing().getConfiguration()
.get(DEVICE_PARAMETER_AUDIO_SOURCE_CHANNELS));
if (simpleFormat == null || simpleRate == null || simpleChannels == null) {
return null;
}
switch (simpleFormat) {
case "u8":
return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, null, 8, 1,
simpleRate.longValue(), simpleChannels.intValue());
case "s16le":
return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, false, 16, 1,
simpleRate.longValue(), simpleChannels.intValue());
case "s16be":
return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_SIGNED, true, 16, 1,
simpleRate.longValue(), simpleChannels.intValue());
case "s24le":
return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 24, 1,
simpleRate.longValue(), simpleChannels.intValue());
case "s24be":
return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, true, 24, 1,
simpleRate.longValue(), simpleChannels.intValue());
case "s32le":
return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, false, 32, 1,
simpleRate.longValue(), simpleChannels.intValue());
case "s32be":
return new AudioFormat(AudioFormat.CONTAINER_WAVE, AudioFormat.CODEC_PCM_UNSIGNED, true, 32, 1,
simpleRate.longValue(), simpleChannels.intValue());
default:
logger.warn("unsupported format {}", simpleFormat);
return null;
}
}
public int getIdleTimeout() {
return ((BigDecimal) getThing().getConfiguration()
.get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT)).intValue();
var handler = getPulseaudioBridgeHandler();
if (handler == null) {
return 30000;
}
AbstractAudioDeviceConfig device = handler.getDevice(name);
String idleTimeoutPropName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT
: DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT;
var idleTimeout = (BigDecimal) getThing().getConfiguration().get(idleTimeoutPropName);
return idleTimeout != null ? idleTimeout.intValue() : 30000;
}
public int getBasicProtocolSOTimeout() {
var soTimeout = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOCKET_SO_TIMEOUT);
return soTimeout != null ? soTimeout.intValue() : 500;
}
@Override
@ -400,4 +503,8 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
public void setAudioSink(PulseAudioAudioSink audioSink) {
this.audioSink = audioSink;
}
public void setAudioSource(PulseAudioAudioSource audioSource) {
this.audioSource = audioSource;
}
}

View File

@ -20,7 +20,7 @@
<parameter name="source" type="boolean">
<label>Import Sources</label>
<description>Activate the import of source elements.</description>
<default>false</default>
<default>true</default>
</parameter>
<parameter name="sourceOutput" type="boolean">
<label>Import Source Outputs</label>

View File

@ -43,14 +43,37 @@ thing-type.config.pulseaudio.sink.activateSimpleProtocolSink.label = Create an A
thing-type.config.pulseaudio.sink.activateSimpleProtocolSink.description = Activation of a corresponding sink in OpenHAB (module-simple-protocol-tcp must be available on the pulseaudio server)
thing-type.config.pulseaudio.sink.name.label = Name
thing-type.config.pulseaudio.sink.name.description = The name of one specific device.
thing-type.config.pulseaudio.sink.simpleProtocolSOTimeout.label = Simple Protocol SO Timeout
thing-type.config.pulseaudio.sink.simpleProtocolSOTimeout.description = Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp. You can tune this option if the socket disconnect frequently.
thing-type.config.pulseaudio.sink.simpleProtocolSinkIdleTimeout.label = Idle Timeout
thing-type.config.pulseaudio.sink.simpleProtocolSinkIdleTimeout.description = Timeout in ms after which the connection will be closed when no stream is running. This ensures that your speaker is not on all the time and the pulseaudio sink can go to idle mode. -1 for no disconnection.
thing-type.config.pulseaudio.sink.simpleProtocolSinkPort.label = Simple Protocol Port
thing-type.config.pulseaudio.sink.simpleProtocolSinkPort.description = Default Port to allocate for use by module-simple-protocol-tcp on the pulseaudio server
thing-type.config.pulseaudio.sinkInput.name.label = Name
thing-type.config.pulseaudio.sinkInput.name.description = The name of one specific device.
thing-type.config.pulseaudio.source.activateSimpleProtocolSource.label = Create an Audio Source with simple-protocol-tcp
thing-type.config.pulseaudio.source.activateSimpleProtocolSource.description = Activation of a corresponding source in OpenHAB (module-simple-protocol-tcp must be available on the pulseaudio server)
thing-type.config.pulseaudio.source.name.label = Name
thing-type.config.pulseaudio.source.name.description = The name of one specific device.
thing-type.config.pulseaudio.source.simpleProtocolSOTimeout.label = Simple Protocol SO Timeout
thing-type.config.pulseaudio.source.simpleProtocolSOTimeout.description = Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp. You can tune this option if the socket disconnect frequently.
thing-type.config.pulseaudio.source.simpleProtocolSourceChannels.label = Simple Protocol Channels
thing-type.config.pulseaudio.source.simpleProtocolSourceChannels.description = The audio channel number to be used by module-simple-protocol-tcp on the pulseaudio server
thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.label = Simple Protocol Format
thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.description = The audio format to be used by module-simple-protocol-tcp on the pulseaudio server
thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.u8 = PCM signed 8-bit
thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s16le = PCM signed 16-bit little-endian
thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s16be = PCM signed 16-bit big-endian
thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s24le = PCM unsigned 24-bit little-endian
thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s24be = PCM unsigned 24-bit big-endian
thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s32le = PCM signed 32-bit little-endian
thing-type.config.pulseaudio.source.simpleProtocolSourceFormat.option.s32be = PCM signed 32-bit big-endian
thing-type.config.pulseaudio.source.simpleProtocolSourceIdleTimeout.label = Idle Timeout
thing-type.config.pulseaudio.source.simpleProtocolSourceIdleTimeout.description = Timeout in ms after which the connection will be closed when no stream is running. This ensures that your speaker is not on all the time and the pulseaudio source can go to idle mode. -1 for no disconnection.
thing-type.config.pulseaudio.source.simpleProtocolSourcePort.label = Simple Protocol Port
thing-type.config.pulseaudio.source.simpleProtocolSourcePort.description = Default Port to allocate to be used by module-simple-protocol-tcp on the pulseaudio server
thing-type.config.pulseaudio.source.simpleProtocolSourceRate.label = Simple Protocol Rate
thing-type.config.pulseaudio.source.simpleProtocolSourceRate.description = The audio sample rate to be used by module-simple-protocol-tcp on the pulseaudio server
thing-type.config.pulseaudio.sourceOutput.name.label = Name
thing-type.config.pulseaudio.sourceOutput.name.description = The name of one specific device.

View File

@ -40,6 +40,13 @@
</description>
<default>30000</default>
</parameter>
<parameter name="simpleProtocolSOTimeout" type="integer" min="250" max="2000">
<label>Simple Protocol SO Timeout</label>
<description>Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp. You can tune
this option if the socket disconnect frequently.</description>
<default>500</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>

View File

@ -21,6 +21,58 @@
<label>Name</label>
<description>The name of one specific device.</description>
</parameter>
<parameter name="activateSimpleProtocolSource" type="boolean" required="false">
<label>Create an Audio Source with simple-protocol-tcp</label>
<description>Activation of a corresponding source in OpenHAB (module-simple-protocol-tcp must be available on the
pulseaudio server)</description>
<default>false</default>
</parameter>
<parameter name="simpleProtocolSourcePort" type="integer" required="false">
<label>Simple Protocol Port</label>
<description>Default Port to allocate to be used by module-simple-protocol-tcp on the pulseaudio server</description>
<default>4710</default>
</parameter>
<parameter name="simpleProtocolSourceIdleTimeout" type="integer" required="false">
<label>Idle Timeout</label>
<description>Timeout in ms after which the connection will be closed when no stream is running. This ensures that
your speaker is not on all the time and the pulseaudio source can go to idle mode. -1 for no disconnection.
</description>
<default>30000</default>
</parameter>
<parameter name="simpleProtocolSourceFormat" type="text">
<label>Simple Protocol Format</label>
<description>The audio format to be used by module-simple-protocol-tcp on the pulseaudio server</description>
<default>s16le</default>
<advanced>true</advanced>
<options>
<option value="u8">PCM signed 8-bit</option>
<option value="s16le">PCM signed 16-bit little-endian</option>
<option value="s16be">PCM signed 16-bit big-endian</option>
<option value="s24le">PCM unsigned 24-bit little-endian</option>
<option value="s24be">PCM unsigned 24-bit big-endian</option>
<option value="s32le">PCM signed 32-bit little-endian</option>
<option value="s32be">PCM signed 32-bit big-endian</option>
</options>
</parameter>
<parameter name="simpleProtocolSourceRate" type="integer" min="0">
<label>Simple Protocol Rate</label>
<description>The audio sample rate to be used by module-simple-protocol-tcp on the pulseaudio server</description>
<default>16000</default>
<advanced>true</advanced>
</parameter>
<parameter name="simpleProtocolSourceChannels" type="integer" min="1">
<label>Simple Protocol Channels</label>
<description>The audio channel number to be used by module-simple-protocol-tcp on the pulseaudio server</description>
<default>1</default>
<advanced>true</advanced>
</parameter>
<parameter name="simpleProtocolSOTimeout" type="integer" min="250" max="2000">
<label>Simple Protocol SO Timeout</label>
<description>Socket SO timeout when connecting to pulseaudio server though module-simple-protocol-tcp. You can tune
this option if the socket disconnect frequently.</description>
<default>500</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>