The Pulseaudio bridge is required as a "bridge" for accessing any other Pulseaudio devices.
-You need a running pulseaudio server whith module **module-cli-protocol-tcp** loaded and accessible by the server which runs your openHAB instance. The following pulseaudio devices are supported:
+You need a running pulseaudio server with module **module-cli-protocol-tcp** loaded and accessible by the server which runs your openHAB instance. The following pulseaudio devices are supported:
* Sink
* Source
| slaves | String | Slave sinks of a combined sink |
| routeToSink | String | Shows the sink a sink-input is currently routed to |
+## Audio sink
+
+Sink things can register themselves as audio sink in openHAB. MP3 and WAV files are supported.
+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.
+
## Full Example
### pulseaudio.things
```
Bridge pulseaudio:bridge:<bridgname> "<Bridge Label>" @ "<Room>" [ host="<ipAddress>", port=4712 ] {
Things:
- Thing sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3"] // this name corresponds to pactl list-sinks output
+ Thing sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3", activateSimpleProtocolSink="true", simpleProtocolSinkPort="4711"] // the name corresponds to pactl list-sinks output
Thing source microphone "microphone" @ "Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"]
Thing sink-input openhabTTS "OH-Voice" @ "Room" [name="alsa_output.pci-0000_00_1f.3.hdmi-stereo-extra1"]
Thing source-output remotePulseSink "Other Room Speaker" @ "Other Room" [name="alsa_input.pci-0000_00_14.2.analog-stereo"]
<name>openHAB Add-ons :: Bundles :: Pulseaudio Binding</name>
+ <dependencies>
+ <dependency>
+ <groupId>com.googlecode.soundlibs</groupId>
+ <artifactId>mp3spi</artifactId>
+ <version>1.9.5.4</version>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.googlecode.soundlibs</groupId>
+ <artifactId>jlayer</artifactId>
+ <version>1.0.1.4</version>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.googlecode.soundlibs</groupId>
+ <artifactId>tritonus-share</artifactId>
+ <version>0.3.7.4</version>
+ <scope>compile</scope>
+ </dependency>
+ </dependencies>
+
</project>
<feature>openhab-transport-mdns</feature>
<feature>openhab-transport-upnp</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.pulseaudio/${project.version}</bundle>
+ <bundle dependency="true">mvn:com.googlecode.soundlibs/tritonus-share/0.3.7.4</bundle>
+ <bundle dependency="true">mvn:com.googlecode.soundlibs/mp3spi/1.9.5.4</bundle>
+ <bundle dependency="true">mvn:com.googlecode.soundlibs/jlayer/1.0.1.4</bundle>
</feature>
</features>
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.net.Socket;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider;
+import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader;
+
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.UnsupportedAudioFileException;
+
+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.AudioFormat;
+import org.openhab.core.audio.AudioSink;
+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;
+
+/**
+ * The audio sink for openhab, implemented by a connection to a pulseaudio sink
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class PulseAudioAudioSink 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 @Nullable Socket clientSocket;
+
+ static {
+ SUPPORTED_FORMATS.add(AudioFormat.WAV);
+ SUPPORTED_FORMATS.add(AudioFormat.MP3);
+ SUPPORTED_STREAMS.add(FixedLengthAudioStream.class);
+ }
+
+ public PulseAudioAudioSink(PulseaudioHandler pulseaudioHandler) {
+ this.pulseaudioHandler = pulseaudioHandler;
+ }
+
+ @Override
+ public String getId() {
+ return pulseaudioHandler.getThing().getUID().toString();
+ }
+
+ @Override
+ public @Nullable String getLabel(@Nullable Locale locale) {
+ return pulseaudioHandler.getThing().getLabel();
+ }
+
+ /**
+ * Convert MP3 to PCM, as this is the only possible format
+ *
+ * @param input
+ * @return
+ */
+ private @Nullable InputStream getPCMStreamFromMp3Stream(InputStream input) {
+ try {
+ MpegAudioFileReader mpegAudioFileReader = new MpegAudioFileReader();
+ AudioInputStream sourceAIS = mpegAudioFileReader.getAudioInputStream(input);
+ javax.sound.sampled.AudioFormat sourceFormat = sourceAIS.getFormat();
+
+ MpegFormatConversionProvider mpegconverter = new MpegFormatConversionProvider();
+ javax.sound.sampled.AudioFormat convertFormat = new javax.sound.sampled.AudioFormat(
+ javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(), 16,
+ sourceFormat.getChannels(), sourceFormat.getChannels() * 2, sourceFormat.getSampleRate(), false);
+
+ return mpegconverter.getAudioInputStream(convertFormat, sourceAIS);
+
+ } catch (IOException | UnsupportedAudioFileException e) {
+ logger.warn("Cannot convert this mp3 stream to pcm stream: {}", e.getMessage());
+ }
+ return null;
+ }
+
+ /**
+ * 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() {
+ if (clientSocket != null) {
+ try {
+ clientSocket.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ @Override
+ public void process(@Nullable AudioStream audioStream)
+ throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
+
+ if (audioStream == null) {
+ return;
+ }
+
+ InputStream audioInputStream = null;
+ try {
+
+ if (AudioFormat.MP3.isCompatible(audioStream.getFormat())) {
+ audioInputStream = getPCMStreamFromMp3Stream(audioStream);
+ } else if (AudioFormat.WAV.isCompatible(audioStream.getFormat())) {
+ audioInputStream = audioStream;
+ } else {
+ throw new UnsupportedAudioFormatException("pulseaudio audio sink can only play pcm or mp3 stream",
+ audioStream.getFormat());
+ }
+
+ for (int countAttempt = 1; countAttempt <= 2; countAttempt++) { // two attempts allowed
+ try {
+ connectIfNeeded();
+ if (audioInputStream != null && clientSocket != null) {
+ // send raw audio to the socket and to pulse audio
+ audioInputStream.transferTo(clientSocket.getOutputStream());
+ break;
+ }
+ } 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());
+ }
+ break;
+ }
+ } catch (InterruptedException ie) {
+ logger.info("Interrupted during sink audio connection: {}", ie.getMessage());
+ break;
+ }
+ }
+ } finally {
+ try {
+ if (audioInputStream != null) {
+ audioInputStream.close();
+ }
+ audioStream.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ @Override
+ public Set<AudioFormat> getSupportedFormats() {
+ return SUPPORTED_FORMATS;
+ }
+
+ @Override
+ 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());
+ }
+}
public static final String BRIDGE_PARAMETER_REFRESH_INTERVAL = "refresh";
public static final String DEVICE_PARAMETER_NAME = "name";
+ public static final String DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION = "activateSimpleProtocolSink";
+ public static final String DEVICE_PARAMETER_AUDIO_SINK_PORT = "simpleProtocolSinkPort";
+
+ public static final String MODULE_SIMPLE_PROTOCOL_TCP_NAME = "module-simple-protocol-tcp";
+ public static final int MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT = 4711;
public static final Map<String, Boolean> TYPE_FILTERS = new HashMap<>();
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
+import java.util.Random;
+import org.eclipse.jdt.annotation.NonNull;
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;
/**
* updates the item states and their relationships
*/
- public void update() {
- modules.clear();
- modules.addAll(Parser.parseModules(listModules()));
+ public synchronized void update() {
+ // one step copy
+ modules = new ArrayList<Module>(Parser.parseModules(listModules()));
- items.clear();
- if (TYPE_FILTERS.get(SINK_THING_TYPE.getId())) {
+ List<AbstractAudioDeviceConfig> newItems = new ArrayList<>(); // prepare new list before assigning it
+ newItems.clear();
+ if (Optional.ofNullable(TYPE_FILTERS.get(SINK_THING_TYPE.getId())).orElse(false)) {
logger.debug("reading sinks");
- items.addAll(Parser.parseSinks(listSinks(), this));
+ newItems.addAll(Parser.parseSinks(listSinks(), this));
}
- if (TYPE_FILTERS.get(SOURCE_THING_TYPE.getId())) {
+ if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_THING_TYPE.getId())).orElse(false)) {
logger.debug("reading sources");
- items.addAll(Parser.parseSources(listSources(), this));
+ newItems.addAll(Parser.parseSources(listSources(), this));
}
- if (TYPE_FILTERS.get(SINK_INPUT_THING_TYPE.getId())) {
+ if (Optional.ofNullable(TYPE_FILTERS.get(SINK_INPUT_THING_TYPE.getId())).orElse(false)) {
logger.debug("reading sink-inputs");
- items.addAll(Parser.parseSinkInputs(listSinkInputs(), this));
+ newItems.addAll(Parser.parseSinkInputs(listSinkInputs(), this));
}
- if (TYPE_FILTERS.get(SOURCE_OUTPUT_THING_TYPE.getId())) {
+ if (Optional.ofNullable(TYPE_FILTERS.get(SOURCE_OUTPUT_THING_TYPE.getId())).orElse(false)) {
logger.debug("reading source-outputs");
- items.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this));
+ newItems.addAll(Parser.parseSourceOutputs(listSourceOutputs(), this));
}
- logger.debug("Pulseaudio server {}: {} modules and {} items updated", host, modules.size(), items.size());
+ logger.debug("Pulseaudio server {}: {} modules and {} items updated", host, modules.size(), newItems.size());
+ items = newItems;
}
private String listModules() {
item.setVolume(Math.round(100f / 65536f * vol));
}
+ /**
+ * Locate or load (if needed) the simple protocol tcp module for the given sink
+ * and returns the port.
+ * The module loading (if needed) will be tried several times, on a new random port each time.
+ *
+ * @param item the sink we are searching for
+ * @param simpleTcpPortPref the port to use if we have to load the module
+ * @return the port on which the module is listening
+ * @throws InterruptedException
+ */
+ public Optional<Integer> loadModuleSimpleProtocolTcpIfNeeded(AbstractAudioDeviceConfig item,
+ Integer simpleTcpPortPref) throws InterruptedException {
+ int currentTry = 0;
+ int simpleTcpPortToTry = simpleTcpPortPref;
+ do {
+ Optional<Integer> simplePort = findSimpleProtocolTcpModule(item);
+
+ if (simplePort.isPresent()) {
+ return simplePort;
+ } else {
+ sendRawCommand("load-module module-simple-protocol-tcp sink=" + item.getPaName() + " port="
+ + simpleTcpPortToTry);
+ simpleTcpPortToTry = new Random().nextInt(64512) + 1024; // a random port above 1024
+ }
+ Thread.sleep(100);
+ currentTry++;
+ } while (currentTry < 3);
+
+ logger.warn("The pulseaudio binding tried 3 times to load the module-simple-protocol-tcp"
+ + " on random port on the pulseaudio server and give up trying");
+ return Optional.empty();
+ }
+
+ /**
+ * Find a simple protocol module corresponding to the given sink in argument
+ * and returns the port it listens to
+ *
+ * @param item
+ * @return
+ */
+ private Optional<Integer> findSimpleProtocolTcpModule(AbstractAudioDeviceConfig item) {
+ update();
+
+ List<Module> modulesCopy = new ArrayList<Module>(modules);
+ 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
+ .findAny() // get a corresponding module
+ .map(module -> extractArgumentFromLine("port", module.getArgument())
+ .orElse(Integer.toString(MODULE_SIMPLE_PROTOCOL_TCP_DEFAULT_PORT))) // get port
+ .map(portS -> Integer.parseInt(portS));
+ }
+
+ private @NonNull Optional<@NonNull String> extractArgumentFromLine(String argumentWanted, String argumentLine) {
+ String argument = null;
+ int startPortIndex = argumentLine.indexOf(argumentWanted + "=");
+ if (startPortIndex != -1) {
+ startPortIndex = startPortIndex + argumentWanted.length() + 1;
+ int endPortIndex = argumentLine.indexOf(" ", startPortIndex);
+ if (endPortIndex == -1) {
+ endPortIndex = argumentLine.length();
+ }
+ argument = argumentLine.substring(startPortIndex, endPortIndex);
+ }
+ return Optional.ofNullable(argument);
+ }
+
/**
* returns the item names that can be used in commands
*
* values from 0 - 100)
*/
public void setVolumePercent(AbstractAudioDeviceConfig item, int vol) {
+ int volumeToSet = vol;
if (item == null) {
return;
}
- if (vol <= 100) {
- vol = toAbsoluteVolume(vol);
+ if (volumeToSet <= 100) {
+ volumeToSet = toAbsoluteVolume(volumeToSet);
}
- setVolume(item, vol);
+ setVolume(item, volumeToSet);
}
/**
} catch (SocketTimeoutException e) {
// Timeout -> as newer PA versions (>=5.0) do not send the >>> we have no chance
// to detect the end of the answer, except by this timeout
+ } catch (SocketException e) {
+ logger.warn("Socket exception while sending pulseaudio command: {}", e.getMessage());
} catch (IOException e) {
logger.error("Exception while reading socket: {}", e.getMessage());
}
} catch (NoRouteToHostException e) {
logger.error("no route to host {}", host);
} catch (SocketException e) {
- logger.error("{}", e.getLocalizedMessage(), e);
+ logger.error("cannot connect to host {} : {}", host, e.getMessage());
}
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
- if (this.discoveryServiceReg.containsKey(thingHandler)) {
+ ServiceRegistration<?> serviceRegistration = this.discoveryServiceReg.get(thingHandler);
+ if (serviceRegistration != null) {
PulseaudioDeviceDiscoveryService service = (PulseaudioDeviceDiscoveryService) bundleContext
- .getService(discoveryServiceReg.get(thingHandler).getReference());
+ .getService(serviceRegistration.getReference());
service.deactivate();
- discoveryServiceReg.get(thingHandler).unregister();
- discoveryServiceReg.remove(thingHandler);
+ serviceRegistration.unregister();
}
+ discoveryServiceReg.remove(thingHandler);
super.removeHandler(thingHandler);
}
@Override
protected ThingHandler createHandler(Thing thing) {
+
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
if (PulseaudioBridgeHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
PulseaudioBridgeHandler handler = new PulseaudioBridgeHandler((Bridge) thing);
registerDeviceDiscoveryService(handler);
return handler;
} else if (PulseaudioHandler.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
- return new PulseaudioHandler(thing);
+ return new PulseaudioHandler(thing, bundleContext);
}
return null;
}
}
if (properties.containsKey("muted")) {
- sink.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
+ sink.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
}
if (properties.containsKey("volume")) {
sink.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
}
if (properties.containsKey("combine.slaves")) {
// this is a combined sink, the combined sink object should be
- for (String sinkName : properties.get("combine.slaves").replace("\"", "").split(",")) {
- sink.addCombinedSinkName(sinkName);
+ String sinkNames = properties.get("combine.slaves");
+ if (sinkNames != null) {
+ for (String sinkName : sinkNames.replace("\"", "").split(",")) {
+ sink.addCombinedSinkName(sinkName);
+ }
}
combinedSinks.add(sink);
}
}
}
if (properties.containsKey("muted")) {
- item.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
+ item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
}
if (properties.containsKey("volume")) {
item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
}
}
if (properties.containsKey("muted")) {
- source.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
+ source.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
}
if (properties.containsKey("volume")) {
source.setVolume(parseVolume(properties.get("volume")));
}
}
if (properties.containsKey("muted")) {
- item.setMuted(properties.get("muted").equalsIgnoreCase("yes"));
+ item.setMuted("yes".equalsIgnoreCase(properties.get("muted")));
}
if (properties.containsKey("volume")) {
item.setVolume(Integer.valueOf(parseVolume(properties.get("volume"))));
}
return result;
} catch (IOException e) {
- result = null;
}
}
return result;
@Override
public void dispose() {
- pollingJob.cancel(true);
- client.disconnect();
+ if (pollingJob != null) {
+ pollingJob.cancel(true);
+ }
+ if (client != null) {
+ client.disconnect();
+ }
super.dispose();
}
import static org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants.*;
+import java.io.IOException;
+import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Hashtable;
import java.util.List;
+import java.util.Map;
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;
+import org.openhab.binding.pulseaudio.internal.PulseAudioAudioSink;
+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.core.audio.AudioSink;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.IncreaseDecreaseType;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private String name;
- public PulseaudioHandler(Thing thing) {
+ private PulseAudioAudioSink audioSink;
+
+ private Integer savedVolume;
+
+ private Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
+
+ private BundleContext bundleContext;
+
+ public PulseaudioHandler(Thing thing, BundleContext bundleContext) {
super(thing);
+ this.bundleContext = bundleContext;
}
@Override
// until we get an update put the Thing offline
updateStatus(ThingStatus.OFFLINE);
deviceOnlineWatchdog();
+
+ // if it's a SINK thing, then maybe we have to activate the audio sink
+ if (PulseaudioBindingConstants.SINK_THING_TYPE.equals(thing.getThingTypeUID())) {
+ // check the property to see if we it's enabled :
+ Boolean sinkActivated = (Boolean) thing.getConfiguration()
+ .get(PulseaudioBindingConstants.DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION);
+ if (sinkActivated != null && sinkActivated) {
+ audioSinkSetup();
+ }
+ }
+ }
+
+ private void audioSinkSetup() {
+ final PulseaudioHandler thisHandler = this;
+ 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);
+ setAudioSink(audioSink);
+ try {
+ audioSink.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 sink audio connection: {}", i.getMessage());
+ return;
+ }
+ @SuppressWarnings("unchecked")
+ ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
+ .registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
+ audioSinkRegistrations.put(thing.getUID().toString(), reg);
+ }
+ });
}
@Override
refreshJob = null;
}
updateStatus(ThingStatus.OFFLINE);
+ bridgeHandler.unregisterDeviceStatusListener(this);
bridgeHandler = null;
logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
super.dispose();
+
+ if (audioSink != null) {
+ audioSink.disconnect();
+ }
+
+ // Unregister the potential pulse audio sink's audio sink
+ ServiceRegistration<AudioSink> reg = audioSinkRegistrations.remove(getThing().getUID().toString());
+ if (reg != null) {
+ logger.trace("Unregistering the audio sync service for pulse audio sink thing {}", getThing().getUID());
+ reg.unregister();
+ }
}
private void deviceOnlineWatchdog() {
// refresh to get the current volume level
bridge.getClient().update();
device = bridge.getDevice(name);
- int volume = device.getVolume();
+ savedVolume = device.getVolume();
if (command.equals(IncreaseDecreaseType.INCREASE)) {
- volume = Math.min(100, volume + 5);
+ savedVolume = Math.min(100, savedVolume + 5);
}
if (command.equals(IncreaseDecreaseType.DECREASE)) {
- volume = Math.max(0, volume - 5);
+ savedVolume = Math.max(0, savedVolume - 5);
}
- bridge.getClient().setVolumePercent(device, volume);
- updateState = new PercentType(volume);
+ bridge.getClient().setVolumePercent(device, savedVolume);
+ updateState = new PercentType(savedVolume);
} else if (command instanceof PercentType) {
DecimalType volume = (DecimalType) command;
bridge.getClient().setVolumePercent(device, volume.intValue());
}
}
+ /**
+ * Use last checked volume for faster access
+ *
+ * @return
+ */
+ public int getLastVolume() {
+ if (savedVolume == null) {
+ PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
+ AbstractAudioDeviceConfig device = bridge.getDevice(name);
+ // refresh to get the current volume level
+ bridge.getClient().update();
+ device = bridge.getDevice(name);
+ savedVolume = device.getVolume();
+ }
+ return savedVolume == null ? 50 : savedVolume;
+ }
+
+ public void setVolume(int volume) {
+ PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
+ AbstractAudioDeviceConfig device = bridge.getDevice(name);
+ bridge.getClient().setVolumePercent(device, volume);
+ updateState(VOLUME_CHANNEL, new PercentType(volume));
+ }
+
@Override
public void onDeviceStateChanged(ThingUID bridge, AbstractAudioDeviceConfig device) {
if (device.getPaName().equals(name)) {
updateStatus(ThingStatus.ONLINE);
logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL);
- updateState(VOLUME_CHANNEL, new PercentType(device.getVolume()));
+ 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("-"));
}
}
+ public String getHost() {
+ Bridge bridge = getBridge();
+ if (bridge != null) {
+ return (String) bridge.getConfiguration().get(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST);
+ } else {
+ logger.error("A bridge must be configured for this pulseaudio thing");
+ return "null";
+ }
+ }
+
+ /**
+ * This method will scan the pulseaudio server to find the port on which the module/sink 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
+ * @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();
+ AbstractAudioDeviceConfig device = bridgeHandler.getDevice(name);
+ return getPulseaudioBridgeHandler().getClient().loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPortPref)
+ .orElse(simpleTcpPortPref);
+ }
+
@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);
}
}
public void onDeviceAdded(Bridge bridge, AbstractAudioDeviceConfig device) {
logger.trace("new device discovered {} by {}", device, bridge);
}
+
+ public void setAudioSink(PulseAudioAudioSink audioSink) {
+ this.audioSink = audioSink;
+ }
}
</supported-bridge-type-refs>
<label>A Pulseaudio Sink</label>
<description>represents a pulseaudio sink</description>
+ <category>Speaker</category>
<channels>
<channel id="volume" typeId="volume"/>
<label>Name</label>
<description>The name of one specific device.</description>
</parameter>
+ <parameter name="activateSimpleProtocolSink" type="boolean" required="false">
+ <label>Create an Audio Sink with simple-protocol-tcp</label>
+ <description>Activation of a corresponding sink in OpenHAB (module-simple-protocol-tcp must be available on the
+ pulseaudio server)</description>
+ <default>false</default>
+ </parameter>
+ <parameter name="simpleProtocolSinkPort" type="integer" required="false">
+ <label>Simple Protocol Port</label>
+ <description>Default Port to allocate for use by module-simple-protocol-tcp on the pulseaudio server</description>
+ <default>4711</default>
+ </parameter>
</config-description>
</thing-type>