[pulseaudio] Fix exception handling when connecting (#12423)

Fix bridge/thing status update
Also update log levels

Fix #12419
Fix #12424

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
lolodomo 2022-03-08 00:01:54 +01:00 committed by GitHub
parent 20f6b52e71
commit e3ca3b01bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 161 additions and 126 deletions

View File

@ -18,7 +18,6 @@ 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;
import java.net.SocketTimeoutException;
@ -129,20 +128,18 @@ public class PulseaudioClient {
*/
private static final String MODULE_COMBINE_SINK = "module-combine-sink";
public PulseaudioClient(String host, int port, PulseAudioBindingConfiguration configuration) throws IOException {
public PulseaudioClient(String host, int port, PulseAudioBindingConfiguration configuration) {
this.host = host;
this.port = port;
this.configuration = configuration;
items = new ArrayList<>();
modules = new ArrayList<>();
connect();
update();
}
public boolean isConnected() {
return client != null ? client.isConnected() : false;
Socket clientSocket = client;
return clientSocket != null ? clientSocket.isConnected() : false;
}
/**
@ -378,9 +375,6 @@ public class PulseaudioClient {
* 0 - 65536)
*/
public void setVolume(AbstractAudioDeviceConfig item, int vol) {
if (item == null) {
return;
}
String itemCommandName = getItemCommandName(item);
if (itemCommandName == null) {
return;
@ -485,7 +479,7 @@ public class PulseaudioClient {
.map(portS -> Integer.parseInt(portS));
}
private @NonNull Optional<@NonNull String> extractArgumentFromLine(String argumentWanted, String argumentLine) {
private Optional<@NonNull String> extractArgumentFromLine(String argumentWanted, String argumentLine) {
String argument = null;
int startPortIndex = argumentLine.indexOf(argumentWanted + "=");
if (startPortIndex != -1) {
@ -525,11 +519,8 @@ 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(@Nullable AbstractAudioDeviceConfig item, int vol) {
public void setVolumePercent(AbstractAudioDeviceConfig item, int vol) {
int volumeToSet = vol;
if (item == null) {
return;
}
if (volumeToSet <= 100) {
volumeToSet = toAbsoluteVolume(volumeToSet);
}
@ -662,15 +653,16 @@ public class PulseaudioClient {
private synchronized void sendRawCommand(String command) {
checkConnection();
if (client != null && client.isConnected()) {
Socket clientSocket = client;
if (clientSocket != null && clientSocket.isConnected()) {
try {
PrintStream out = new PrintStream(client.getOutputStream(), true);
PrintStream out = new PrintStream(clientSocket.getOutputStream(), true);
logger.trace("sending command {} to pa-server {}", command, host);
out.print(command + "\r\n");
out.close();
client.close();
clientSocket.close();
} catch (IOException e) {
logger.error("{}", e.getLocalizedMessage(), e);
logger.warn("{}", e.getMessage(), e);
}
}
}
@ -679,12 +671,13 @@ public class PulseaudioClient {
logger.trace("_sendRawRequest({})", command);
checkConnection();
String result = "";
if (client != null && client.isConnected()) {
Socket clientSocket = client;
if (clientSocket != null && clientSocket.isConnected()) {
try {
PrintStream out = new PrintStream(client.getOutputStream(), true);
PrintStream out = new PrintStream(clientSocket.getOutputStream(), true);
out.print(command + "\r\n");
InputStream instr = client.getInputStream();
InputStream instr = clientSocket.getInputStream();
try {
byte[] buff = new byte[1024];
@ -709,42 +702,52 @@ public class PulseaudioClient {
} catch (SocketException e) {
logger.warn("Socket exception while sending pulseaudio command: {}", e.getMessage());
} catch (IOException e) {
logger.error("Exception while reading socket: {}", e.getMessage());
logger.warn("Exception while reading socket: {}", e.getMessage());
}
instr.close();
out.close();
client.close();
clientSocket.close();
return result;
} catch (IOException e) {
logger.error("{}", e.getLocalizedMessage(), e);
logger.warn("{}", e.getMessage(), e);
}
}
return result;
}
private void checkConnection() {
if (client == null || client.isClosed() || !client.isConnected()) {
try {
connect();
} catch (IOException e) {
logger.error("{}", e.getLocalizedMessage(), e);
}
try {
connect();
} catch (IOException e) {
logger.debug("{}", e.getMessage(), e);
}
}
/**
* Connects to the pulseaudio server (timeout 500ms)
*/
private void connect() throws IOException {
try {
client = new Socket(host, port);
client.setSoTimeout(500);
} catch (UnknownHostException e) {
logger.error("unknown socket host {}", host);
} catch (NoRouteToHostException e) {
logger.error("no route to host {}", host);
} catch (SocketException e) {
logger.error("cannot connect to host {} : {}", host, e.getMessage());
public void connect() throws IOException {
Socket clientSocket = client;
if (clientSocket == null || clientSocket.isClosed() || !clientSocket.isConnected()) {
logger.trace("Try to connect...");
try {
client = new Socket(host, port);
client.setSoTimeout(500);
logger.trace("connected");
} catch (UnknownHostException e) {
client = null;
throw new IOException("Unknown host", e);
} catch (IllegalArgumentException e) {
client = null;
throw new IOException("Invalid port", e);
} catch (SecurityException | SocketException e) {
client = null;
throw new IOException(
String.format("Cannot connect socket: %s", e.getMessage() != null ? e.getMessage() : ""), e);
} catch (IOException e) {
client = null;
throw e;
}
}
}
@ -752,11 +755,12 @@ public class PulseaudioClient {
* Disconnects from the pulseaudio server
*/
public void disconnect() {
if (client != null) {
Socket clientSocket = client;
if (clientSocket != null) {
try {
client.close();
clientSocket.close();
} catch (IOException e) {
logger.error("{}", e.getLocalizedMessage(), e);
logger.debug("{}", e.getMessage(), e);
}
}
}

View File

@ -34,6 +34,7 @@ import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
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.types.Command;
@ -67,11 +68,22 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
private HashSet<String> lastActiveDevices = new HashSet<>();
private ScheduledFuture<?> pollingJob;
private Runnable pollingRunnable = () -> {
update();
};
private 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);
}
} catch (IOException e) {
logger.debug("{}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
String.format("Couldn't connect to Pulsaudio server [Host '%s':'%d']: %s", host, port,
e.getMessage() != null ? e.getMessage() : ""));
return;
}
client.update();
for (AbstractAudioDeviceConfig device : client.getItems()) {
if (lastActiveDevices != null && lastActiveDevices.contains(device.getPaName())) {
@ -79,7 +91,7 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
try {
deviceStatusListener.onDeviceStateChanged(getThing().getUID(), device);
} catch (Exception e) {
logger.error("An exception occurred while calling the DeviceStatusListener", e);
logger.warn("An exception occurred while calling the DeviceStatusListener", e);
}
}
} else {
@ -88,7 +100,7 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
deviceStatusListener.onDeviceAdded(getThing(), device);
deviceStatusListener.onDeviceStateChanged(getThing().getUID(), device);
} catch (Exception e) {
logger.error("An exception occurred while calling the DeviceStatusListener", e);
logger.warn("An exception occurred while calling the DeviceStatusListener", e);
}
lastActiveDevices.add(device.getPaName());
}
@ -106,13 +118,7 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
if (command instanceof RefreshType) {
client.update();
} else {
logger.warn("received invalid command for pulseaudio bridge '{}'.", host);
}
}
private synchronized void startAutomaticRefresh() {
if (pollingJob == null || pollingJob.isCancelled()) {
pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, refreshInterval, TimeUnit.MILLISECONDS);
logger.debug("received unexpected command for pulseaudio bridge '{}'.", host);
}
}
@ -140,26 +146,15 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
}
if (host != null && !host.isEmpty()) {
Runnable connectRunnable = () -> {
try {
client = new PulseaudioClient(host, port, configuration);
if (client.isConnected()) {
updateStatus(ThingStatus.ONLINE);
logger.info("Established connection to Pulseaudio server on Host '{}':'{}'.", host, port);
startAutomaticRefresh();
}
} catch (IOException e) {
logger.error("Couldn't connect to Pulsaudio server [Host '{}':'{}']: {}", host, port,
e.getLocalizedMessage());
updateStatus(ThingStatus.OFFLINE);
}
};
scheduler.schedule(connectRunnable, 0, TimeUnit.SECONDS);
client = new PulseaudioClient(host, port, configuration);
updateStatus(ThingStatus.UNKNOWN);
if (pollingJob == null || pollingJob.isCancelled()) {
pollingJob = scheduler.scheduleWithFixedDelay(this::update, 0, refreshInterval, TimeUnit.MILLISECONDS);
}
} else {
logger.warn(
"Couldn't connect to Pulseaudio server because of missing connection parameters [Host '{}':'{}'].",
host, port);
updateStatus(ThingStatus.OFFLINE);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
"Couldn't connect to Pulseaudio server because of missing connection parameters [Host '%s':'%d']",
host, port));
}
this.configuration.addPulseAudioBindingConfigurationListener(this);
@ -168,8 +163,10 @@ public class PulseaudioBridgeHandler extends BaseBridgeHandler implements PulseA
@Override
public void dispose() {
this.configuration.removePulseAudioBindingConfigurationListener(this);
if (pollingJob != null) {
pollingJob.cancel(true);
ScheduledFuture<?> job = pollingJob;
if (job != null) {
job.cancel(true);
pollingJob = null;
}
if (client != null) {
client.disconnect();

View File

@ -50,6 +50,8 @@ 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.ThingStatusInfo;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandler;
@ -101,15 +103,13 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
Configuration config = getThing().getConfiguration();
name = (String) config.get(DEVICE_PARAMETER_NAME);
// until we get an update put the Thing offline
updateStatus(ThingStatus.OFFLINE);
updateStatus(ThingStatus.UNKNOWN);
deviceOnlineWatchdog();
// if it's a SINK thing, then maybe we have to activate the audio sink
if (PulseaudioBindingConstants.SINK_THING_TYPE.equals(thing.getThingTypeUID())) {
if (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);
Boolean sinkActivated = (Boolean) thing.getConfiguration().get(DEVICE_PARAMETER_AUDIO_SINK_ACTIVATION);
if (sinkActivated != null && sinkActivated) {
audioSinkSetup();
}
@ -182,22 +182,25 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
@Override
public void dispose() {
if (refreshJob != null && !refreshJob.isCancelled()) {
refreshJob.cancel(true);
ScheduledFuture<?> job = refreshJob;
if (job != null && !job.isCancelled()) {
job.cancel(true);
refreshJob = null;
}
updateStatus(ThingStatus.OFFLINE);
if (bridgeHandler != null) {
bridgeHandler.unregisterDeviceStatusListener(this);
PulseaudioBridgeHandler briHandler = bridgeHandler;
if (briHandler != null) {
briHandler.unregisterDeviceStatusListener(this);
bridgeHandler = null;
}
logger.trace("Thing {} {} disposed.", getThing().getUID(), name);
super.dispose();
if (audioSink != null) {
audioSink.disconnect();
PulseAudioAudioSink sink = audioSink;
if (sink != null) {
sink.disconnect();
}
if (audioSource != null) {
audioSource.disconnect();
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());
@ -213,20 +216,42 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
}
}
@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);
}
}
private void deviceOnlineWatchdog() {
Runnable runnable = () -> {
try {
PulseaudioBridgeHandler bridgeHandler = getPulseaudioBridgeHandler();
if (bridgeHandler != null) {
if (bridgeHandler.getDevice(name) == null) {
updateStatus(ThingStatus.OFFLINE);
this.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.ONLINE);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
} else {
logger.debug("Bridge for pulseaudio device {} not found.", name);
updateStatus(ThingStatus.OFFLINE);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
}
} catch (Exception e) {
logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
@ -258,17 +283,17 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
if (bridge == null) {
logger.warn("pulseaudio server bridge handler not found. Cannot handle command without bridge.");
PulseaudioBridgeHandler briHandler = getPulseaudioBridgeHandler();
if (briHandler == null) {
logger.debug("pulseaudio server bridge handler not found. Cannot handle command without bridge.");
return;
}
if (command instanceof RefreshType) {
bridge.handleCommand(channelUID, command);
briHandler.handleCommand(channelUID, command);
return;
}
AbstractAudioDeviceConfig device = bridge.getDevice(name);
AbstractAudioDeviceConfig device = briHandler.getDevice(name);
if (device == null) {
logger.warn("device {} not found", name);
updateStatus(ThingStatus.OFFLINE);
@ -279,8 +304,8 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
if (channelUID.getId().equals(VOLUME_CHANNEL)) {
if (command instanceof IncreaseDecreaseType) {
// refresh to get the current volume level
bridge.getClient().update();
device = bridge.getDevice(name);
briHandler.getClient().update();
device = briHandler.getDevice(name);
if (device == null) {
logger.warn("missing device info, aborting");
return;
@ -293,24 +318,24 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
if (command.equals(IncreaseDecreaseType.DECREASE)) {
newVolume = Math.max(0, oldVolume - 5);
}
bridge.getClient().setVolumePercent(device, newVolume);
briHandler.getClient().setVolumePercent(device, newVolume);
updateState = new PercentType(newVolume);
savedVolume = newVolume;
} else if (command instanceof PercentType) {
DecimalType volume = (DecimalType) command;
bridge.getClient().setVolumePercent(device, volume.intValue());
briHandler.getClient().setVolumePercent(device, volume.intValue());
updateState = (PercentType) command;
savedVolume = volume.intValue();
} else if (command instanceof DecimalType) {
// set volume
DecimalType volume = (DecimalType) command;
bridge.getClient().setVolume(device, volume.intValue());
briHandler.getClient().setVolume(device, volume.intValue());
updateState = (DecimalType) command;
savedVolume = volume.intValue();
}
} else if (channelUID.getId().equals(MUTE_CHANNEL)) {
if (command instanceof OnOffType) {
bridge.getClient().setMute(device, OnOffType.ON.equals(command));
briHandler.getClient().setMute(device, OnOffType.ON.equals(command));
updateState = (OnOffType) command;
}
} else if (channelUID.getId().equals(SLAVES_CHANNEL)) {
@ -318,32 +343,32 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
if (command instanceof StringType) {
List<Sink> slaves = new ArrayList<>();
for (String slaveName : command.toString().split(",")) {
Sink slave = bridge.getClient().getSink(slaveName.trim());
Sink slave = briHandler.getClient().getSink(slaveName.trim());
if (slave != null) {
slaves.add(slave);
}
}
if (!slaves.isEmpty()) {
bridge.getClient().setCombinedSinkSlaves(((Sink) device), slaves);
briHandler.getClient().setCombinedSinkSlaves(((Sink) device), slaves);
}
}
} else {
logger.error("{} is no combined sink", device);
logger.warn("{} is no combined sink", device);
}
} else if (channelUID.getId().equals(ROUTE_TO_SINK_CHANNEL)) {
if (device instanceof SinkInput) {
Sink newSink = null;
if (command instanceof DecimalType) {
newSink = bridge.getClient().getSink(((DecimalType) command).intValue());
newSink = briHandler.getClient().getSink(((DecimalType) command).intValue());
} else {
newSink = bridge.getClient().getSink(command.toString());
newSink = briHandler.getClient().getSink(command.toString());
}
if (newSink != null) {
logger.debug("rerouting {} to {}", device, newSink);
bridge.getClient().moveSinkInput(((SinkInput) device), newSink);
briHandler.getClient().moveSinkInput(((SinkInput) device), newSink);
updateState = new StringType(newSink.getPaName());
} else {
logger.error("no sink {} found", command.toString());
logger.warn("no sink {} found", command.toString());
}
}
}
@ -361,25 +386,31 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
*/
public int getLastVolume() {
if (savedVolume == null) {
PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
// refresh to get the current volume level
bridge.getClient().update();
AbstractAudioDeviceConfig device = bridge.getDevice(name);
if (device != null) {
savedVolume = device.getVolume();
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();
}
}
}
return savedVolume == null ? 50 : savedVolume;
}
public void setVolume(int volume) {
PulseaudioBridgeHandler bridge = getPulseaudioBridgeHandler();
AbstractAudioDeviceConfig device = bridge.getDevice(name);
PulseaudioBridgeHandler briHandler = getPulseaudioBridgeHandler();
if (briHandler == null) {
logger.warn("bridge is not ready");
return;
}
AbstractAudioDeviceConfig device = briHandler.getDevice(name);
if (device == null) {
logger.warn("missing device info, aborting");
return;
}
bridge.getClient().setVolumePercent(device, volume);
briHandler.getClient().setVolumePercent(device, volume);
updateState(VOLUME_CHANNEL, new PercentType(volume));
savedVolume = volume;
}
@ -411,7 +442,7 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
if (bridge != null) {
return (String) bridge.getConfiguration().get(PulseaudioBindingConstants.BRIDGE_PARAMETER_HOST);
} else {
logger.error("A bridge must be configured for this pulseaudio thing");
logger.warn("A bridge must be configured for this pulseaudio thing");
return "null";
}
}
@ -425,8 +456,11 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
* @throws InterruptedException when interrupted during the loading module wait
*/
public int getSimpleTcpPort() throws IOException, InterruptedException {
var bridgeHandler = getPulseaudioBridgeHandler();
AbstractAudioDeviceConfig device = bridgeHandler.getDevice(name);
var briHandler = getPulseaudioBridgeHandler();
if (briHandler == null) {
throw new IOException("bridge is not ready");
}
AbstractAudioDeviceConfig device = briHandler.getDevice(name);
if (device == null) {
throw new IOException("missing device info, device appears to be offline");
}
@ -439,7 +473,7 @@ public class PulseaudioHandler extends BaseThingHandler implements DeviceStatusL
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()
return briHandler.getClient()
.loadModuleSimpleProtocolTcpIfNeeded(device, simpleTcpPort, simpleFormat, simpleRate, simpleChannels)
.orElse(simpleTcpPort);
}