137 lines
4.0 KiB
Python
137 lines
4.0 KiB
Python
"""Switch logic for loading/unloading pulseaudio loopback modules."""
|
|
from __future__ import annotations
|
|
|
|
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
|
|
from homeassistant.core import HomeAssistant
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
|
|
|
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: HomeAssistant,
|
|
config: ConfigType,
|
|
add_entities: AddEntitiesCallback,
|
|
discovery_info: DiscoveryInfoType | None = None,
|
|
) -> 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 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()
|