Files
linux-voice-assistant/linux_voice_assistant/config.py
T
imonlinux eb4c4c26c8 Update config.py
What this fixes:

On boot, if PipeWire/Pulse starts with the sink at ~40%, LVA now pushes the sink volume to match the persisted LVA volume (0.0–1.0).

When you move the HA media_player volume slider, LVA now updates:

wpctl (PipeWire) if available

pactl (PulseAudio / PipeWire-Pulse) if available

amixer (ALSA) as a fallback

No reboot / alsactl store required — LVA simply re-applies the persisted volume at start.
2026-01-05 21:51:21 -08:00

152 lines
5.2 KiB
Python

from abc import abstractmethod
from collections.abc import Iterable
from typing import TYPE_CHECKING, Callable, List, Optional, Union
# pylint: disable=no-name-in-module
from aioesphomeapi.api_pb2 import ( # type: ignore[attr-defined]
ListEntitiesMediaPlayerResponse,
ListEntitiesRequest,
MediaPlayerCommandRequest,
MediaPlayerStateResponse,
SubscribeHomeAssistantStatesRequest,
)
from aioesphomeapi.model import MediaPlayerCommand, MediaPlayerState
from google.protobuf import message
from .api_server import APIServer
from .mpv_player import MpvMediaPlayer
from .util import call_all
if TYPE_CHECKING:
from .models import ServerState
class ESPHomeEntity:
def __init__(self, server: APIServer, state: "ServerState") -> None:
self.server = server
self.state = state
@abstractmethod
def handle_message(self, msg: message.Message) -> Iterable[message.Message]:
pass
# -----------------------------------------------------------------------------
class MediaPlayerEntity(ESPHomeEntity):
def __init__(
self,
server: APIServer,
state: "ServerState",
key: int,
name: str,
object_id: str,
music_player: MpvMediaPlayer,
announce_player: MpvMediaPlayer,
) -> None:
super().__init__(server, state)
self.key = key
self.name = name
self.object_id = object_id
self.state_enum = MediaPlayerState.IDLE
self.volume = state.preferences.volume_level # Initialize with saved volume
self.muted = False
self.music_player = music_player
self.announce_player = announce_player
def play(
self,
url: Union[str, List[str]],
announcement: bool = False,
done_callback: Optional[Callable[[], None]] = None,
) -> Iterable[message.Message]:
if announcement:
if self.music_player.is_playing:
# Announce, resume music
self.music_player.pause()
self.announce_player.play(
url,
done_callback=lambda: call_all(
self.music_player.resume, done_callback
),
)
else:
# Announce, idle
self.announce_player.play(
url,
done_callback=lambda: call_all(
self.server.send_messages(
[self._update_state(MediaPlayerState.IDLE)]
),
done_callback,
),
)
else:
# Music
self.music_player.play(
url,
done_callback=lambda: call_all(
self.server.send_messages(
[self._update_state(MediaPlayerState.IDLE)]
),
done_callback,
),
)
yield self._update_state(MediaPlayerState.PLAYING)
def handle_message(self, msg: message.Message) -> Iterable[message.Message]:
if isinstance(msg, MediaPlayerCommandRequest) and (msg.key == self.key):
if msg.has_media_url:
announcement = msg.has_announcement and msg.announcement
yield from self.play(msg.media_url, announcement=announcement)
elif msg.has_command:
if msg.command == MediaPlayerCommand.PAUSE:
self.music_player.pause()
yield self._update_state(MediaPlayerState.PAUSED)
elif msg.command == MediaPlayerCommand.PLAY:
self.music_player.resume()
yield self._update_state(MediaPlayerState.PLAYING)
if msg.has_volume:
# HA sends volume as 0.0-1.0. We treat this as the *master*
# output volume and sync it to the OS audio backend.
self.volume = msg.volume
# Keep mpv at 100% for normal playback; ducking uses mpv volume.
self.music_player.set_volume(100)
self.announce_player.set_volume(100)
# Apply master volume to the system sink (PipeWire/Pulse/ALSA).
self.music_player.set_master_volume(self.volume)
# Save the new volume level to preferences
self.state.preferences.volume_level = self.volume
self.state.save_preferences()
yield self._update_state(self.state_enum)
elif isinstance(msg, ListEntitiesRequest):
yield ListEntitiesMediaPlayerResponse(
object_id=self.object_id,
key=self.key,
name=self.name,
supports_pause=True,
)
elif isinstance(msg, SubscribeHomeAssistantStatesRequest):
yield self._get_state_message()
def _update_state(self, new_state: MediaPlayerState) -> MediaPlayerStateResponse:
self.state_enum = new_state
return self._get_state_message()
def _get_state_message(self) -> MediaPlayerStateResponse:
return MediaPlayerStateResponse(
key=self.key,
state=self.state_enum,
volume=self.volume,
muted=self.muted,
)