mirror of
https://github.com/home-assistant/core.git
synced 2025-11-12 20:40:18 +00:00
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).
127 lines
3.6 KiB
Python
127 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()
|