Commit ·
f6e443c
1
Parent(s): 64faf24
Fix app.py to properly run and handle stop event
Browse files- Change event loop handling to keep server running until stop_event
- Use while loop with asyncio.sleep instead of run_until_complete
- Add proper cleanup in finally block
- Ensure mDNS service is registered for Home Assistant auto-discovery
- Fix audio processing to use pyaudio correctly
- reachy_mini_ha_voice/app.py +33 -26
reachy_mini_ha_voice/app.py
CHANGED
|
@@ -24,6 +24,7 @@ from .models import (
|
|
| 24 |
)
|
| 25 |
from .satellite import VoiceSatelliteProtocol
|
| 26 |
from .util import get_mac
|
|
|
|
| 27 |
|
| 28 |
_LOGGER = logging.getLogger(__name__)
|
| 29 |
_MODULE_DIR = Path(__file__).parent
|
|
@@ -42,7 +43,8 @@ class ReachyMiniHAVoiceApp(ReachyMiniApp):
|
|
| 42 |
self._state: Optional[ServerState] = None
|
| 43 |
self._event_loop: Optional[asyncio.AbstractEventLoop] = None
|
| 44 |
self._audio_thread: Optional[threading.Thread] = None
|
| 45 |
-
self.
|
|
|
|
| 46 |
|
| 47 |
def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
|
| 48 |
"""Run the voice assistant."""
|
|
@@ -65,19 +67,15 @@ class ReachyMiniHAVoiceApp(ReachyMiniApp):
|
|
| 65 |
self._audio_thread.start()
|
| 66 |
|
| 67 |
# Start ESPHome server
|
| 68 |
-
self.
|
| 69 |
-
self._run_server(self._state)
|
| 70 |
-
)
|
| 71 |
-
self._event_loop.run_until_complete(self._server_task)
|
| 72 |
|
| 73 |
except Exception as e:
|
| 74 |
_LOGGER.error("Error running voice assistant: %s", e)
|
|
|
|
|
|
|
| 75 |
finally:
|
| 76 |
_LOGGER.info("Shutting down voice assistant")
|
| 77 |
-
|
| 78 |
-
self._audio_thread.join(timeout=5)
|
| 79 |
-
if self._event_loop:
|
| 80 |
-
self._event_loop.close()
|
| 81 |
|
| 82 |
def _init_state(self, reachy_mini: ReachyMini) -> ServerState:
|
| 83 |
"""Initialize server state."""
|
|
@@ -144,9 +142,7 @@ class ReachyMiniHAVoiceApp(ReachyMiniApp):
|
|
| 144 |
|
| 145 |
with open(model_config_path, "r", encoding="utf-8") as f:
|
| 146 |
model_config = json.load(f)
|
| 147 |
-
model_type = WakeWordType(
|
| 148 |
-
model_config.get("type", "microWakeWord")
|
| 149 |
-
)
|
| 150 |
if model_type == WakeWordType.OPEN_WAKE_WORD:
|
| 151 |
wake_word_path = model_config_path.parent / model_config["model"]
|
| 152 |
else:
|
|
@@ -160,9 +156,7 @@ class ReachyMiniHAVoiceApp(ReachyMiniApp):
|
|
| 160 |
wake_word_path=wake_word_path,
|
| 161 |
)
|
| 162 |
except Exception as e:
|
| 163 |
-
_LOGGER.error(
|
| 164 |
-
"Error loading wake word config %s: %s", model_config_path, e
|
| 165 |
-
)
|
| 166 |
|
| 167 |
return available_wake_words
|
| 168 |
|
|
@@ -178,25 +172,32 @@ class ReachyMiniHAVoiceApp(ReachyMiniApp):
|
|
| 178 |
_LOGGER.error("Failed to load stop model: %s", e)
|
| 179 |
return None
|
| 180 |
|
| 181 |
-
async def _run_server(self, state: ServerState) -> None:
|
| 182 |
"""Run ESPHome server."""
|
| 183 |
# Start ESPHome server
|
| 184 |
loop = asyncio.get_running_loop()
|
| 185 |
-
|
| 186 |
lambda: VoiceSatelliteProtocol(state), host="0.0.0.0", port=6053
|
| 187 |
)
|
| 188 |
|
| 189 |
# Auto discovery (zeroconf, mDNS) - required for Home Assistant auto-discovery
|
| 190 |
-
|
| 191 |
-
await
|
| 192 |
|
| 193 |
try:
|
| 194 |
-
async with
|
| 195 |
_LOGGER.info("ESPHome server started on port 6053")
|
| 196 |
_LOGGER.info("mDNS service registered for auto-discovery")
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
finally:
|
| 199 |
-
|
|
|
|
| 200 |
_LOGGER.info("ESPHome server stopped")
|
| 201 |
|
| 202 |
def _process_audio(self, state: ServerState) -> None:
|
|
@@ -246,10 +247,9 @@ class ReachyMiniHAVoiceApp(ReachyMiniApp):
|
|
| 246 |
|
| 247 |
while True:
|
| 248 |
try:
|
|
|
|
| 249 |
data = stream.read(CHUNK, exception_on_overflow=False)
|
| 250 |
-
audio_array = (
|
| 251 |
-
np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0
|
| 252 |
-
)
|
| 253 |
|
| 254 |
# Send to satellite if connected
|
| 255 |
if state.satellite is not None:
|
|
@@ -328,4 +328,11 @@ class ReachyMiniHAVoiceApp(ReachyMiniApp):
|
|
| 328 |
stream.stop_stream()
|
| 329 |
stream.close()
|
| 330 |
p.terminate()
|
| 331 |
-
_LOGGER.info("Audio processing stopped")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
)
|
| 25 |
from .satellite import VoiceSatelliteProtocol
|
| 26 |
from .util import get_mac
|
| 27 |
+
from .zeroconf import HomeAssistantZeroconf
|
| 28 |
|
| 29 |
_LOGGER = logging.getLogger(__name__)
|
| 30 |
_MODULE_DIR = Path(__file__).parent
|
|
|
|
| 43 |
self._state: Optional[ServerState] = None
|
| 44 |
self._event_loop: Optional[asyncio.AbstractEventLoop] = None
|
| 45 |
self._audio_thread: Optional[threading.Thread] = None
|
| 46 |
+
self._server: Optional[asyncio.Server] = None
|
| 47 |
+
self._discovery: Optional[HomeAssistantZeroconf] = None
|
| 48 |
|
| 49 |
def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
|
| 50 |
"""Run the voice assistant."""
|
|
|
|
| 67 |
self._audio_thread.start()
|
| 68 |
|
| 69 |
# Start ESPHome server
|
| 70 |
+
self._event_loop.run_until_complete(self._run_server(self._state, stop_event))
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
except Exception as e:
|
| 73 |
_LOGGER.error("Error running voice assistant: %s", e)
|
| 74 |
+
import traceback
|
| 75 |
+
traceback.print_exc()
|
| 76 |
finally:
|
| 77 |
_LOGGER.info("Shutting down voice assistant")
|
| 78 |
+
self._cleanup()
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
def _init_state(self, reachy_mini: ReachyMini) -> ServerState:
|
| 81 |
"""Initialize server state."""
|
|
|
|
| 142 |
|
| 143 |
with open(model_config_path, "r", encoding="utf-8") as f:
|
| 144 |
model_config = json.load(f)
|
| 145 |
+
model_type = WakeWordType(model_config.get("type", "microWakeWord"))
|
|
|
|
|
|
|
| 146 |
if model_type == WakeWordType.OPEN_WAKE_WORD:
|
| 147 |
wake_word_path = model_config_path.parent / model_config["model"]
|
| 148 |
else:
|
|
|
|
| 156 |
wake_word_path=wake_word_path,
|
| 157 |
)
|
| 158 |
except Exception as e:
|
| 159 |
+
_LOGGER.error("Error loading wake word config %s: %s", model_config_path, e)
|
|
|
|
|
|
|
| 160 |
|
| 161 |
return available_wake_words
|
| 162 |
|
|
|
|
| 172 |
_LOGGER.error("Failed to load stop model: %s", e)
|
| 173 |
return None
|
| 174 |
|
| 175 |
+
async def _run_server(self, state: ServerState, stop_event: threading.Event) -> None:
|
| 176 |
"""Run ESPHome server."""
|
| 177 |
# Start ESPHome server
|
| 178 |
loop = asyncio.get_running_loop()
|
| 179 |
+
self._server = await loop.create_server(
|
| 180 |
lambda: VoiceSatelliteProtocol(state), host="0.0.0.0", port=6053
|
| 181 |
)
|
| 182 |
|
| 183 |
# Auto discovery (zeroconf, mDNS) - required for Home Assistant auto-discovery
|
| 184 |
+
self._discovery = HomeAssistantZeroconf(port=6053, name="ReachyMini")
|
| 185 |
+
await self._discovery.register_server()
|
| 186 |
|
| 187 |
try:
|
| 188 |
+
async with self._server:
|
| 189 |
_LOGGER.info("ESPHome server started on port 6053")
|
| 190 |
_LOGGER.info("mDNS service registered for auto-discovery")
|
| 191 |
+
|
| 192 |
+
# Run until stop_event is set
|
| 193 |
+
while not stop_event.is_set():
|
| 194 |
+
await asyncio.sleep(0.1)
|
| 195 |
+
|
| 196 |
+
_LOGGER.info("Stop event received, shutting down...")
|
| 197 |
+
|
| 198 |
finally:
|
| 199 |
+
if self._discovery:
|
| 200 |
+
await self._discovery.unregister_server()
|
| 201 |
_LOGGER.info("ESPHome server stopped")
|
| 202 |
|
| 203 |
def _process_audio(self, state: ServerState) -> None:
|
|
|
|
| 247 |
|
| 248 |
while True:
|
| 249 |
try:
|
| 250 |
+
# Read audio chunk
|
| 251 |
data = stream.read(CHUNK, exception_on_overflow=False)
|
| 252 |
+
audio_array = np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0
|
|
|
|
|
|
|
| 253 |
|
| 254 |
# Send to satellite if connected
|
| 255 |
if state.satellite is not None:
|
|
|
|
| 328 |
stream.stop_stream()
|
| 329 |
stream.close()
|
| 330 |
p.terminate()
|
| 331 |
+
_LOGGER.info("Audio processing stopped")
|
| 332 |
+
|
| 333 |
+
def _cleanup(self) -> None:
|
| 334 |
+
"""Clean up resources."""
|
| 335 |
+
if self._audio_thread:
|
| 336 |
+
self._audio_thread.join(timeout=5)
|
| 337 |
+
if self._event_loop and not self._event_loop.is_closed():
|
| 338 |
+
self._event_loop.close()
|