diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java index 94bf0d28e61..4f7d6dc2c2b 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioClient.java @@ -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); } } } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java index 7a90d94a4e1..133669d743d 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioBridgeHandler.java @@ -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 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(); diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java index 2a25df118a0..a357b663188 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/PulseaudioHandler.java @@ -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 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 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); }