Add timers
This commit is contained in:
@@ -1 +1 @@
|
||||
1.2.0
|
||||
1.3.0
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user