Home Assistant Unofficial Reference 2024.12.1
assist_satellite.py
Go to the documentation of this file.
1 """Assist satellite entity for Wyoming integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import AsyncGenerator
7 import io
8 import logging
9 from typing import Any, Final
10 import wave
11 
12 from wyoming.asr import Transcribe, Transcript
13 from wyoming.audio import AudioChunk, AudioChunkConverter, AudioStart, AudioStop
14 from wyoming.client import AsyncTcpClient
15 from wyoming.error import Error
16 from wyoming.event import Event
17 from wyoming.info import Describe, Info
18 from wyoming.ping import Ping, Pong
19 from wyoming.pipeline import PipelineStage, RunPipeline
20 from wyoming.satellite import PauseSatellite, RunSatellite
21 from wyoming.snd import Played
22 from wyoming.timer import TimerCancelled, TimerFinished, TimerStarted, TimerUpdated
23 from wyoming.tts import Synthesize, SynthesizeVoice
24 from wyoming.vad import VoiceStarted, VoiceStopped
25 from wyoming.wake import Detect, Detection
26 
27 from homeassistant.components import assist_pipeline, intent, tts
28 from homeassistant.components.assist_pipeline import PipelineEvent
30  AssistSatelliteConfiguration,
31  AssistSatelliteEntity,
32  AssistSatelliteEntityDescription,
33 )
34 from homeassistant.config_entries import ConfigEntry
35 from homeassistant.core import HomeAssistant, callback
36 from homeassistant.helpers.entity_platform import AddEntitiesCallback
37 
38 from .const import DOMAIN
39 from .data import WyomingService
40 from .devices import SatelliteDevice
41 from .entity import WyomingSatelliteEntity
42 from .models import DomainDataItem
43 
44 _LOGGER = logging.getLogger(__name__)
45 
46 _SAMPLES_PER_CHUNK: Final = 1024
47 _RECONNECT_SECONDS: Final = 10
48 _RESTART_SECONDS: Final = 3
49 _PING_TIMEOUT: Final = 5
50 _PING_SEND_DELAY: Final = 2
51 _PIPELINE_FINISH_TIMEOUT: Final = 1
52 
53 # Wyoming stage -> Assist stage
54 _STAGES: dict[PipelineStage, assist_pipeline.PipelineStage] = {
55  PipelineStage.WAKE: assist_pipeline.PipelineStage.WAKE_WORD,
56  PipelineStage.ASR: assist_pipeline.PipelineStage.STT,
57  PipelineStage.HANDLE: assist_pipeline.PipelineStage.INTENT,
58  PipelineStage.TTS: assist_pipeline.PipelineStage.TTS,
59 }
60 
61 
63  hass: HomeAssistant,
64  config_entry: ConfigEntry,
65  async_add_entities: AddEntitiesCallback,
66 ) -> None:
67  """Set up Wyoming Assist satellite entity."""
68  domain_data: DomainDataItem = hass.data[DOMAIN][config_entry.entry_id]
69  assert domain_data.device is not None
70 
72  [
74  hass, domain_data.service, domain_data.device, config_entry
75  )
76  ]
77  )
78 
79 
81  """Assist satellite for Wyoming devices."""
82 
83  entity_description = AssistSatelliteEntityDescription(key="assist_satellite")
84  _attr_translation_key = "assist_satellite"
85  _attr_name = None
86 
87  def __init__(
88  self,
89  hass: HomeAssistant,
90  service: WyomingService,
91  device: SatelliteDevice,
92  config_entry: ConfigEntry,
93  ) -> None:
94  """Initialize an Assist satellite."""
95  WyomingSatelliteEntity.__init__(self, device)
96  AssistSatelliteEntity.__init__(self)
97 
98  self.serviceservice = service
99  self.devicedevice = device
100  self.config_entryconfig_entry = config_entry
101 
102  self.is_runningis_running = True
103 
104  self._client_client: AsyncTcpClient | None = None
105  self._chunk_converter_chunk_converter = AudioChunkConverter(rate=16000, width=2, channels=1)
106  self._is_pipeline_running_is_pipeline_running = False
107  self._pipeline_ended_event_pipeline_ended_event = asyncio.Event()
108  self._audio_queue_audio_queue: asyncio.Queue[bytes | None] = asyncio.Queue()
109  self._pipeline_id: str | None = None
110  self._muted_changed_event_muted_changed_event = asyncio.Event()
111 
112  self._conversation_id_conversation_id: str | None = None
113  self._conversation_id_time_conversation_id_time: float | None = None
114 
115  self.devicedevice.set_is_muted_listener(self._muted_changed_muted_changed)
116  self.devicedevice.set_pipeline_listener(self._pipeline_changed_pipeline_changed)
117  self.devicedevice.set_audio_settings_listener(self._audio_settings_changed_audio_settings_changed)
118 
119  @property
120  def pipeline_entity_id(self) -> str | None:
121  """Return the entity ID of the pipeline to use for the next conversation."""
122  return self.devicedevice.get_pipeline_entity_id(self.hass)
123 
124  @property
125  def vad_sensitivity_entity_id(self) -> str | None:
126  """Return the entity ID of the VAD sensitivity to use for the next conversation."""
127  return self.devicedevice.get_vad_sensitivity_entity_id(self.hass)
128 
129  @property
130  def tts_options(self) -> dict[str, Any] | None:
131  """Options passed for text-to-speech."""
132  return {
133  tts.ATTR_PREFERRED_FORMAT: "wav",
134  tts.ATTR_PREFERRED_SAMPLE_RATE: 16000,
135  tts.ATTR_PREFERRED_SAMPLE_CHANNELS: 1,
136  tts.ATTR_PREFERRED_SAMPLE_BYTES: 2,
137  }
138 
139  async def async_added_to_hass(self) -> None:
140  """Run when entity about to be added to hass."""
141  await super().async_added_to_hass()
142  self.start_satellitestart_satellite()
143 
144  async def async_will_remove_from_hass(self) -> None:
145  """Run when entity will be removed from hass."""
146  await super().async_will_remove_from_hass()
147  self.stop_satellitestop_satellite()
148 
149  @callback
151  self,
152  ) -> AssistSatelliteConfiguration:
153  """Get the current satellite configuration."""
154  raise NotImplementedError
155 
157  self, config: AssistSatelliteConfiguration
158  ) -> None:
159  """Set the current satellite configuration."""
160  raise NotImplementedError
161 
162  def on_pipeline_event(self, event: PipelineEvent) -> None:
163  """Set state based on pipeline stage."""
164  assert self._client_client is not None
165 
166  if event.type == assist_pipeline.PipelineEventType.RUN_END:
167  # Pipeline run is complete
168  self._is_pipeline_running_is_pipeline_running = False
169  self._pipeline_ended_event_pipeline_ended_event.set()
170  self.devicedevice.set_is_active(False)
171  elif event.type == assist_pipeline.PipelineEventType.WAKE_WORD_START:
172  self.hass.add_job(self._client_client.write_event(Detect().event()))
173  elif event.type == assist_pipeline.PipelineEventType.WAKE_WORD_END:
174  # Wake word detection
175  # Inform client of wake word detection
176  if event.data and (wake_word_output := event.data.get("wake_word_output")):
177  detection = Detection(
178  name=wake_word_output["wake_word_id"],
179  timestamp=wake_word_output.get("timestamp"),
180  )
181  self.hass.add_job(self._client_client.write_event(detection.event()))
182  elif event.type == assist_pipeline.PipelineEventType.STT_START:
183  # Speech-to-text
184  self.devicedevice.set_is_active(True)
185 
186  if event.data:
187  self.hass.add_job(
188  self._client_client.write_event(
189  Transcribe(language=event.data["metadata"]["language"]).event()
190  )
191  )
192  elif event.type == assist_pipeline.PipelineEventType.STT_VAD_START:
193  # User started speaking
194  if event.data:
195  self.hass.add_job(
196  self._client_client.write_event(
197  VoiceStarted(timestamp=event.data["timestamp"]).event()
198  )
199  )
200  elif event.type == assist_pipeline.PipelineEventType.STT_VAD_END:
201  # User stopped speaking
202  if event.data:
203  self.hass.add_job(
204  self._client_client.write_event(
205  VoiceStopped(timestamp=event.data["timestamp"]).event()
206  )
207  )
208  elif event.type == assist_pipeline.PipelineEventType.STT_END:
209  # Speech-to-text transcript
210  if event.data:
211  # Inform client of transript
212  stt_text = event.data["stt_output"]["text"]
213  self.hass.add_job(
214  self._client_client.write_event(Transcript(text=stt_text).event())
215  )
216  elif event.type == assist_pipeline.PipelineEventType.TTS_START:
217  # Text-to-speech text
218  if event.data:
219  # Inform client of text
220  self.hass.add_job(
221  self._client_client.write_event(
222  Synthesize(
223  text=event.data["tts_input"],
224  voice=SynthesizeVoice(
225  name=event.data.get("voice"),
226  language=event.data.get("language"),
227  ),
228  ).event()
229  )
230  )
231  elif event.type == assist_pipeline.PipelineEventType.TTS_END:
232  # TTS stream
233  if event.data and (tts_output := event.data["tts_output"]):
234  media_id = tts_output["media_id"]
235  self.hass.add_job(self._stream_tts_stream_tts(media_id))
236  elif event.type == assist_pipeline.PipelineEventType.ERROR:
237  # Pipeline error
238  if event.data:
239  self.hass.add_job(
240  self._client_client.write_event(
241  Error(
242  text=event.data["message"], code=event.data["code"]
243  ).event()
244  )
245  )
246 
247  # -------------------------------------------------------------------------
248 
249  def start_satellite(self) -> None:
250  """Start satellite task."""
251  self.is_runningis_running = True
252 
253  self.config_entryconfig_entry.async_create_background_task(
254  self.hass, self.runrun(), "wyoming satellite run"
255  )
256 
257  def stop_satellite(self) -> None:
258  """Signal satellite task to stop running."""
259  # Stop existing pipeline
260  self._audio_queue_audio_queue.put_nowait(None)
261 
262  # Tell satellite to stop running
263  self._send_pause_send_pause()
264 
265  # Stop task loop
266  self.is_runningis_running = False
267 
268  # Unblock waiting for unmuted
269  self._muted_changed_event_muted_changed_event.set()
270 
271  # -------------------------------------------------------------------------
272 
273  async def run(self) -> None:
274  """Run and maintain a connection to satellite."""
275  _LOGGER.debug("Running satellite task")
276 
277  unregister_timer_handler = intent.async_register_timer_handler(
278  self.hass, self.devicedevice.device_id, self._handle_timer_handle_timer
279  )
280 
281  try:
282  while self.is_runningis_running:
283  try:
284  # Check if satellite has been muted
285  while self.devicedevice.is_muted:
286  _LOGGER.debug("Satellite is muted")
287  await self.on_mutedon_muted()
288  if not self.is_runningis_running:
289  # Satellite was stopped while waiting to be unmuted
290  return
291 
292  # Connect and run pipeline loop
293  await self._connect_and_loop_connect_and_loop()
294  except asyncio.CancelledError:
295  raise # don't restart
296  except Exception as err: # noqa: BLE001
297  _LOGGER.debug("%s: %s", err.__class__.__name__, str(err))
298 
299  # Stop any existing pipeline
300  self._audio_queue_audio_queue.put_nowait(None)
301 
302  # Ensure sensor is off (before restart)
303  self.devicedevice.set_is_active(False)
304 
305  # Wait to restart
306  await self.on_restarton_restart()
307  finally:
308  unregister_timer_handler()
309 
310  # Ensure sensor is off (before stop)
311  self.devicedevice.set_is_active(False)
312 
313  await self.on_stoppedon_stopped()
314 
315  async def on_restart(self) -> None:
316  """Block until pipeline loop will be restarted."""
317  _LOGGER.warning(
318  "Satellite has been disconnected. Reconnecting in %s second(s)",
319  _RECONNECT_SECONDS,
320  )
321  await asyncio.sleep(_RESTART_SECONDS)
322 
323  async def on_reconnect(self) -> None:
324  """Block until a reconnection attempt should be made."""
325  _LOGGER.debug(
326  "Failed to connect to satellite. Reconnecting in %s second(s)",
327  _RECONNECT_SECONDS,
328  )
329  await asyncio.sleep(_RECONNECT_SECONDS)
330 
331  async def on_muted(self) -> None:
332  """Block until device may be unmuted again."""
333  await self._muted_changed_event_muted_changed_event.wait()
334 
335  async def on_stopped(self) -> None:
336  """Run when run() has fully stopped."""
337  _LOGGER.debug("Satellite task stopped")
338 
339  # -------------------------------------------------------------------------
340 
341  def _send_pause(self) -> None:
342  """Send a pause message to satellite."""
343  if self._client_client is not None:
344  self.config_entryconfig_entry.async_create_background_task(
345  self.hass,
346  self._client_client.write_event(PauseSatellite().event()),
347  "pause satellite",
348  )
349 
350  def _muted_changed(self) -> None:
351  """Run when device muted status changes."""
352  if self.devicedevice.is_muted:
353  # Cancel any running pipeline
354  self._audio_queue_audio_queue.put_nowait(None)
355 
356  # Send pause event so satellite can react immediately
357  self._send_pause_send_pause()
358 
359  self._muted_changed_event_muted_changed_event.set()
360  self._muted_changed_event_muted_changed_event.clear()
361 
362  def _pipeline_changed(self) -> None:
363  """Run when device pipeline changes."""
364 
365  # Cancel any running pipeline
366  self._audio_queue_audio_queue.put_nowait(None)
367 
368  def _audio_settings_changed(self) -> None:
369  """Run when device audio settings."""
370 
371  # Cancel any running pipeline
372  self._audio_queue_audio_queue.put_nowait(None)
373 
374  async def _connect_and_loop(self) -> None:
375  """Connect to satellite and run pipelines until an error occurs."""
376  while self.is_runningis_running and (not self.devicedevice.is_muted):
377  try:
378  await self._connect_connect()
379  break
380  except ConnectionError:
381  self._client_client = None # client is not valid
382 
383  await self.on_reconnecton_reconnect()
384 
385  if self._client_client is None:
386  return
387 
388  _LOGGER.debug("Connected to satellite")
389 
390  if (not self.is_runningis_running) or self.devicedevice.is_muted:
391  # Run was cancelled or satellite was disabled during connection
392  return
393 
394  # Tell satellite that we're ready
395  await self._client_client.write_event(RunSatellite().event())
396 
397  # Run until stopped or muted
398  while self.is_runningis_running and (not self.devicedevice.is_muted):
399  await self._run_pipeline_loop_run_pipeline_loop()
400 
401  async def _run_pipeline_loop(self) -> None:
402  """Run a pipeline one or more times."""
403  assert self._client_client is not None
404  client_info: Info | None = None
405  wake_word_phrase: str | None = None
406  run_pipeline: RunPipeline | None = None
407  send_ping = True
408 
409  # Read events and check for pipeline end in parallel
410  pipeline_ended_task = self.config_entryconfig_entry.async_create_background_task(
411  self.hass, self._pipeline_ended_event_pipeline_ended_event.wait(), "satellite pipeline ended"
412  )
413  client_event_task = self.config_entryconfig_entry.async_create_background_task(
414  self.hass, self._client_client.read_event(), "satellite event read"
415  )
416  pending = {pipeline_ended_task, client_event_task}
417 
418  # Update info from satellite
419  await self._client_client.write_event(Describe().event())
420 
421  while self.is_runningis_running and (not self.devicedevice.is_muted):
422  if send_ping:
423  # Ensure satellite is still connected
424  send_ping = False
425  self.config_entryconfig_entry.async_create_background_task(
426  self.hass, self._send_delayed_ping_send_delayed_ping(), "ping satellite"
427  )
428 
429  async with asyncio.timeout(_PING_TIMEOUT):
430  done, pending = await asyncio.wait(
431  pending, return_when=asyncio.FIRST_COMPLETED
432  )
433 
434  if pipeline_ended_task in done:
435  # Pipeline run end event was received
436  _LOGGER.debug("Pipeline finished")
437  self._pipeline_ended_event_pipeline_ended_event.clear()
438  pipeline_ended_task = (
439  self.config_entryconfig_entry.async_create_background_task(
440  self.hass,
441  self._pipeline_ended_event_pipeline_ended_event.wait(),
442  "satellite pipeline ended",
443  )
444  )
445  pending.add(pipeline_ended_task)
446 
447  # Clear last wake word detection
448  wake_word_phrase = None
449 
450  if (run_pipeline is not None) and run_pipeline.restart_on_end:
451  # Automatically restart pipeline.
452  # Used with "always on" streaming satellites.
453  self._run_pipeline_once_run_pipeline_once(run_pipeline)
454  continue
455 
456  if client_event_task not in done:
457  continue
458 
459  client_event = client_event_task.result()
460  if client_event is None:
461  raise ConnectionResetError("Satellite disconnected")
462 
463  if Pong.is_type(client_event.type):
464  # Satellite is still there, send next ping
465  send_ping = True
466  elif Ping.is_type(client_event.type):
467  # Respond to ping from satellite
468  ping = Ping.from_event(client_event)
469  await self._client_client.write_event(Pong(text=ping.text).event())
470  elif RunPipeline.is_type(client_event.type):
471  # Satellite requested pipeline run
472  run_pipeline = RunPipeline.from_event(client_event)
473  self._run_pipeline_once_run_pipeline_once(run_pipeline, wake_word_phrase)
474  elif (
475  AudioChunk.is_type(client_event.type) and self._is_pipeline_running_is_pipeline_running
476  ):
477  # Microphone audio
478  chunk = AudioChunk.from_event(client_event)
479  chunk = self._chunk_converter_chunk_converter.convert(chunk)
480  self._audio_queue_audio_queue.put_nowait(chunk.audio)
481  elif AudioStop.is_type(client_event.type) and self._is_pipeline_running_is_pipeline_running:
482  # Stop pipeline
483  _LOGGER.debug("Client requested pipeline to stop")
484  self._audio_queue_audio_queue.put_nowait(None)
485  elif Info.is_type(client_event.type):
486  client_info = Info.from_event(client_event)
487  _LOGGER.debug("Updated client info: %s", client_info)
488  elif Detection.is_type(client_event.type):
489  detection = Detection.from_event(client_event)
490  wake_word_phrase = detection.name
491 
492  # Resolve wake word name/id to phrase if info is available.
493  #
494  # This allows us to deconflict multiple satellite wake-ups
495  # with the same wake word.
496  if (client_info is not None) and (client_info.wake is not None):
497  found_phrase = False
498  for wake_service in client_info.wake:
499  for wake_model in wake_service.models:
500  if wake_model.name == detection.name:
501  wake_word_phrase = (
502  wake_model.phrase or wake_model.name
503  )
504  found_phrase = True
505  break
506 
507  if found_phrase:
508  break
509 
510  _LOGGER.debug("Client detected wake word: %s", wake_word_phrase)
511  elif Played.is_type(client_event.type):
512  # TTS response has finished playing on satellite
513  self.tts_response_finishedtts_response_finished()
514  else:
515  _LOGGER.debug("Unexpected event from satellite: %s", client_event)
516 
517  # Next event
518  client_event_task = self.config_entryconfig_entry.async_create_background_task(
519  self.hass, self._client_client.read_event(), "satellite event read"
520  )
521  pending.add(client_event_task)
522 
524  self, run_pipeline: RunPipeline, wake_word_phrase: str | None = None
525  ) -> None:
526  """Run a pipeline once."""
527  _LOGGER.debug("Received run information: %s", run_pipeline)
528 
529  start_stage = _STAGES.get(run_pipeline.start_stage)
530  end_stage = _STAGES.get(run_pipeline.end_stage)
531 
532  if start_stage is None:
533  raise ValueError(f"Invalid start stage: {start_stage}")
534 
535  if end_stage is None:
536  raise ValueError(f"Invalid end stage: {end_stage}")
537 
538  # We will push audio in through a queue
539  self._audio_queue_audio_queue = asyncio.Queue()
540 
541  self._is_pipeline_running_is_pipeline_running = True
542  self._pipeline_ended_event_pipeline_ended_event.clear()
543  self.config_entryconfig_entry.async_create_background_task(
544  self.hass,
545  self.async_accept_pipeline_from_satelliteasync_accept_pipeline_from_satellite(
546  audio_stream=self._stt_stream_stt_stream(),
547  start_stage=start_stage,
548  end_stage=end_stage,
549  wake_word_phrase=wake_word_phrase,
550  ),
551  "wyoming satellite pipeline",
552  )
553 
554  async def _send_delayed_ping(self) -> None:
555  """Send ping to satellite after a delay."""
556  assert self._client_client is not None
557 
558  try:
559  await asyncio.sleep(_PING_SEND_DELAY)
560  await self._client_client.write_event(Ping().event())
561  except ConnectionError:
562  pass # handled with timeout
563 
564  async def _connect(self) -> None:
565  """Connect to satellite over TCP."""
566  await self._disconnect_disconnect()
567 
568  _LOGGER.debug(
569  "Connecting to satellite at %s:%s", self.serviceservice.host, self.serviceservice.port
570  )
571  self._client_client = AsyncTcpClient(self.serviceservice.host, self.serviceservice.port)
572  await self._client_client.connect()
573 
574  async def _disconnect(self) -> None:
575  """Disconnect if satellite is currently connected."""
576  if self._client_client is None:
577  return
578 
579  _LOGGER.debug("Disconnecting from satellite")
580  await self._client_client.disconnect()
581  self._client_client = None
582 
583  async def _stream_tts(self, media_id: str) -> None:
584  """Stream TTS WAV audio to satellite in chunks."""
585  assert self._client_client is not None
586 
587  extension, data = await tts.async_get_media_source_audio(self.hass, media_id)
588  if extension != "wav":
589  raise ValueError(f"Cannot stream audio format to satellite: {extension}")
590 
591  with io.BytesIO(data) as wav_io, wave.open(wav_io, "rb") as wav_file:
592  sample_rate = wav_file.getframerate()
593  sample_width = wav_file.getsampwidth()
594  sample_channels = wav_file.getnchannels()
595  _LOGGER.debug("Streaming %s TTS sample(s)", wav_file.getnframes())
596 
597  timestamp = 0
598  await self._client_client.write_event(
599  AudioStart(
600  rate=sample_rate,
601  width=sample_width,
602  channels=sample_channels,
603  timestamp=timestamp,
604  ).event()
605  )
606 
607  # Stream audio chunks
608  while audio_bytes := wav_file.readframes(_SAMPLES_PER_CHUNK):
609  chunk = AudioChunk(
610  rate=sample_rate,
611  width=sample_width,
612  channels=sample_channels,
613  audio=audio_bytes,
614  timestamp=timestamp,
615  )
616  await self._client_client.write_event(chunk.event())
617  timestamp += chunk.seconds
618 
619  await self._client_client.write_event(AudioStop(timestamp=timestamp).event())
620  _LOGGER.debug("TTS streaming complete")
621 
622  async def _stt_stream(self) -> AsyncGenerator[bytes]:
623  """Yield audio chunks from a queue."""
624  is_first_chunk = True
625  while chunk := await self._audio_queue_audio_queue.get():
626  if chunk is None:
627  break
628 
629  if is_first_chunk:
630  is_first_chunk = False
631  _LOGGER.debug("Receiving audio from satellite")
632 
633  yield chunk
634 
635  @callback
637  self, event_type: intent.TimerEventType, timer: intent.TimerInfo
638  ) -> None:
639  """Forward timer events to satellite."""
640  assert self._client_client is not None
641 
642  _LOGGER.debug("Timer event: type=%s, info=%s", event_type, timer)
643  event: Event | None = None
644  if event_type == intent.TimerEventType.STARTED:
645  event = TimerStarted(
646  id=timer.id,
647  total_seconds=timer.seconds,
648  name=timer.name,
649  start_hours=timer.start_hours,
650  start_minutes=timer.start_minutes,
651  start_seconds=timer.start_seconds,
652  ).event()
653  elif event_type == intent.TimerEventType.UPDATED:
654  event = TimerUpdated(
655  id=timer.id,
656  is_active=timer.is_active,
657  total_seconds=timer.seconds,
658  ).event()
659  elif event_type == intent.TimerEventType.CANCELLED:
660  event = TimerCancelled(id=timer.id).event()
661  elif event_type == intent.TimerEventType.FINISHED:
662  event = TimerFinished(id=timer.id).event()
663 
664  if event is not None:
665  # Send timer event to satellite
666  self.config_entryconfig_entry.async_create_background_task(
667  self.hass, self._client_client.write_event(event), "wyoming timer event"
668  )
None async_accept_pipeline_from_satellite(self, AsyncIterable[bytes] audio_stream, PipelineStage start_stage=PipelineStage.STT, PipelineStage end_stage=PipelineStage.TTS, str|None wake_word_phrase=None)
Definition: entity.py:260
None _run_pipeline_once(self, RunPipeline run_pipeline, str|None wake_word_phrase=None)
None _handle_timer(self, intent.TimerEventType event_type, intent.TimerInfo timer)
None async_set_configuration(self, AssistSatelliteConfiguration config)
None __init__(self, HomeAssistant hass, WyomingService service, SatelliteDevice device, ConfigEntry config_entry)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)