Get rid of internal library code and use pulsectl library to communicate with PulseAudio server. This is a breaking change as the library uses the much more powerful native interface instead of the CLI interface, requiring the need to change the default port. On the bright side, this also solves some issues with the existing implementation: - There was no test if the complete list of loaded modules was already received. If not all data could be read at once, the remaining modules not yet in the buffer were considered absent, resulting in unreliable behavior when a lot of modules were loaded on the server. - A switch could be turned on before the list of loaded modules was loaded, leading to a loopback module being loaded even though this module was already active (#32016).
126 lines
3.6 KiB
Python
126 lines
3.6 KiB
Python
"""Switch logic for loading/unloading pulseaudio loopback modules."""
|
|
import logging
|
|
|
|
from pulsectl import Pulse, PulseError
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
|
|
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
|
|
import homeassistant.helpers.config_validation as cv
|
|
|
|
DOMAIN = "pulseaudio_loopback"
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CONF_SINK_NAME = "sink_name"
|
|
CONF_SOURCE_NAME = "source_name"
|
|
|
|
DEFAULT_NAME = "paloopback"
|
|
DEFAULT_PORT = 4713
|
|
|
|
IGNORED_SWITCH_WARN = "Switch is already in the desired state. Ignoring."
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
{
|
|
vol.Required(CONF_SINK_NAME): cv.string,
|
|
vol.Required(CONF_SOURCE_NAME): cv.string,
|
|
vol.Optional(CONF_HOST): cv.string,
|
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
|
}
|
|
)
|
|
|
|
|
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
|
"""Read in all of our configuration, and initialize the loopback switch."""
|
|
name = config.get(CONF_NAME)
|
|
sink_name = config.get(CONF_SINK_NAME)
|
|
source_name = config.get(CONF_SOURCE_NAME)
|
|
host = config.get(CONF_HOST)
|
|
port = config.get(CONF_PORT)
|
|
|
|
hass.data.setdefault(DOMAIN, {})
|
|
|
|
server_id = str.format("{0}:{1}", host, port)
|
|
|
|
if host:
|
|
connect_to_server = server_id
|
|
else:
|
|
connect_to_server = None
|
|
|
|
if server_id in hass.data[DOMAIN]:
|
|
server = hass.data[DOMAIN][server_id]
|
|
else:
|
|
server = Pulse(server=connect_to_server, connect=False, threading_lock=True)
|
|
hass.data[DOMAIN][server_id] = server
|
|
|
|
add_entities([PALoopbackSwitch(name, server, sink_name, source_name)], True)
|
|
|
|
|
|
class PALoopbackSwitch(SwitchEntity):
|
|
"""Representation the presence or absence of a PA loopback module."""
|
|
|
|
def __init__(self, name, pa_server, sink_name, source_name):
|
|
"""Initialize the Pulseaudio switch."""
|
|
self._module_idx = None
|
|
self._name = name
|
|
self._sink_name = sink_name
|
|
self._source_name = source_name
|
|
self._pa_svr = pa_server
|
|
|
|
def _get_module_idx(self):
|
|
try:
|
|
self._pa_svr.connect()
|
|
|
|
for module in self._pa_svr.module_list():
|
|
if not module.name == "module-loopback":
|
|
continue
|
|
|
|
if f"sink={self._sink_name}" not in module.argument:
|
|
continue
|
|
|
|
if f"source={self._source_name}" not in module.argument:
|
|
continue
|
|
|
|
return module.index
|
|
|
|
except PulseError:
|
|
return None
|
|
|
|
return None
|
|
|
|
@property
|
|
def available(self):
|
|
"""Return true when connected to server."""
|
|
return self._pa_svr.connected
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the switch."""
|
|
return self._name
|
|
|
|
@property
|
|
def is_on(self):
|
|
"""Return true if device is on."""
|
|
return self._module_idx is not None
|
|
|
|
def turn_on(self, **kwargs):
|
|
"""Turn the device on."""
|
|
if not self.is_on:
|
|
self._pa_svr.module_load(
|
|
"module-loopback",
|
|
args=f"sink={self._sink_name} source={self._source_name}",
|
|
)
|
|
else:
|
|
_LOGGER.warning(IGNORED_SWITCH_WARN)
|
|
|
|
def turn_off(self, **kwargs):
|
|
"""Turn the device off."""
|
|
if self.is_on:
|
|
self._pa_svr.module_unload(self._module_idx)
|
|
else:
|
|
_LOGGER.warning(IGNORED_SWITCH_WARN)
|
|
|
|
def update(self):
|
|
"""Refresh state in case an alternate process modified this data."""
|
|
self._module_idx = self._get_module_idx()
|