Add timers

This commit is contained in:
Michael Hansen
2024-05-24 15:14:15 -05:00
parent 9b35e0334f
commit 16847ef5e9
8 changed files with 127 additions and 7 deletions
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## 1.3.0
- Add support for voice timers
## 1.2.0
- Add `--tts-played-command`
+1 -1
View File
@@ -1,3 +1,3 @@
wyoming==1.5.3
wyoming==1.5.4
zeroconf==0.88.0
pyring-buffer==1.0.0
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
1.2.0
1.3.0
+48 -3
View File
@@ -1,4 +1,5 @@
"""Main entry point for Wyoming satellite."""
import argparse
import asyncio
import logging
@@ -22,6 +23,7 @@ from .settings import (
MicSettings,
SatelliteSettings,
SndSettings,
TimerSettings,
VadSettings,
WakeSettings,
WakeWordAndPipeline,
@@ -225,6 +227,23 @@ async def main() -> None:
"--disconnected-command",
help="Command to run when disconnected from the server",
)
parser.add_argument(
"--timer-started-command",
help="Command to run when a timer starts",
)
parser.add_argument(
"--timer-updated-command",
help="Command to run when a timer is paused, resumed, or has time added or removed",
)
parser.add_argument(
"--timer-cancelled-command",
"--timer-canceled-command",
help="Command to run when a timer is cancelled",
)
parser.add_argument(
"--timer-finished-command",
help="Command to run when a timer finishes",
)
# Sounds
parser.add_argument(
@@ -233,6 +252,17 @@ async def main() -> None:
parser.add_argument(
"--done-wav", help="WAV file to play when voice command is done"
)
parser.add_argument(
"--timer-finished-wav", help="WAV file to play when a timer finishes"
)
parser.add_argument(
"--timer-finished-wav-repeat",
nargs=2,
metavar=("repeat", "delay"),
type=float,
default=(1, 0),
help="Number of times to play timer finished WAV and delay between repeats in seconds",
)
# Satellite details
parser.add_argument("--uri", required=True, help="unix:// or tcp://")
@@ -296,6 +326,10 @@ async def main() -> None:
_LOGGER.fatal("%s does not exist", args.done_wav)
sys.exit(1)
if args.timer_finished_wav and (not Path(args.timer_finished_wav).is_file()):
_LOGGER.fatal("%s does not exist", args.timer_finished_wav)
sys.exit(1)
if args.vad and (args.wake_uri or args.wake_command):
_LOGGER.warning("VAD is not used with local wake word detection")
@@ -347,9 +381,11 @@ async def main() -> None:
names=[
WakeWordAndPipeline(*wake_name) for wake_name in args.wake_word_name
],
refractory_seconds=args.wake_refractory_seconds
if args.wake_refractory_seconds > 0
else None,
refractory_seconds=(
args.wake_refractory_seconds
if args.wake_refractory_seconds > 0
else None
),
),
snd=SndSettings(
uri=args.snd_uri,
@@ -379,6 +415,15 @@ async def main() -> None:
connected=split_command(args.connected_command),
disconnected=split_command(args.disconnected_command),
),
timer=TimerSettings(
started=split_command(args.timer_started_command),
updated=split_command(args.timer_updated_command),
cancelled=split_command(args.timer_cancelled_command),
finished=split_command(args.timer_finished_command),
finished_wav=args.timer_finished_wav,
finished_wav_plays=int(args.timer_finished_wav_repeat[0]),
finished_wav_delay=args.timer_finished_wav_repeat[1],
),
debug_recording_dir=args.debug_recording_dir,
)
+35
View File
@@ -28,6 +28,7 @@ from wyoming.satellite import (
StreamingStopped,
)
from wyoming.snd import Played, SndProcessAsyncClient
from wyoming.timer import TimerCancelled, TimerFinished, TimerStarted, TimerUpdated
from wyoming.tts import Synthesize
from wyoming.vad import VoiceStarted, VoiceStopped
from wyoming.wake import Detect, Detection, WakeProcessAsyncClient
@@ -289,6 +290,18 @@ class SatelliteBase:
elif Error.is_type(event.type):
_LOGGER.warning(event)
await self.trigger_error(Error.from_event(event))
elif TimerStarted.is_type(event.type):
_LOGGER.debug(event)
await self.trigger_timer_started(TimerStarted.from_event(event))
elif TimerUpdated.is_type(event.type):
_LOGGER.debug(event)
await self.trigger_timer_updated(TimerUpdated.from_event(event))
elif TimerCancelled.is_type(event.type):
_LOGGER.debug(event)
await self.trigger_timer_cancelled(TimerCancelled.from_event(event))
elif TimerFinished.is_type(event.type):
_LOGGER.debug(event)
await self.trigger_timer_finished(TimerFinished.from_event(event))
# Forward everything except audio to event service
if not AudioChunk.is_type(event.type):
@@ -852,6 +865,28 @@ class SatelliteBase:
"""Called when an error occurs on the server."""
await run_event_command(self.settings.event.error, error.text)
async def trigger_timer_started(self, timer_started: TimerStarted) -> None:
"""Called when timer-started event is received."""
await run_event_command(self.settings.timer.started, timer_started)
async def trigger_timer_updated(self, timer_updated: TimerUpdated) -> None:
"""Called when timer-updated event is received."""
await run_event_command(self.settings.timer.updated, timer_updated)
async def trigger_timer_cancelled(self, timer_cancelled: TimerCancelled) -> None:
"""Called when timer-cancelled event is received."""
await run_event_command(self.settings.timer.cancelled, timer_cancelled.id)
async def trigger_timer_finished(self, timer_finished: TimerFinished) -> None:
"""Called when timer-finished event is received."""
await run_event_command(self.settings.timer.finished, timer_finished.id)
for _ in range(self.settings.timer.finished_wav_plays):
await self._play_wav(
self.settings.timer.finished_wav,
mute_microphone=self.settings.mic.mute_during_awake_wav,
)
await asyncio.sleep(self.settings.timer.finished_wav_delay)
async def forward_event(self, event: Event) -> None:
"""Forward an event to the event service."""
if self._event_queue is not None:
+28
View File
@@ -1,4 +1,5 @@
"""Satellite settings."""
from abc import ABC
from dataclasses import dataclass, field
from pathlib import Path
@@ -172,6 +173,32 @@ class EventSettings(ServiceSettings):
disconnected: Optional[List[str]] = None
@dataclass(frozen=True)
class TimerSettings:
"""Voice timer settings."""
started: Optional[List[str]] = None
"""Command to run when a timer starts."""
updated: Optional[List[str]] = None
"""Command to run when a timer is paused, resumed, or has time added or removed."""
cancelled: Optional[List[str]] = None
"""Command to run when a timer is cancelled."""
finished: Optional[List[str]] = None
"""Command to run when a timer finishes."""
finished_wav: Optional[str] = None
"""WAV file to play when a timer finishes."""
finished_wav_plays: int = 1
"""Number of times to play finished WAV."""
finished_wav_delay: float = 0
"""Delay in seconds between repeats of finished WAV."""
@dataclass(frozen=True)
class SatelliteSettings:
"""Wyoming satellite settings."""
@@ -181,6 +208,7 @@ class SatelliteSettings:
wake: WakeSettings = field(default_factory=WakeSettings)
snd: SndSettings = field(default_factory=SndSettings)
event: EventSettings = field(default_factory=EventSettings)
timer: TimerSettings = field(default_factory=TimerSettings)
restart_timeout: float = 5.0
+10 -2
View File
@@ -1,24 +1,32 @@
"""Miscellaneous utilities."""
import argparse
import asyncio
import json
import logging
import re
import shlex
import unicodedata
import uuid
from functools import lru_cache
from typing import List, Optional
from typing import List, Optional, Union
from wyoming.event import Eventable
_LOGGER = logging.getLogger()
async def run_event_command(
command: Optional[List[str]], command_input: Optional[str] = None
command: Optional[List[str]], command_input: Optional[Union[str, Eventable]] = None
) -> None:
"""Run a custom event command with optional input."""
if not command:
return
if isinstance(command_input, Eventable):
# Convert event to JSON
event_dict = command_input.event().to_dict()
command_input = json.dumps(event_dict, ensure_ascii=False)
_LOGGER.debug("Running %s", command)
program, *program_args = command
proc = await asyncio.create_subprocess_exec(