docs: добавлен протокол WebSocket команд (set_volume, mute, play_sound, etc.)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -225,10 +225,22 @@ async def start_websocket_server(self) -> None:
|
|||||||
self._websocket_clients.add(websocket)
|
self._websocket_clients.add(websocket)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Ждать отключения клиента
|
# Ждать команды от клиента
|
||||||
async for message in websocket:
|
async for message in websocket:
|
||||||
# Игнорировать входящие сообщения (только рассылка)
|
try:
|
||||||
_LOGGER.debug(f"Received from WebSocket: {message}")
|
data = json.loads(message)
|
||||||
|
command = data.get('command')
|
||||||
|
command_data = data.get('data', {})
|
||||||
|
|
||||||
|
if command:
|
||||||
|
_LOGGER.debug(f"Received command: {command}")
|
||||||
|
await self.handle_websocket_command(command, command_data)
|
||||||
|
else:
|
||||||
|
_LOGGER.debug(f"Received message (no command): {data}")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
_LOGGER.warning(f"Invalid JSON from WebSocket: {message}")
|
||||||
|
except Exception as exc:
|
||||||
|
_LOGGER.error(f"Error processing command: {exc}")
|
||||||
except websockets.exceptions.ConnectionClosed:
|
except websockets.exceptions.ConnectionClosed:
|
||||||
pass # Нормальное отключение
|
pass # Нормальное отключение
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -270,7 +282,83 @@ async def stop_websocket_server(self) -> None:
|
|||||||
_LOGGER.info("WebSocket server stopped")
|
_LOGGER.info("WebSocket server stopped")
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2.4 Интеграция в жизненный цикл
|
#### 2.4 Обработчик команд
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def handle_websocket_command(self, command: str, data: dict) -> None:
|
||||||
|
"""
|
||||||
|
Обработать команду от WebSocket клиента.
|
||||||
|
|
||||||
|
Поддерживаемые команды:
|
||||||
|
- set_volume: Изменить громкость
|
||||||
|
- mute: Заглушить микрофон
|
||||||
|
- unmute: Включить микрофон
|
||||||
|
- play_sound: Воспроизвести WAV файл
|
||||||
|
- set_led_brightness: Яркость LED
|
||||||
|
- set_led_color: Цвет LED
|
||||||
|
- trigger_event: Отправить событие в Home Assistant
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
if command == "set_volume":
|
||||||
|
# Изменить громкость (0.0 - 1.0)
|
||||||
|
level = float(data.get("level", 1.0))
|
||||||
|
level = max(0.0, min(1.0, level)) # Clamp 0-1
|
||||||
|
|
||||||
|
# Сохранить в settings (потребуется добавить mutable settings)
|
||||||
|
_LOGGER.info(f"Setting volume to {level}")
|
||||||
|
# Примечание: settings frozen, нужно будет изменить архитектуру
|
||||||
|
|
||||||
|
elif command == "mute":
|
||||||
|
_LOGGER.info("Muting microphone")
|
||||||
|
self.microphone_muted = True
|
||||||
|
|
||||||
|
elif command == "unmute":
|
||||||
|
_LOGGER.info("Unmuting microphone")
|
||||||
|
self.microphone_muted = False
|
||||||
|
|
||||||
|
elif command == "play_sound":
|
||||||
|
# Воспроизвести WAV файл
|
||||||
|
wav_path = data.get("path")
|
||||||
|
if wav_path:
|
||||||
|
_LOGGER.info(f"Playing sound: {wav_path}")
|
||||||
|
await self._play_wav(wav_path, mute_microphone=False)
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("play_sound command missing 'path'")
|
||||||
|
|
||||||
|
elif command == "set_led_brightness":
|
||||||
|
# Отправить команду в LED сервис (через event.uri)
|
||||||
|
brightness = int(data.get("brightness", 128))
|
||||||
|
_LOGGER.info(f"Setting LED brightness to {brightness}")
|
||||||
|
# Отправить событие в event service
|
||||||
|
|
||||||
|
elif command == "set_led_color":
|
||||||
|
# Установить цвет LED
|
||||||
|
r = int(data.get("r", 0))
|
||||||
|
g = int(data.get("g", 0))
|
||||||
|
b = int(data.get("b", 0))
|
||||||
|
_LOGGER.info(f"Setting LED color to RGB({r}, {g}, {b})")
|
||||||
|
# Отправить событие в event service
|
||||||
|
|
||||||
|
elif command == "trigger_event":
|
||||||
|
# Отправить событие в Home Assistant
|
||||||
|
event_type = data.get("event_type")
|
||||||
|
event_data = data.get("data", {})
|
||||||
|
if event_type:
|
||||||
|
_LOGGER.info(f"Triggering event: {event_type}")
|
||||||
|
# Отправить RunPipeline или другое событие
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("trigger_event missing 'event_type'")
|
||||||
|
|
||||||
|
else:
|
||||||
|
_LOGGER.warning(f"Unknown command: {command}")
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
_LOGGER.error(f"Error handling command {command}: {exc}")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.5 Интеграция в жизненный цикл
|
||||||
|
|
||||||
**В `_start()`:**
|
**В `_start()`:**
|
||||||
|
|
||||||
@@ -305,7 +393,7 @@ async def _stop(self) -> None:
|
|||||||
self.state = State.STOPPED
|
self.state = State.STOPPED
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2.5 Обновить `forward_event()`
|
#### 2.6 Обновить `forward_event()`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def forward_event(self, event: Event) -> None:
|
async def forward_event(self, event: Event) -> None:
|
||||||
@@ -328,6 +416,81 @@ async def forward_event(self, event: Event) -> None:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Этап WS-2A: Протокол WebSocket команд (30 мин) ⏳ ОЖИДАЕТСЯ
|
||||||
|
|
||||||
|
**Формат сообщений:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"command": "имя_команды",
|
||||||
|
"data": {
|
||||||
|
"ключ": "значение"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Поддерживаемые команды:**
|
||||||
|
|
||||||
|
| Команда | Данные | Описание |
|
||||||
|
|---------|--------|----------|
|
||||||
|
| `set_volume` | `{"level": 0.0-1.0}` | Изменить громкость |
|
||||||
|
| `mute` | `{}` | Заглушить микрофон |
|
||||||
|
| `unmute` | `{}` | Включить микрофон |
|
||||||
|
| `play_sound` | `{"path": "/path/to.wav"}` | Воспроизвести WAV |
|
||||||
|
| `set_led_brightness` | `{"brightness": 0-255}` | Яркость LED |
|
||||||
|
| `set_led_color` | `{"r": 255, "g": 0, "b": 0}` | Цвет LED (RGB) |
|
||||||
|
| `trigger_event` | `{"event_type": "...", "data": {...}}` | Событие в HA |
|
||||||
|
|
||||||
|
**Примеры:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Установить громкость 70%
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
"command": "set_volume",
|
||||||
|
"data": {"level": 0.7}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Заглушить микрофон
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
"command": "mute",
|
||||||
|
"data": {}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Воспроизвести звук
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
"command": "play_sound",
|
||||||
|
"data": {"path": "/sounds/awake.wav"}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Установить цвет LED (красный)
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
"command": "set_led_color",
|
||||||
|
"data": {"r": 255, "g": 0, "b": 0}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Отправить событие в Home Assistant
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
"command": "trigger_event",
|
||||||
|
"data": {
|
||||||
|
"event_type": "wyoming_satellite.custom",
|
||||||
|
"data": {"custom_data": "value"}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответы сервера:**
|
||||||
|
|
||||||
|
Сервер отправляет события в формате:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "voice-started",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Этап WS-3: Модификация `wyoming_satellite/__main__.py` (20 мин) ⏳ ОЖИДАЕТСЯ
|
### Этап WS-3: Модификация `wyoming_satellite/__main__.py` (20 мин) ⏳ ОЖИДАЕТСЯ
|
||||||
|
|
||||||
#### 3.1 Добавить CLI аргументы
|
#### 3.1 Добавить CLI аргументы
|
||||||
@@ -497,11 +660,11 @@ python3 -m wyoming_satellite \
|
|||||||
| Файл | Тип | Изменения | Строк |
|
| Файл | Тип | Изменения | Строк |
|
||||||
|------|-----|-----------|-------|
|
|------|-----|-----------|-------|
|
||||||
| `wyoming_satellite/settings.py` | Изменён | EventSettings + WebSocket | ~10 |
|
| `wyoming_satellite/settings.py` | Изменён | EventSettings + WebSocket | ~10 |
|
||||||
| `wyoming_satellite/satellite.py` | Изменён | WebSocket server + broadcast | ~150 |
|
| `wyoming_satellite/satellite.py` | Изменён | WebSocket server + broadcast + commands | ~200 |
|
||||||
| `wyoming_satellite/__main__.py` | Изменён | CLI аргументы | ~20 |
|
| `wyoming_satellite/__main__.py` | Изменён | CLI аргументы | ~20 |
|
||||||
| `examples/websocket-service/*` | Существующий | Готовые клиенты | — |
|
| `examples/websocket-service/*` | Существующий | Готовые клиенты | — |
|
||||||
| `docs/integration/mqtt-led.md` | Изменён | Документация WebSocket | ~50 |
|
| `docs/integration/mqtt-led.md` | Изменён | Документация WebSocket + commands | ~80 |
|
||||||
| **ВСЕГО** | | | **~230 строк** |
|
| **ВСЕГО** | | | **~310 строк** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -564,6 +727,16 @@ sudo systemctl restart wyoming-satelite-vosk.service
|
|||||||
- [ ] Интеграция в `_stop()` выполнена
|
- [ ] Интеграция в `_stop()` выполнена
|
||||||
- [ ] `python3 -m py_compile wyoming_satellite/satellite.py` проходит
|
- [ ] `python3 -m py_compile wyoming_satellite/satellite.py` проходит
|
||||||
|
|
||||||
|
### ⏳ Этап WS-2A: Протокол команд (ожидается)
|
||||||
|
|
||||||
|
- [ ] `handle_websocket_command()` реализован
|
||||||
|
- [ ] Команда `set_volume` работает
|
||||||
|
- [ ] Команда `mute`/`unmute` работает
|
||||||
|
- [ ] Команда `play_sound` работает
|
||||||
|
- [ ] Команда `set_led_*` работает
|
||||||
|
- [ ] Команда `trigger_event` работает
|
||||||
|
- [ ] Обработка ошибок реализована
|
||||||
|
|
||||||
### ⏳ Этап WS-3: __main__.py (ожидается)
|
### ⏳ Этап WS-3: __main__.py (ожидается)
|
||||||
|
|
||||||
- [ ] `--websocket-enabled` аргумент добавлен
|
- [ ] `--websocket-enabled` аргумент добавлен
|
||||||
|
|||||||
Reference in New Issue
Block a user