Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add force activate button to wyoming satellite
Browse files Browse the repository at this point in the history
chatziko committed Jun 10, 2024
1 parent 460909a commit 59f2b30
Showing 5 changed files with 113 additions and 6 deletions.
1 change: 1 addition & 0 deletions homeassistant/components/wyoming/__init__.py
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@

SATELLITE_PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.SELECT,
Platform.SWITCH,
Platform.NUMBER,
43 changes: 43 additions & 0 deletions homeassistant/components/wyoming/button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Wyoming button entities."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import DOMAIN
from .entity import WyomingSatelliteEntity

if TYPE_CHECKING:
from .models import DomainDataItem


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up button entities."""
item: DomainDataItem = hass.data[DOMAIN][config_entry.entry_id]

# Setup is only forwarded for satellites
assert item.satellite is not None

async_add_entities([WyomingSatelliteForceActivate(item.satellite.device)])


class WyomingSatelliteForceActivate(WyomingSatelliteEntity, ButtonEntity):
"""Manually activate the satellite instead of using a wake word."""

entity_description = ButtonEntityDescription(
key="activate",
translation_key="activat",
)

async def async_press(self, **kwargs: Any) -> None:
"""Turn on."""
self._device.force_activate()
14 changes: 14 additions & 0 deletions homeassistant/components/wyoming/devices.py
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ class SatelliteDevice:

_is_active_listener: Callable[[], None] | None = None
_is_muted_listener: Callable[[], None] | None = None
_force_activate_listener: Callable[[], None] | None = None
_pipeline_listener: Callable[[], None] | None = None
_audio_settings_listener: Callable[[], None] | None = None

@@ -45,6 +46,12 @@ def set_is_muted(self, muted: bool) -> None:
if self._is_muted_listener is not None:
self._is_muted_listener()

@callback
def force_activate(self) -> None:
"""Request to activate without a wake word."""
if self._force_activate_listener is not None:
self._force_activate_listener()

@callback
def set_pipeline_name(self, pipeline_name: str) -> None:
"""Inform listeners that pipeline selection has changed."""
@@ -87,6 +94,13 @@ def set_is_muted_listener(self, is_muted_listener: Callable[[], None]) -> None:
"""Listen for updates to muted status."""
self._is_muted_listener = is_muted_listener

@callback
def set_force_activate_listener(
self, force_activate_listener: Callable[[], None]
) -> None:
"""Listen for force activate requests."""
self._force_activate_listener = force_activate_listener

@callback
def set_pipeline_listener(self, pipeline_listener: Callable[[], None]) -> None:
"""Listen for updates to pipeline."""
56 changes: 50 additions & 6 deletions homeassistant/components/wyoming/satellite.py
Original file line number Diff line number Diff line change
@@ -68,12 +68,14 @@ def __init__(
self._client: AsyncTcpClient | None = None
self._chunk_converter = AudioChunkConverter(rate=16000, width=2, channels=1)
self._is_pipeline_running = False
self._is_force_activated = False
self._pipeline_ended_event = asyncio.Event()
self._audio_queue: asyncio.Queue[bytes | None] = asyncio.Queue()
self._pipeline_id: str | None = None
self._muted_changed_event = asyncio.Event()

self.device.set_is_muted_listener(self._muted_changed)
self.device.set_force_activate_listener(self._force_activated)
self.device.set_pipeline_listener(self._pipeline_changed)
self.device.set_audio_settings_listener(self._audio_settings_changed)

@@ -89,7 +91,7 @@ async def run(self) -> None:
while self.is_running:
try:
# Check if satellite has been muted
while self.device.is_muted:
while not self._connection_needed():
_LOGGER.debug("Satellite is muted")
await self.on_muted()
if not self.is_running:
@@ -164,6 +166,10 @@ def _send_pause(self) -> None:

def _muted_changed(self) -> None:
"""Run when device muted status changes."""
if self._is_force_activated:
# Force activated pipeline running, muted state will be restored when it finishes
return

if self.device.is_muted:
# Cancel any running pipeline
self._audio_queue.put_nowait(None)
@@ -174,6 +180,34 @@ def _muted_changed(self) -> None:
self._muted_changed_event.set()
self._muted_changed_event.clear()

def _force_activated(self) -> None:
self.config_entry.async_create_background_task(
self.hass, self._run_force_activated(), "force activated pipeline"
)

async def _run_force_activated(self) -> None:
self._is_force_activated = True

if self.device.is_muted:
# We're muted, connect again, which will cause RunSatellite to be sent
self._muted_changed_event.set()
self._muted_changed_event.clear()

else:
# Not muted, finish the current pipeline, if any, and send RunSatellite
if self._is_pipeline_running:
self._audio_queue.put_nowait(None)
await self._pipeline_ended_event.wait()

if self._client is not None:
await self._client.write_event(
RunSatellite(start_stage=PipelineStage.ASR).event()
)

# Restore mute state when the force activated pipeline finishes
await self._pipeline_ended_event.wait()
self._muted_changed()

def _pipeline_changed(self) -> None:
"""Run when device pipeline changes."""

@@ -186,9 +220,14 @@ def _audio_settings_changed(self) -> None:
# Cancel any running pipeline
self._audio_queue.put_nowait(None)

def _connection_needed(self) -> bool:
return self.is_running and (
self._is_force_activated or not self.device.is_muted
)

async def _connect_and_loop(self) -> None:
"""Connect to satellite and run pipelines until an error occurs."""
while self.is_running and (not self.device.is_muted):
while self._connection_needed():
try:
await self._connect()
break
@@ -202,15 +241,19 @@ async def _connect_and_loop(self) -> None:

_LOGGER.debug("Connected to satellite")

if (not self.is_running) or self.device.is_muted:
if not self._connection_needed():
# Run was cancelled or satellite was disabled during connection
return

# Tell satellite that we're ready
await self._client.write_event(RunSatellite().event())
if self._is_force_activated:
start_stage = PipelineStage.ASR
else:
start_stage = None
await self._client.write_event(RunSatellite(start_stage=start_stage).event())

# Run until stopped or muted
while self.is_running and (not self.device.is_muted):
while self._connection_needed():
await self._run_pipeline_loop()

async def _run_pipeline_loop(self) -> None:
@@ -233,7 +276,7 @@ async def _run_pipeline_loop(self) -> None:
# Update info from satellite
await self._client.write_event(Describe().event())

while self.is_running and (not self.device.is_muted):
while self._connection_needed():
if send_ping:
# Ensure satellite is still connected
send_ping = False
@@ -413,6 +456,7 @@ def _event_callback(self, event: assist_pipeline.PipelineEvent) -> None:

if event.type == assist_pipeline.PipelineEventType.RUN_END:
# Pipeline run is complete
self._is_force_activated = False
self._is_pipeline_running = False
self._pipeline_ended_event.set()
self.device.set_is_active(False)
5 changes: 5 additions & 0 deletions homeassistant/components/wyoming/strings.json
Original file line number Diff line number Diff line change
@@ -53,6 +53,11 @@
"name": "Mute"
}
},
"button": {
"activat": {
"name": "Activate"
}
},
"number": {
"auto_gain": {
"name": "Auto gain"

0 comments on commit 59f2b30

Please sign in to comment.