переделан MQTT

This commit is contained in:
2026-05-19 15:27:02 +03:00
parent f06b7322a8
commit df5ce8a683
+144 -301
View File
@@ -1,13 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Controls the LEDs on the ReSpeaker Mic Array via xvf_host.py Wyoming LED Control - Простая надёжная версия для HA
С поддержкой MQTT Discovery для Home Assistant (без эхо-петли)
""" """
import argparse import argparse
import asyncio import asyncio
import logging import logging
import subprocess import subprocess
import os
import sys import sys
import signal import signal
import json import json
@@ -16,467 +15,314 @@ from pathlib import Path
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
from wyoming.event import Event from wyoming.event import Event
from wyoming.satellite import ( from wyoming.satellite import SatelliteConnected
SatelliteConnected,
SatelliteDisconnected,
StreamingStarted,
StreamingStopped,
)
from wyoming.server import AsyncEventHandler, AsyncServer from wyoming.server import AsyncEventHandler, AsyncServer
from wyoming.snd import Played from wyoming.snd import Played
from wyoming.vad import VoiceStarted, VoiceStopped from wyoming.vad import VoiceStarted, VoiceStopped
from wyoming.wake import Detection from wyoming.wake import Detection
# 🔹 Настройка логирования # ====================== НАСТРОЙКИ ======================
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S" datefmt="%Y-%m-%d %H:%M:%S"
) )
logger = logging.getLogger("XVF3800 LED Service via xvf_host") logger = logging.getLogger("XVF3800 LED Service")
# 🔹 Пути
SCRIPT_DIR = Path(__file__).resolve().parent SCRIPT_DIR = Path(__file__).resolve().parent
XVF_HOST_PATH = str(SCRIPT_DIR / "xvf_host.py") XVF_HOST_PATH = str(SCRIPT_DIR / "xvf_host.py")
CONFIG_FILE = SCRIPT_DIR / "led_brightness_config.json" CONFIG_FILE = SCRIPT_DIR / "led_brightness_config.json"
# 🔹 MQTT конфигурация
MQTT_BROKER = "192.168.200.100" MQTT_BROKER = "192.168.200.100"
MQTT_PORT = 1883 MQTT_PORT = 1883
MQTT_USER = "mqtt" MQTT_USER = "mqtt"
MQTT_PASS = "mqtt" MQTT_PASS = "mqtt"
# 🔹 Разделённые топики (команды ≠ состояния) # Топики
MQTT_TOPIC_COMMAND = "wyoming/led/command" # HA → Скрипт (команды) MQTT_TOPIC_COMMAND = "wyoming/led/command"
MQTT_TOPIC_BRIGHTNESS_SET = "wyoming/led/brightness/set" # HA → Скрипт (установка) MQTT_TOPIC_BRIGHTNESS_SET = "wyoming/led/brightness/set"
MQTT_TOPIC_BRIGHTNESS_STATE = "wyoming/led/brightness/state" # Скрипт → HA (отчёт) MQTT_TOPIC_STATE = "wyoming/led/state" # ON/OFF
MQTT_TOPIC_STATE = "wyoming/led/state" # Скрипт → HA (состояние) MQTT_TOPIC_BRIGHTNESS_STATE = "wyoming/led/brightness/state" # яркость
MQTT_TOPIC_AVAILABILITY = "wyoming/led/availability" # Статус онлайн/офлайн MQTT_TOPIC_SATELLITE_STATE = "wyoming/satellite/state"
MQTT_TOPIC_AVAILABILITY = "wyoming/led/availability"
MQTT_DISCOVERY_PREFIX = "homeassistant"
MQTT_CLIENT_ID = "wyoming-led-service" MQTT_CLIENT_ID = "wyoming-led-service"
# 🔹 MQTT Discovery
MQTT_DISCOVERY_PREFIX = "homeassistant"
MQTT_DISCOVERY_DEVICE = { MQTT_DISCOVERY_DEVICE = {
"identifiers": ["wyoming-led-repeaker"], "identifiers": ["wyoming-led-repeaker"],
"name": "Wyoming LED ReSpeaker", "name": "Wyoming LED ReSpeaker",
"manufacturer": "ReSpeaker", "manufacturer": "ReSpeaker",
"model": "XVF3800 USB 4-MIC Array", "model": "XVF3800",
"sw_version": "1.0",
"connections": [["mac", "unknown"]]
} }
# 🔹 Глобальные настройки
BRIGHTNESS_CONFIG = { BRIGHTNESS_CONFIG = {
"listening": 120, "wake": 200, "thinking": 150, "listening": 120, "wake": 200, "thinking": 150,
"speaking": 140, "idle": 80, "paused": 40, "error": 255, "speaking": 140, "idle": 80, "paused": 40, "error": 255,
"global_multiplier": 1.0 "global_multiplier": 1.0
} }
# 🔹 Глобальные переменные
shutdown_event = asyncio.Event()
mqtt_client = None mqtt_client = None
current_state = "idle" current_state = "idle"
last_mqtt_brightness = None # 🔹 Защита от эха: последнее значение из MQTT last_mqtt_brightness = None
ha_disabled = False
# ============================================================ # ====================== CONFIG ======================
# 💾 Настройки яркости
# ============================================================
def load_brightness_config(): def load_brightness_config():
global BRIGHTNESS_CONFIG global BRIGHTNESS_CONFIG
if CONFIG_FILE.exists(): if CONFIG_FILE.exists():
try: try:
with open(CONFIG_FILE, "r") as f: with open(CONFIG_FILE) as f:
saved = json.load(f) BRIGHTNESS_CONFIG.update(json.load(f))
BRIGHTNESS_CONFIG.update(saved) logger.info("✅ Загружены настройки яркости")
logger.info(f"📂 Загружены настройки из {CONFIG_FILE}")
except Exception as e: except Exception as e:
logger.warning(f"⚠️ Ошибка загрузки конфига: {e}") logger.warning(f"Ошибка загрузки config: {e}")
else: else:
save_brightness_config() save_brightness_config()
def save_brightness_config(): def save_brightness_config():
try: try:
with open(CONFIG_FILE, "w") as f: with open(CONFIG_FILE, "w") as f:
json.dump(BRIGHTNESS_CONFIG, f, indent=2) json.dump(BRIGHTNESS_CONFIG, f, indent=2)
logger.debug(f"💾 Сохранены настройки яркости")
except Exception as e: except Exception as e:
logger.error(f"❌ Ошибка сохранения: {e}") logger.error(f"Ошибка сохранения config: {e}")
# ============================================================ # ====================== LED ======================
# 🎛️ Управление LED
# ============================================================
def run_xvf_command(command: str, values=None): def run_xvf_command(command: str, values=None):
args = [sys.executable, XVF_HOST_PATH, command] args = [sys.executable, XVF_HOST_PATH, command]
if values: if values:
args.extend(["--values"] + [str(v) for v in values]) args.extend(["--values"] + [str(v) for v in values])
try: try:
result = subprocess.run(args, capture_output=True, text=True, check=True, timeout=5) subprocess.run(args, capture_output=True, text=True, check=True, timeout=5)
if result.stdout.strip():
logger.debug(f"{command} output: {result.stdout.strip()}")
return True return True
except Exception as e: except Exception as e:
logger.error(f"❌ Ошибка {command}: {e}") logger.error(f"LED error {command}: {e}")
return False return False
def get_effective_brightness(state: str) -> int: def get_effective_brightness(state: str) -> int:
base = BRIGHTNESS_CONFIG.get(state, 100) base = BRIGHTNESS_CONFIG.get(state, 100)
multiplier = BRIGHTNESS_CONFIG.get("global_multiplier", 1.0) return int(base * BRIGHTNESS_CONFIG.get("global_multiplier", 1.0))
return int(base * multiplier)
def set_led_state(state: str, skip_publish=False): def set_led_state(state: str, skip_publish=False):
"""Устанавливает состояние LED. Если skip_publish=True — не публикуем в MQTT (избегаем эха)"""
global current_state global current_state
current_state = state current_state = state
brightness = get_effective_brightness(state) brightness = get_effective_brightness(state)
if state == "listening": if state == "listening":
run_xvf_command("LED_EFFECT", [0])
run_xvf_command("LED_EFFECT", [1]) run_xvf_command("LED_EFFECT", [1])
run_xvf_command("LED_COLOR", ["0x00BCD4"]) run_xvf_command("LED_COLOR", ["0x00BCD4"])
run_xvf_command("LED_BRIGHTNESS", [brightness]) run_xvf_command("LED_BRIGHTNESS", [brightness])
run_xvf_command("LED_SPEED", [2]) run_xvf_command("LED_SPEED", [2])
logger.info(f"🔵 Listening: бирюзовое дыхание ({brightness})")
elif state == "wake": elif state == "wake":
run_xvf_command("LED_EFFECT", [1]) run_xvf_command("LED_EFFECT", [1])
run_xvf_command("LED_COLOR", ["0xFF0000"]) run_xvf_command("LED_COLOR", ["0xFF0000"])
run_xvf_command("LED_BRIGHTNESS", [brightness]) run_xvf_command("LED_BRIGHTNESS", [brightness])
logger.info(f"🔴 Wake: красный ({brightness})")
elif state == "thinking": elif state == "thinking":
run_xvf_command("LED_EFFECT", [2]) run_xvf_command("LED_EFFECT", [2])
run_xvf_command("LED_BRIGHTNESS", [brightness]) run_xvf_command("LED_BRIGHTNESS", [brightness])
run_xvf_command("LED_SPEED", [4]) run_xvf_command("LED_SPEED", [4])
logger.info(f"🌈 Thinking: радуга ({brightness})")
elif state == "speaking": elif state == "speaking":
run_xvf_command("LED_EFFECT", [1]) run_xvf_command("LED_EFFECT", [1])
run_xvf_command("LED_COLOR", ["0x4CAF50"]) run_xvf_command("LED_COLOR", ["0x4CAF50"])
run_xvf_command("LED_BRIGHTNESS", [brightness]) run_xvf_command("LED_BRIGHTNESS", [brightness])
run_xvf_command("LED_SPEED", [2])
logger.info(f"🟢 Speaking: зелёный ({brightness})")
elif state == "idle": elif state == "idle":
run_xvf_command("LED_EFFECT", [4]) run_xvf_command("LED_EFFECT", [4])
run_xvf_command("LED_DOA_COLOR", ["0x303030", "0x00BCD4"]) run_xvf_command("LED_DOA_COLOR", ["0x303030", "0x00BCD4"])
run_xvf_command("LED_BRIGHTNESS", [brightness]) run_xvf_command("LED_BRIGHTNESS", [brightness])
logger.info(f"⚪ Idle: DoA ({brightness})")
elif state == "paused": elif state == "paused":
run_xvf_command("LED_EFFECT", [1]) run_xvf_command("LED_EFFECT", [1])
run_xvf_command("LED_COLOR", ["0x404040"]) run_xvf_command("LED_COLOR", ["0x404040"])
run_xvf_command("LED_BRIGHTNESS", [brightness]) run_xvf_command("LED_BRIGHTNESS", [brightness])
logger.info(f"⚫ Paused: серый ({brightness})")
elif state == "error": elif state == "error":
run_xvf_command("LED_EFFECT", [1]) run_xvf_command("LED_EFFECT", [1])
run_xvf_command("LED_COLOR", ["0xFF00FF"]) run_xvf_command("LED_COLOR", ["0xFF00FF"])
run_xvf_command("LED_BRIGHTNESS", [brightness]) run_xvf_command("LED_BRIGHTNESS", [brightness])
logger.info(f"🟣 Error: маджента ({brightness})")
elif state == "off": elif state == "off":
run_xvf_command("LED_EFFECT", [0]) run_xvf_command("LED_EFFECT", [0])
run_xvf_command("LED_BRIGHTNESS", [0]) run_xvf_command("LED_BRIGHTNESS", [0])
logger.info("⬛ Off: выключено")
else:
run_xvf_command("LED_EFFECT", [0])
logger.info("⬛ Unknown")
# 🔹 Публикуем состояние только если не скипаем
if not skip_publish: if not skip_publish:
publish_state(state, brightness) publish_state(state, brightness)
# ============================================================ # ====================== MQTT ======================
# 🔍 MQTT Discovery
# ============================================================
def publish_discovery_config(): def publish_discovery_config():
if not mqtt_client: if not mqtt_client:
return return
try: try:
# 🔹 Number (слайдер) — читаем из STATE, пишем в SET discovery = {
discovery_number = {
"name": "Wyoming LED Brightness",
"unique_id": "wyoming_led_brightness",
"state_topic": MQTT_TOPIC_BRIGHTNESS_STATE, # ← Читаем отчёт
"command_topic": MQTT_TOPIC_BRIGHTNESS_SET, # ← Пишем команду
"min": 0, "max": 255,
"icon": "mdi:brightness-6",
"availability_topic": MQTT_TOPIC_AVAILABILITY,
"payload_available": "online",
"payload_not_available": "offline",
"device": MQTT_DISCOVERY_DEVICE,
"object_id": "wyoming_led_brightness"
}
# 🔹 Sensor (состояние)
discovery_sensor = {
"name": "Wyoming LED State",
"unique_id": "wyoming_led_state",
"state_topic": MQTT_TOPIC_STATE,
"value_template": "{{ value_json.state }}",
"icon": "mdi:led-strip",
"availability_topic": MQTT_TOPIC_AVAILABILITY,
"payload_available": "online",
"payload_not_available": "offline",
"device": MQTT_DISCOVERY_DEVICE,
"json_attributes_topic": MQTT_TOPIC_STATE,
"object_id": "wyoming_led_state"
}
# 🔹 Light (полноценная лампа)
discovery_light = {
"name": "Wyoming LED", "name": "Wyoming LED",
"unique_id": "wyoming_led_light", "unique_id": "wyoming_led_light",
"state_topic": MQTT_TOPIC_STATE, "state_topic": MQTT_TOPIC_STATE,
"command_topic": MQTT_TOPIC_COMMAND, "command_topic": MQTT_TOPIC_COMMAND,
"brightness_state_topic": MQTT_TOPIC_BRIGHTNESS_STATE,
"brightness_command_topic": MQTT_TOPIC_BRIGHTNESS_SET, "brightness_command_topic": MQTT_TOPIC_BRIGHTNESS_SET,
"brightness_scale": 255, "brightness_state_topic": MQTT_TOPIC_BRIGHTNESS_STATE,
"availability_topic": MQTT_TOPIC_AVAILABILITY, "availability_topic": MQTT_TOPIC_AVAILABILITY,
"payload_available": "online", "payload_available": "online",
"payload_not_available": "offline", "payload_not_available": "offline",
"payload_on": '{"type": "set_state", "state": "speaking"}', "payload_on": "ON",
"payload_off": '{"type": "set_state", "state": "idle"}', "payload_off": "OFF",
"brightness_scale": 100, # ← ИЗМЕНИЛИ НА 100
"optimistic": False,
"qos": 1,
"device": MQTT_DISCOVERY_DEVICE, "device": MQTT_DISCOVERY_DEVICE,
"object_id": "wyoming_led",
"schema": "json"
} }
# Публикуем с retain=True для автообнаружения mqtt_client.publish(
mqtt_client.publish(f"{MQTT_DISCOVERY_PREFIX}/number/wyoming_led/config", f"{MQTT_DISCOVERY_PREFIX}/light/wyoming_led/config",
json.dumps(discovery_number), retain=True) json.dumps(discovery),
mqtt_client.publish(f"{MQTT_DISCOVERY_PREFIX}/sensor/wyoming_led/config", retain=True,
json.dumps(discovery_sensor), retain=True) qos=1
mqtt_client.publish(f"{MQTT_DISCOVERY_PREFIX}/light/wyoming_led/config", )
json.dumps(discovery_light), retain=True) logger.info("✅ Discovery опубликован (scale = 100)")
logger.info("🔍 MQTT Discovery: конфигурации опубликованы")
except Exception as e: except Exception as e:
logger.error(f"❌ Ошибка Discovery: {e}") logger.error(f"Discovery error: {e}")
def publish_state(state: str, brightness: int = None):
"""Публикуем в HA именно global_multiplier, а не effective brightness"""
if not mqtt_client:
return
ha_state = "OFF" if (ha_disabled or state == "off") else "ON"
# Главное: публикуем текущий global_multiplier в процентах (0-100)
multiplier = BRIGHTNESS_CONFIG.get("global_multiplier", 1.0)
ha_brightness = int(multiplier * 100) # 0..100
mqtt_client.publish(MQTT_TOPIC_STATE, ha_state, retain=True, qos=1)
mqtt_client.publish(MQTT_TOPIC_BRIGHTNESS_STATE, str(ha_brightness), retain=True, qos=1)
logger.info(f"📤 HA ← {ha_state} | Яркость: {ha_brightness}% (multiplier: {multiplier:.2f})")
# ============================================================
# 📡 MQTT функции
# ============================================================
def on_mqtt_connect(client, userdata, flags, rc, properties=None): def on_mqtt_connect(client, userdata, flags, rc, properties=None):
if rc == 0: if rc == 0:
logger.info(f"✅ MQTT подключён к {MQTT_BROKER}:{MQTT_PORT}") logger.info("✅ MQTT подключён")
client.publish(MQTT_TOPIC_AVAILABILITY, "online", retain=True) client.publish(MQTT_TOPIC_AVAILABILITY, "online", retain=True, qos=1)
# 🔹 Подписываемся ТОЛЬКО на командные топики (не на state!)
client.subscribe(MQTT_TOPIC_COMMAND) client.subscribe(MQTT_TOPIC_COMMAND)
client.subscribe(MQTT_TOPIC_BRIGHTNESS_SET) client.subscribe(MQTT_TOPIC_BRIGHTNESS_SET)
publish_discovery_config() publish_discovery_config()
logger.info(f"📡 Подписки: {MQTT_TOPIC_COMMAND}, {MQTT_TOPIC_BRIGHTNESS_SET}")
# Отправляем текущее состояние
brightness = get_effective_brightness(current_state)
publish_state(current_state, brightness)
else: else:
logger.error(f"❌ MQTT ошибка подключения: {rc}") logger.error(f"❌ MQTT ошибка: {rc}")
def on_mqtt_disconnect(client, userdata, rc, properties=None):
logger.warning(f"⚠️ MQTT отключён (код: {rc})")
def on_mqtt_message(client, userdata, msg): def on_mqtt_message(client, userdata, msg):
"""Обработчик входящих MQTT сообщений""" global ha_disabled, last_mqtt_brightness, current_state
global last_mqtt_brightness
try: try:
topic = msg.topic payload = msg.payload.decode('utf-8').strip()
payload = msg.payload.decode('utf-8') logger.info(f"📩 {msg.topic}: {payload}")
logger.debug(f"📩 MQTT {topic}: {payload}")
# 🔹 Обработка яркости (только из _SET топика!) # ==================== ЯРКОСТЬ ====================
if topic == MQTT_TOPIC_BRIGHTNESS_SET: if msg.topic == MQTT_TOPIC_BRIGHTNESS_SET:
brightness = int(payload) try:
brightness = max(0, min(255, brightness)) ha_value = max(0, min(100, int(payload))) # 0-100
new_multiplier = ha_value / 100.0
# 🔹 Защита от эха: если значение не изменилось — игнорируем last_mqtt_brightness = ha_value
if brightness == last_mqtt_brightness: BRIGHTNESS_CONFIG["global_multiplier"] = new_multiplier
logger.debug(f"⏭️ Эхо: яркость {brightness} уже обработана")
return
last_mqtt_brightness = brightness
BRIGHTNESS_CONFIG["global_multiplier"] = brightness / 255.0
save_brightness_config() save_brightness_config()
logger.info(f"🔆 Яркость: {brightness} (×{BRIGHTNESS_CONFIG['global_multiplier']:.2f})") logger.info(f"🔆 HA установил яркость: {ha_value}% → multiplier = {new_multiplier:.2f}")
# 🔹 Применяем к текущему состоянию, но НЕ публикуем обратно (skip_publish=True) if not ha_disabled:
set_led_state(current_state, skip_publish=True) # Применяем новую яркость к текущему состоянию
set_led_state(current_state, skip_publish=False)
# 🔹 Публикуем отчёт в отдельный _STATE топик (без эха) except ValueError:
publish_brightness(brightness) logger.warning(f"Неверное значение яркости: {payload}")
# ==================== КОМАНДА (ON / OFF) ====================
elif msg.topic == MQTT_TOPIC_COMMAND:
ha_state = payload.upper()
# 🔹 Обработка команд if ha_state in ["ON", "OFF"]:
elif topic == MQTT_TOPIC_COMMAND: if ha_state == "OFF":
command = json.loads(payload) logger.info("🔴 HA → OFF")
cmd_type = command.get("type", "") ha_disabled = True
run_xvf_command("LED_EFFECT", [0])
run_xvf_command("LED_BRIGHTNESS", [0])
publish_state("off", 0)
else: # ON
logger.info("🟢 HA → ON")
ha_disabled = False
set_led_state(current_state)
else:
# Если вдруг пришёл JSON (редкий случай)
try:
cmd = json.loads(payload)
ha_state = str(cmd.get("state", "")).upper()
bright = cmd.get("brightness")
if cmd_type == "set_brightness": if ha_state == "OFF":
brightness = max(0, min(255, command.get("value", 100))) ha_disabled = True
if brightness != last_mqtt_brightness: run_xvf_command("LED_EFFECT", [0])
last_mqtt_brightness = brightness run_xvf_command("LED_BRIGHTNESS", [0])
BRIGHTNESS_CONFIG["global_multiplier"] = brightness / 255.0 publish_state("off", 0)
elif ha_state == "ON":
ha_disabled = False
if bright is not None:
bright = max(0, min(255, int(bright)))
last_mqtt_brightness = bright
BRIGHTNESS_CONFIG["global_multiplier"] = bright / 255.0
save_brightness_config() save_brightness_config()
publish_brightness(brightness) set_led_state(current_state)
set_led_state(current_state, skip_publish=True) except:
logger.info(f"🔆 Яркость из команды: {brightness}") logger.warning(f"Неизвестная команда: {payload}")
elif cmd_type == "set_state":
state = command.get("state", "idle")
set_led_state(state, skip_publish=False) # Можно публиковать, это новое состояние
logger.info(f"🎨 Состояние: {state}")
elif cmd_type == "set_effect":
run_xvf_command("LED_EFFECT", [command.get("effect", 0)])
run_xvf_command("LED_COLOR", [command.get("color", "0x000000")])
run_xvf_command("LED_BRIGHTNESS", [command.get("brightness", 100)])
logger.info(f"🎨 Эффект применён")
except json.JSONDecodeError:
logger.warning(f"⚠️ Неверный JSON: {payload}")
except Exception as e: except Exception as e:
logger.error(f"❌ Ошибка MQTT: {e}") logger.error(f"❌ Ошибка обработки MQTT: {e}", exc_info=True)
def publish_state(state: str, brightness: int):
"""Публикует состояние в топик состояния"""
if mqtt_client:
try:
payload = json.dumps({
"state": state,
"brightness": brightness,
"timestamp": asyncio.get_event_loop().time()
})
mqtt_client.publish(MQTT_TOPIC_STATE, payload, retain=False) # ← Не retain!
except Exception as e:
logger.debug(f"⚠️ Ошибка публикации state: {e}")
def publish_brightness(brightness: int):
"""Публикует яркость в топик состояния (отчёт, не команда!)"""
if mqtt_client:
try:
# 🔹 Публикуем в _STATE, а не в _SET! И без retain, чтобы не триггерить эхо
mqtt_client.publish(MQTT_TOPIC_BRIGHTNESS_STATE, str(brightness), retain=False)
except Exception as e:
logger.debug(f"⚠️ Ошибка публикации brightness: {e}")
# ====================== START ======================
def start_mqtt_client(): def start_mqtt_client():
global mqtt_client global mqtt_client
mqtt_client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2, client_id=MQTT_CLIENT_ID)
mqtt_client = mqtt.Client(
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
client_id=MQTT_CLIENT_ID,
protocol=mqtt.MQTTv311
)
mqtt_client.username_pw_set(MQTT_USER, MQTT_PASS) mqtt_client.username_pw_set(MQTT_USER, MQTT_PASS)
mqtt_client.on_connect = on_mqtt_connect mqtt_client.on_connect = on_mqtt_connect
mqtt_client.on_disconnect = on_mqtt_disconnect
mqtt_client.on_message = on_mqtt_message mqtt_client.on_message = on_mqtt_message
mqtt_client.will_set(MQTT_TOPIC_AVAILABILITY, "offline", retain=True, qos=1) mqtt_client.will_set(MQTT_TOPIC_AVAILABILITY, "offline", retain=True, qos=1)
try:
mqtt_client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60) mqtt_client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60)
mqtt_client.loop_start() mqtt_client.loop_start()
logger.info("📡 MQTT клиент запущен")
except Exception as e:
logger.error(f"❌ Ошибка подключения MQTT: {e}")
return mqtt_client
def stop_mqtt_client():
global mqtt_client
if mqtt_client:
try:
mqtt_client.publish(MQTT_TOPIC_AVAILABILITY, "offline", retain=True)
mqtt_client.loop_stop()
mqtt_client.disconnect()
logger.info("📡 MQTT остановлен")
except Exception as e:
logger.error(f"❌ Остановка MQTT: {e}")
# ============================================================
# 🧹 Очистка
# ============================================================
def cleanup_leds():
logger.info("🧹 Очистка: выключаем LED...")
try:
run_xvf_command("LED_EFFECT", [0])
run_xvf_command("LED_BRIGHTNESS", [0])
logger.info("✅ LED выключены")
except Exception as e:
logger.error(f"❌ Ошибка очистки: {e}")
# ============================================================
# 🎯 Wyoming Handler
# ============================================================
class LedEventHandler(AsyncEventHandler): class LedEventHandler(AsyncEventHandler):
async def handle_event(self, event: Event) -> bool: async def handle_event(self, event: Event) -> bool:
event_type = event.type if ha_disabled:
logger.debug(f"🔍 Событие: {event_type}") return True
et = event.type
if Detection.is_type(event_type): if Detection.is_type(et):
logger.info("🎯 Detection → wake")
set_led_state("wake") set_led_state("wake")
elif VoiceStarted.is_type(event_type): elif VoiceStarted.is_type(et):
logger.info("🎤 VoiceStarted → listening")
set_led_state("listening") set_led_state("listening")
elif VoiceStopped.is_type(event_type): elif VoiceStopped.is_type(et):
logger.info("💭 VoiceStopped → thinking")
set_led_state("thinking") set_led_state("thinking")
elif event_type == "audio-start": elif et == "audio-start":
logger.info("🔊 audio-start → speaking")
set_led_state("speaking") set_led_state("speaking")
elif Played.is_type(event_type): elif Played.is_type(et):
logger.info("✅ Played → idle")
set_led_state("idle") set_led_state("idle")
elif event_type in ["transcribe", "transcript", "synthesize", "intent"]: elif et == "pause-satellite":
logger.debug(f"🧠 {event_type} → thinking")
elif event_type == "pause-satellite":
logger.info("⏸️ pause → paused")
set_led_state("paused") set_led_state("paused")
elif SatelliteConnected.is_type(event_type) or event_type == "satellite-connected": elif SatelliteConnected.is_type(et):
logger.info("🔌 connected → idle")
set_led_state("idle") set_led_state("idle")
elif event_type == "error":
logger.warning("⚠️ error")
set_led_state("error")
elif event_type in ["run-pipeline", "run-satellite", "streaming-started",
"streaming-stopped", "audio-stop", "satellite-disconnected"]:
logger.debug(f"⏭️ {event_type} — игнор")
else:
logger.debug(f"❓ Неизвестно: {event_type}")
return True return True
async def on_connected(self) -> None:
logger.info("✅ TCP подключён") def cleanup_leds():
async def on_disconnected(self) -> None: logger.info("🧹 Выключаем LED")
logger.info("❌ TCP отключён") run_xvf_command("LED_EFFECT", [0])
set_led_state("idle") run_xvf_command("LED_BRIGHTNESS", [0])
# ============================================================
# 🚀 Main
# ============================================================
async def main(args): async def main(args):
logger.info(f"🚀 Запуск на {args.host}:{args.port}") logger.info("🚀 Запуск Wyoming LED Service")
load_brightness_config() load_brightness_config()
start_mqtt_client() start_mqtt_client()
run_xvf_command("VERSION") run_xvf_command("VERSION")
@@ -484,19 +330,24 @@ async def main(args):
uri = f"tcp://{args.host}:{args.port}" uri = f"tcp://{args.host}:{args.port}"
server = AsyncServer.from_uri(uri) server = AsyncServer.from_uri(uri)
logger.info(f"📡 Слушаем {uri}")
await server.run(partial(LedEventHandler)) await server.run(partial(LedEventHandler))
def signal_handler(sig, frame): def signal_handler(sig, frame):
logger.info(f"\n🛑 Сигнал {sig}") logger.info(f"🛑 Сигнал {sig}")
cleanup_leds() cleanup_leds()
stop_mqtt_client() if mqtt_client:
try:
mqtt_client.publish(MQTT_TOPIC_AVAILABILITY, "offline", retain=True)
mqtt_client.loop_stop()
mqtt_client.disconnect()
except:
pass
sys.exit(0) sys.exit(0)
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="LED service with MQTT Discovery (no echo)") parser = argparse.ArgumentParser()
parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, default=10701) parser.add_argument("--port", type=int, default=10701)
args = parser.parse_args() args = parser.parse_args()
@@ -505,16 +356,8 @@ if __name__ == "__main__":
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
try: try:
logger.info("💡 Ctrl+C для остановки")
asyncio.run(main(args)) asyncio.run(main(args))
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("⌨️ KeyboardInterrupt") pass
cleanup_leds()
stop_mqtt_client()
except Exception as e:
logger.error(f"❌ Ошибка: {e}")
cleanup_leds()
stop_mqtt_client()
sys.exit(1)
finally: finally:
logger.info("👋 Завершено") cleanup_leds()