diff --git a/bundles/org.openhab.binding.pulseaudio/README.md b/bundles/org.openhab.binding.pulseaudio/README.md index 8f4dac6347399..8536a1c4a36b1 100644 --- a/bundles/org.openhab.binding.pulseaudio/README.md +++ b/bundles/org.openhab.binding.pulseaudio/README.md @@ -38,8 +38,12 @@ binding.pulseaudio:sourceOutput=false ## Thing Configuration -The Pulseaudio bridge requires the host (ip address or a hostname) and a port (default: 4712) as a configuration value in order for the binding to know where to access it. -You can use `pactl -s list sinks | grep "name:"` to find the name of a sink. +The Pulseaudio bridge requires the host (ip address or a hostname) and a port (default: 4712) as a configuration value in order for the binding to know where to access it. +A Pulseaudio device requires at least an identifier. For sinks and sources, you can use the name or the description. For sink inputs and source outputs, you can use the name or the application name. +To know without hesitation the correct value to use, you should use the command line utility `pactl`. For example, to find the name of a sink: +`pactl -s list sinks | grep "name:"` +If you need to narrow the identification of a device (in case name or description are not consistent and sufficient), you can use the `additionalFilters` parameter (optional/advanced parameter), in the form of one or several (separator '###') regular expression(s), each one matching a property value of the pulseaudio device. You can use every properties listed with `pactl`. + ## Channels @@ -74,7 +78,7 @@ This requires the module **module-simple-protocol-tcp** to be present on the tar ``` Bridge pulseaudio:bridge: "" @ "" [ host="", port=4712 ] { Things: - 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 sink multiroom "Snapcast" @ "Room" [name="alsa_card.pci-0000_00_1f.3", activateSimpleProtocolSink=true, simpleProtocolSinkPort=4711, additionalFilters="analog-stereo###internal"] 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"] diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java index a8277e88b3ee4..94c9ce0ac85ab 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioBindingConstants.java @@ -48,7 +48,8 @@ public class PulseaudioBindingConstants { public static final String BRIDGE_PARAMETER_PORT = "port"; public static final String BRIDGE_PARAMETER_REFRESH_INTERVAL = "refresh"; - public static final String DEVICE_PARAMETER_NAME = "name"; + public static final String DEVICE_PARAMETER_NAME_OR_DESCRIPTION = "name"; + public static final String DEVICE_PARAMETER_ADDITIONAL_FILTERS = "additionalFilters"; 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"; 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 4cf99f0d4f174..5fe46df82d679 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 @@ -23,13 +23,16 @@ import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.Random; +import java.util.stream.Collectors; 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.handler.DeviceIdentifier; import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig; import org.openhab.binding.pulseaudio.internal.items.AbstractAudioDeviceConfig.State; import org.openhab.binding.pulseaudio.internal.items.Module; @@ -258,15 +261,23 @@ public void sendCommand(String command) { } /** - * retrieves a {@link AbstractAudioDeviceConfig} by its name + * retrieves a {@link AbstractAudioDeviceConfig} by its identifier + * If several devices correspond to the deviceIdentifier, returns the first one (aphabetical order) * + * @param The device identifier to match against * @return the corresponding {@link AbstractAudioDeviceConfig} to the given name */ - public @Nullable AbstractAudioDeviceConfig getGenericAudioItem(String name) { - for (AbstractAudioDeviceConfig item : items) { - if (item.getPaName().equalsIgnoreCase(name)) { - return item; - } + public @Nullable AbstractAudioDeviceConfig getGenericAudioItem(DeviceIdentifier deviceIdentifier) { + List matchingDevices = items.stream() + .filter(device -> device.matches(deviceIdentifier)) + .sorted(Comparator.comparing(AbstractAudioDeviceConfig::getPaName)).collect(Collectors.toList()); + if (matchingDevices.size() == 1) { + return matchingDevices.get(0); + } else if (matchingDevices.size() > 1) { + logger.debug( + "Cannot select exactly one audio device, so, choosing the first. To choose without ambiguity between the {} devices matching the identifier {}, you can maybe use a more restrictive 'additionalFilter' parameter", + matchingDevices.size(), deviceIdentifier.getNameOrDescription()); + return matchingDevices.get(0); } return null; } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java index eb91a1dd4275d..11d242fdb91df 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/PulseaudioHandlerFactory.java @@ -89,7 +89,7 @@ private void registerDeviceDiscoveryService(PulseaudioBridgeHandler paBridgeHand private ThingUID getPulseaudioDeviceUID(ThingTypeUID thingTypeUID, @Nullable ThingUID thingUID, Configuration configuration, @Nullable ThingUID bridgeUID) { if (thingUID == null) { - String name = (String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME); + String name = (String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME_OR_DESCRIPTION); return new ThingUID(thingTypeUID, name, bridgeUID == null ? null : bridgeUID.getId()); } return thingUID; @@ -101,7 +101,9 @@ protected void removeHandler(ThingHandler thingHandler) { if (serviceRegistration != null) { PulseaudioDeviceDiscoveryService service = (PulseaudioDeviceDiscoveryService) bundleContext .getService(serviceRegistration.getReference()); - service.deactivate(); + if (service != null) { + service.deactivate(); + } serviceRegistration.unregister(); } discoveryServiceReg.remove(thingHandler); diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java index 47742c1796eed..b9bcd09074e3e 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/cli/Parser.java @@ -128,7 +128,7 @@ public static Collection parseSinks(String raw, PulseaudioClient client) { } } if (properties.containsKey("name")) { - Sink sink = new Sink(id, properties.get("name"), + Sink sink = new Sink(id, properties.get("name"), properties.get("device.description"), properties, client.getModule(getNumberValue(properties.get("module")))); if (properties.containsKey("state")) { try { @@ -198,7 +198,8 @@ public static List parseSinkInputs(String raw, PulseaudioClient clien if (properties.containsKey("sink")) { String name = properties.containsKey("media.name") ? properties.get("media.name") : properties.get("sink"); - SinkInput item = new SinkInput(id, name, client.getModule(getNumberValue(properties.get("module")))); + SinkInput item = new SinkInput(id, name, properties.get("application.name"), properties, + client.getModule(getNumberValue(properties.get("module")))); if (properties.containsKey("state")) { try { item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state"))); @@ -256,7 +257,7 @@ public static List parseSources(String raw, PulseaudioClient client) { } } if (properties.containsKey("name")) { - Source source = new Source(id, properties.get("name"), + Source source = new Source(id, properties.get("name"), properties.get("device.description"), properties, client.getModule(getNumberValue(properties.get("module")))); if (properties.containsKey("state")) { try { @@ -316,8 +317,8 @@ public static List parseSourceOutputs(String raw, PulseaudioClient } } if (properties.containsKey("source")) { - SourceOutput item = new SourceOutput(id, properties.get("source"), - client.getModule(getNumberValue(properties.get("module")))); + SourceOutput item = new SourceOutput(id, properties.get("source"), properties.get("application.name"), + properties, client.getModule(getNumberValue(properties.get("module")))); if (properties.containsKey("state")) { try { item.setState(AbstractAudioDeviceConfig.State.valueOf(properties.get("state"))); diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDeviceDiscoveryService.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDeviceDiscoveryService.java index b36e53472461d..1ab1f22cc9f4f 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDeviceDiscoveryService.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/discovery/PulseaudioDeviceDiscoveryService.java @@ -13,12 +13,14 @@ package org.openhab.binding.pulseaudio.internal.discovery; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; +import java.util.regex.PatternSyntaxException; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.pulseaudio.internal.PulseaudioBindingConstants; +import org.openhab.binding.pulseaudio.internal.handler.DeviceIdentifier; import org.openhab.binding.pulseaudio.internal.handler.DeviceStatusListener; import org.openhab.binding.pulseaudio.internal.handler.PulseaudioBridgeHandler; import org.openhab.binding.pulseaudio.internal.handler.PulseaudioHandler; @@ -27,6 +29,7 @@ import org.openhab.binding.pulseaudio.internal.items.SinkInput; import org.openhab.binding.pulseaudio.internal.items.Source; import org.openhab.binding.pulseaudio.internal.items.SourceOutput; +import org.openhab.core.config.core.Configuration; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; @@ -70,7 +73,7 @@ public Set getSupportedThingTypes() { @Override public void onDeviceAdded(Thing bridge, AbstractAudioDeviceConfig device) { - if (getAlreadyConfiguredThings().contains(device.getPaName())) { + if (getAlreadyConfiguredThings().stream().anyMatch(deviceIdentifier -> device.matches(deviceIdentifier))) { return; } @@ -79,7 +82,7 @@ public void onDeviceAdded(Thing bridge, AbstractAudioDeviceConfig device) { ThingTypeUID thingType = null; Map properties = new HashMap<>(); // All devices need this parameter - properties.put(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME, uidName); + properties.put(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME_OR_DESCRIPTION, uidName); if (device instanceof Sink) { if (((Sink) device).isCombinedSink()) { thingType = PulseaudioBindingConstants.COMBINED_SINK_THING_TYPE; @@ -104,10 +107,20 @@ public void onDeviceAdded(Thing bridge, AbstractAudioDeviceConfig device) { } } - public Set getAlreadyConfiguredThings() { - return pulseaudioBridgeHandler.getThing().getThings().stream().map(Thing::getConfiguration) - .map(conf -> (String) conf.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME)) - .collect(Collectors.toSet()); + public Set getAlreadyConfiguredThings() { + Set alreadyConfiguredThings = new HashSet<>(); + for (Thing thing : pulseaudioBridgeHandler.getThing().getThings()) { + Configuration configuration = thing.getConfiguration(); + try { + alreadyConfiguredThings.add(new DeviceIdentifier( + (String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_NAME_OR_DESCRIPTION), + (String) configuration.get(PulseaudioBindingConstants.DEVICE_PARAMETER_ADDITIONAL_FILTERS))); + } catch (PatternSyntaxException p) { + logger.debug( + "There is an error with an already configured things. Cannot compare with discovery, skipping it"); + } + } + return alreadyConfiguredThings; } @Override diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceIdentifier.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceIdentifier.java new file mode 100644 index 0000000000000..e7d74cb74177c --- /dev/null +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/handler/DeviceIdentifier.java @@ -0,0 +1,61 @@ +/** + * 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.handler; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * All informations needed to precisely identify a device + * + * @author Gwendal Roulleau - Initial contribution + * + */ +@NonNullByDefault +public class DeviceIdentifier { + + private String nameOrDescription; + private List additionalFilters = new ArrayList<>(); + + public DeviceIdentifier(String nameOrDescription, @Nullable String additionalFilters) { + super(); + this.nameOrDescription = nameOrDescription; + if (additionalFilters != null && !additionalFilters.isEmpty()) { + Arrays.asList(additionalFilters.split("###")).stream() + .forEach(ad -> this.additionalFilters.add(Pattern.compile(ad))); + } + } + + public String getNameOrDescription() { + return nameOrDescription; + } + + public List getAdditionalFilters() { + return additionalFilters; + } + + @Override + public String toString() { + List additionalFiltersFinal = additionalFilters; + String additionalPatternToString = additionalFiltersFinal.stream().map(Pattern::pattern) + .collect(Collectors.joining("###")); + return "DeviceIdentifier [nameOrDescription=" + nameOrDescription + ", additionalFilter=" + + additionalPatternToString + "]"; + } +} 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 d6e0bacafa2e9..d4e5583dfcb8a 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 @@ -97,7 +97,7 @@ public synchronized void update() { } else { // browse all child handlers to update status according to the result of the query to the pulse audio server for (PulseaudioHandler pulseaudioHandler : childHandlersInitialized) { - pulseaudioHandler.deviceUpdate(getDevice(pulseaudioHandler.getName())); + pulseaudioHandler.deviceUpdate(getDevice(pulseaudioHandler.getDeviceIdentifier())); } } // browse query result to notify add event @@ -129,8 +129,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - public @Nullable AbstractAudioDeviceConfig getDevice(String name) { - return getClient().getGenericAudioItem(name); + public @Nullable AbstractAudioDeviceConfig getDevice(@Nullable DeviceIdentifier deviceIdentifier) { + return deviceIdentifier == null ? null : getClient().getGenericAudioItem(deviceIdentifier); } public PulseaudioClient getClient() { 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 b6821bd5ffa39..676cbc1c8f938 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 @@ -24,6 +24,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.PatternSyntaxException; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -78,7 +79,7 @@ public class PulseaudioHandler extends BaseThingHandler { SOURCE_THING_TYPE, SOURCE_OUTPUT_THING_TYPE).collect(Collectors.toSet())); private final Logger logger = LoggerFactory.getLogger(PulseaudioHandler.class); - private String name = ""; + private @Nullable DeviceIdentifier deviceIdentifier; private @Nullable PulseAudioAudioSink audioSink; private @Nullable PulseAudioAudioSource audioSource; private @Nullable Integer savedVolume; @@ -96,12 +97,20 @@ public PulseaudioHandler(Thing thing, BundleContext bundleContext) { @Override public void initialize() { Configuration config = getThing().getConfiguration(); - name = (String) config.get(DEVICE_PARAMETER_NAME); + try { + deviceIdentifier = new DeviceIdentifier((String) config.get(DEVICE_PARAMETER_NAME_OR_DESCRIPTION), + (String) config.get(DEVICE_PARAMETER_ADDITIONAL_FILTERS)); + } catch (PatternSyntaxException p) { + deviceIdentifier = null; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Incorrect regular expression: " + (String) config.get(DEVICE_PARAMETER_ADDITIONAL_FILTERS)); + return; + } initializeWithTheBridge(); } - public String getName() { - return name; + public @Nullable DeviceIdentifier getDeviceIdentifier() { + return deviceIdentifier; } private void audioSinkSetup() { @@ -214,7 +223,7 @@ private void audioSourceUnsetup() { @Override public void dispose() { - logger.trace("Thing {} {} disposed.", getThing().getUID(), name); + logger.trace("Thing {} {} disposed.", getThing().getUID(), safeGetDeviceNameOrDescription()); super.dispose(); audioSinkUnsetup(); audioSourceUnsetup(); @@ -232,21 +241,22 @@ private void initializeWithTheBridge() { } else if (pulseaudioBridgeHandler.getThing().getStatus() != ThingStatus.ONLINE) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); } else { - deviceUpdate(pulseaudioBridgeHandler.getDevice(name)); + deviceUpdate(pulseaudioBridgeHandler.getDevice(deviceIdentifier)); } } private synchronized @Nullable PulseaudioBridgeHandler getPulseaudioBridgeHandler() { Bridge bridge = getBridge(); if (bridge == null) { - logger.debug("Required bridge not defined for device {}.", name); + logger.debug("Required bridge not defined for device {}.", safeGetDeviceNameOrDescription()); return null; } ThingHandler handler = bridge.getHandler(); if (handler instanceof PulseaudioBridgeHandler) { return (PulseaudioBridgeHandler) handler; } else { - logger.debug("No available bridge handler found for device {} bridge {} .", name, bridge.getUID()); + logger.debug("No available bridge handler found for device {} bridge {} .", + safeGetDeviceNameOrDescription(), bridge.getUID()); return null; } } @@ -263,9 +273,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { return; } - AbstractAudioDeviceConfig device = briHandler.getDevice(name); + AbstractAudioDeviceConfig device = briHandler.getDevice(deviceIdentifier); if (device == null) { - logger.warn("device {} not found", name); + logger.warn("device {} not found", safeGetDeviceNameOrDescription()); deviceUpdate(null); return; } else { @@ -274,7 +284,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof IncreaseDecreaseType) { // refresh to get the current volume level briHandler.getClient().update(); - device = briHandler.getDevice(name); + device = briHandler.getDevice(deviceIdentifier); if (device == null) { logger.warn("missing device info, aborting"); return; @@ -360,7 +370,7 @@ public Integer getLastVolume() { if (briHandler != null) { // refresh to get the current volume level briHandler.getClient().update(); - AbstractAudioDeviceConfig device = briHandler.getDevice(name); + AbstractAudioDeviceConfig device = briHandler.getDevice(deviceIdentifier); if (device != null) { savedVolume = savedVolumeFinal = device.getVolume(); } @@ -375,7 +385,7 @@ public void setVolume(int volume) { logger.warn("bridge is not ready"); return; } - AbstractAudioDeviceConfig device = briHandler.getDevice(name); + AbstractAudioDeviceConfig device = briHandler.getDevice(deviceIdentifier); if (device == null) { logger.warn("missing device info, aborting"); return; @@ -386,7 +396,7 @@ public void setVolume(int volume) { } public void deviceUpdate(@Nullable AbstractAudioDeviceConfig device) { - if (device != null && device.getPaName().equals(name)) { + if (device != null) { updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE); logger.debug("Updating states of {} id: {}", device, VOLUME_CHANNEL); int actualVolume = device.getVolume(); @@ -404,7 +414,7 @@ public void deviceUpdate(@Nullable AbstractAudioDeviceConfig device) { } audioSinkSetup(); audioSourceSetup(); - } else if (device == null) { + } else { updateState(VOLUME_CHANNEL, UnDefType.UNDEF); updateState(MUTE_CHANNEL, UnDefType.UNDEF); updateState(STATE_CHANNEL, UnDefType.UNDEF); @@ -443,9 +453,10 @@ public int getSimpleTcpPortAndLoadModuleIfNecessary() throws IOException, Interr if (briHandler == null) { throw new IOException("bridge is not ready"); } - AbstractAudioDeviceConfig device = briHandler.getDevice(name); + AbstractAudioDeviceConfig device = briHandler.getDevice(deviceIdentifier); if (device == null) { - throw new IOException("missing device info, device appears to be offline"); + throw new IOException( + "missing device info, device " + safeGetDeviceNameOrDescription() + " appears to be offline"); } String simpleTcpPortPrefName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_PORT : DEVICE_PARAMETER_AUDIO_SINK_PORT; @@ -501,7 +512,7 @@ public int getIdleTimeout() { var idleTimeout = 3000; var handler = getPulseaudioBridgeHandler(); if (handler != null) { - AbstractAudioDeviceConfig device = handler.getDevice(name); + AbstractAudioDeviceConfig device = handler.getDevice(deviceIdentifier); String idleTimeoutPropName = (device instanceof Source) ? DEVICE_PARAMETER_AUDIO_SOURCE_IDLE_TIMEOUT : DEVICE_PARAMETER_AUDIO_SINK_IDLE_TIMEOUT; var idleTimeoutB = (BigDecimal) getThing().getConfiguration().get(idleTimeoutPropName); @@ -512,6 +523,11 @@ public int getIdleTimeout() { return idleTimeout; } + private String safeGetDeviceNameOrDescription() { + DeviceIdentifier deviceIdentifierFinal = deviceIdentifier; + return deviceIdentifierFinal == null ? "UNKNOWN" : deviceIdentifierFinal.getNameOrDescription(); + } + public int getBasicProtocolSOTimeout() { var soTimeout = (BigDecimal) getThing().getConfiguration().get(DEVICE_PARAMETER_AUDIO_SOCKET_SO_TIMEOUT); return soTimeout != null ? soTimeout.intValue() : 500; diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/AbstractAudioDeviceConfig.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/AbstractAudioDeviceConfig.java index 68a842f2fe136..3c100595b4c47 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/AbstractAudioDeviceConfig.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/AbstractAudioDeviceConfig.java @@ -12,8 +12,13 @@ */ package org.openhab.binding.pulseaudio.internal.items; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.pulseaudio.internal.handler.DeviceIdentifier; /** * GenericAudioItems are any kind of items that deal with audio data and can be @@ -36,10 +41,40 @@ public enum State { protected boolean muted; protected int volume; protected @Nullable Module module; + protected String secondaryIdentifier; + protected Map properties; - public AbstractAudioDeviceConfig(int id, String name, @Nullable Module module) { + public AbstractAudioDeviceConfig(int id, String name, @Nullable String secondaryIdentifier, + Map properties, @Nullable Module module) { super(id, name); this.module = module; + this.secondaryIdentifier = secondaryIdentifier == null ? "" : secondaryIdentifier; + this.properties = properties; + } + + /** + * + * @param deviceIdentifier The device identifier to check against + * @return true if this device match the requested identifier, false otherwise + */ + public boolean matches(DeviceIdentifier deviceIdentifier) { + boolean matches = getPaName().equalsIgnoreCase(deviceIdentifier.getNameOrDescription()) + || secondaryIdentifier.equalsIgnoreCase(deviceIdentifier.getNameOrDescription()); + if (!matches) { + return false; // stop analysis right here, no need to parse properties + } else { + List additionalFilters = deviceIdentifier.getAdditionalFilters(); + if (additionalFilters.isEmpty()) { // the additionalFilter property is not defined, don't check against + return true; + } else { + for (Pattern patternToMatch : additionalFilters) { + if (!properties.values().stream().anyMatch(value -> patternToMatch.matcher(value).find())) { + return false; + } + } + return true; + } + } } public @Nullable Module getModule() { diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Sink.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Sink.java index 0f31498277125..ccea6228ce393 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Sink.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Sink.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -31,8 +32,8 @@ public class Sink extends AbstractAudioDeviceConfig { protected List combinedSinkNames; protected List combinedSinks; - public Sink(int id, String name, @Nullable Module module) { - super(id, name, module); + public Sink(int id, String name, String description, Map properties, @Nullable Module module) { + super(id, name, description, properties, module); combinedSinkNames = new ArrayList<>(); combinedSinks = new ArrayList<>(); } diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SinkInput.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SinkInput.java index fa9cb5046a98c..a85f6c8d88d67 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SinkInput.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SinkInput.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.pulseaudio.internal.items; +import java.util.Map; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -26,8 +28,8 @@ public class SinkInput extends AbstractAudioDeviceConfig { @Nullable private Sink sink; - public SinkInput(int id, String name, @Nullable Module module) { - super(id, name, module); + public SinkInput(int id, String name, String description, Map properties, @Nullable Module module) { + super(id, name, description, properties, module); } public @Nullable Sink getSink() { diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Source.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Source.java index c5162b3a51dc6..299531dd49c67 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Source.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/Source.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.pulseaudio.internal.items; +import java.util.Map; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -27,8 +29,8 @@ public class Source extends AbstractAudioDeviceConfig { @Nullable protected Sink monitorOf; - public Source(int id, String name, @Nullable Module module) { - super(id, name, module); + public Source(int id, String name, String description, Map properties, @Nullable Module module) { + super(id, name, description, properties, module); } public @Nullable Sink getMonitorOf() { diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SourceOutput.java b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SourceOutput.java index 0bda3854553c5..e82d637629c4a 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SourceOutput.java +++ b/bundles/org.openhab.binding.pulseaudio/src/main/java/org/openhab/binding/pulseaudio/internal/items/SourceOutput.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.pulseaudio.internal.items; +import java.util.Map; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -26,8 +28,9 @@ public class SourceOutput extends AbstractAudioDeviceConfig { @Nullable private Source source; - public SourceOutput(int id, String name, @Nullable Module module) { - super(id, name, module); + public SourceOutput(int id, String name, String description, Map properties, + @Nullable Module module) { + super(id, name, description, properties, module); } public @Nullable Source getSource() { diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/i18n/pulseaudio.properties b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/i18n/pulseaudio.properties index 2a56feb6c7ddb..a961124e0c739 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/i18n/pulseaudio.properties +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/i18n/pulseaudio.properties @@ -42,7 +42,9 @@ thing-type.config.pulseaudio.combinedSink.name.description = The name of the com thing-type.config.pulseaudio.sink.activateSimpleProtocolSink.label = Create an Audio Sink with simple-protocol-tcp 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.name.description = The name of one specific device. You can also use the description. +thing-type.config.pulseaudio.sink.additionalFilters.label = Additional Filters +thing-type.config.pulseaudio.sink.additionalFilters.description = Additional filters to select the proper device on the pulseaudio server, in case of ambiguity. To be selected, the device should have at least a property value matching this regular expression. You can use multiple regular expressions (separator is ###). 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 @@ -50,11 +52,15 @@ thing-type.config.pulseaudio.sink.simpleProtocolSinkIdleTimeout.description = Ti 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.sinkInput.name.description = The name of one specific device. You can also use the application name. +thing-type.config.pulseaudio.sinkInput.additionalFilters.label = Additional Filters +thing-type.config.pulseaudio.sinkInput.additionalFilters.description = Additional filters to select the proper device on the pulseaudio server, in case of ambiguity. To be selected, the device should have at least a property value matching this regular expression. You can use multiple regular expressions (separator is ###). 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.name.description = The name of one specific device. You can also use the description. +thing-type.config.pulseaudio.source.additionalFilters.label = Additional Filters +thing-type.config.pulseaudio.source.additionalFilters.description = Additional filters to select the proper device on the pulseaudio server, in case of ambiguity. To be selected, the device should have at least a property value matching this regular expression. You can use multiple regular expressions (separator is ###). 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 @@ -75,7 +81,9 @@ thing-type.config.pulseaudio.source.simpleProtocolSourcePort.description = Defau 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. +thing-type.config.pulseaudio.sourceOutput.name.description = The name of one specific device. You can also use the application name. +thing-type.config.pulseaudio.sourceOutput.additionalFilters.label = Additional Filters +thing-type.config.pulseaudio.sourceOutput.additionalFilters.description = Additional filters to select the proper device on the pulseaudio server, in case of ambiguity. To be selected, the device should have at least a property value matching this regular expression. You can use multiple regular expressions (separator is ###). # channel types diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink-input.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink-input.xml index 76ee975019e9d..854b9513a7031 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink-input.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink-input.xml @@ -20,7 +20,16 @@ - The name of one specific device. + The name of one specific device. You can also use the application name. + + + + true + Additional filters to select the proper device on the pulseaudio server, in case of ambiguity. + To be + selected, the device should have at least a property value matching this regular expression. You can use + multiple + regular expressions (separator ###). diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml index 6e4a44fe96399..dd5fb8910df0c 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/sink.xml @@ -20,7 +20,7 @@ - The name of one specific device. + The name of one specific device. You can also use the description. @@ -28,6 +28,15 @@ pulseaudio server) false + + + true + Additional filters to select the proper device on the pulseaudio server, in case of ambiguity. + To be + selected, the device should have at least a property value matching this regular expression. You can use + multiple + regular expressions (separator ###). + Default Port to allocate for use by module-simple-protocol-tcp on the pulseaudio server diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source-output.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source-output.xml index 847f3fe308d39..cc7b04b91a793 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source-output.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source-output.xml @@ -19,7 +19,16 @@ - The name of one specific device. + The name of one specific device. You can also use the application name. + + + + true + Additional filters to select the proper device on the pulseaudio server, in case of ambiguity. + To be + selected, the device should have at least a property value matching this regular expression. You can use + multiple + regular expressions (separator is ###). diff --git a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml index 9383b62bcbb22..147ec817e824e 100644 --- a/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml +++ b/bundles/org.openhab.binding.pulseaudio/src/main/resources/OH-INF/thing/source.xml @@ -19,7 +19,16 @@ - The name of one specific device. + The name of one specific device. You can also use the description. + + + + true + Additional filters to select the proper device on the pulseaudio server, in case of ambiguity. + To be + selected, the device should have at least a property value matching this regular expression. You can use + multiple + regular expressions (separator ###).