переделан MQTT

This commit is contained in:
2026-05-19 15:27:02 +03:00
parent f06b7322a8
commit df5ce8a683
+177 -334
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,487 +15,339 @@ 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
# 🔹 Защита от эха: если значение не изменилось — игнорируем
if brightness == last_mqtt_brightness: last_mqtt_brightness = ha_value
logger.debug(f"⏭️ Эхо: яркость {brightness} уже обработана") BRIGHTNESS_CONFIG["global_multiplier"] = new_multiplier
return save_brightness_config()
last_mqtt_brightness = brightness logger.info(f"🔆 HA установил яркость: {ha_value}% → multiplier = {new_multiplier:.2f}")
BRIGHTNESS_CONFIG["global_multiplier"] = brightness / 255.0
save_brightness_config() if not ha_disabled:
# Применяем новую яркость к текущему состоянию
logger.info(f"🔆 Яркость: {brightness} (×{BRIGHTNESS_CONFIG['global_multiplier']:.2f})") set_led_state(current_state, skip_publish=False)
# 🔹 Применяем к текущему состоянию, но НЕ публикуем обратно (skip_publish=True) except ValueError:
set_led_state(current_state, skip_publish=True) logger.warning(f"Неверное значение яркости: {payload}")
# ==================== КОМАНДА (ON / OFF) ====================
# 🔹 Публикуем отчёт в отдельный _STATE топик (без эха) elif msg.topic == MQTT_TOPIC_COMMAND:
publish_brightness(brightness) 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])
if cmd_type == "set_brightness": run_xvf_command("LED_BRIGHTNESS", [0])
brightness = max(0, min(255, command.get("value", 100))) publish_state("off", 0)
if brightness != last_mqtt_brightness: else: # ON
last_mqtt_brightness = brightness logger.info("🟢 HA → ON")
BRIGHTNESS_CONFIG["global_multiplier"] = brightness / 255.0 ha_disabled = False
save_brightness_config() set_led_state(current_state)
publish_brightness(brightness) else:
set_led_state(current_state, skip_publish=True) # Если вдруг пришёл JSON (редкий случай)
logger.info(f"🔆 Яркость из команды: {brightness}") try:
cmd = json.loads(payload)
elif cmd_type == "set_state": ha_state = str(cmd.get("state", "")).upper()
state = command.get("state", "idle") bright = cmd.get("brightness")
set_led_state(state, skip_publish=False) # Можно публиковать, это новое состояние
logger.info(f"🎨 Состояние: {state}") if ha_state == "OFF":
ha_disabled = True
elif cmd_type == "set_effect": run_xvf_command("LED_EFFECT", [0])
run_xvf_command("LED_EFFECT", [command.get("effect", 0)]) run_xvf_command("LED_BRIGHTNESS", [0])
run_xvf_command("LED_COLOR", [command.get("color", "0x000000")]) publish_state("off", 0)
run_xvf_command("LED_BRIGHTNESS", [command.get("brightness", 100)]) elif ha_state == "ON":
logger.info(f"🎨 Эффект применён") 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()
set_led_state(current_state)
except:
logger.warning(f"Неизвестная команда: {payload}")
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(): class LedEventHandler(AsyncEventHandler):
global mqtt_client async def handle_event(self, event: Event) -> bool:
if ha_disabled:
return True
et = event.type
if Detection.is_type(et):
set_led_state("wake")
elif VoiceStarted.is_type(et):
set_led_state("listening")
elif VoiceStopped.is_type(et):
set_led_state("thinking")
elif et == "audio-start":
set_led_state("speaking")
elif Played.is_type(et):
set_led_state("idle")
elif et == "pause-satellite":
set_led_state("paused")
elif SatelliteConnected.is_type(et):
set_led_state("idle")
return True
def cleanup_leds():
logger.info("🧹 Выключаем LED")
run_xvf_command("LED_EFFECT", [0])
run_xvf_command("LED_BRIGHTNESS", [0])
async def main(args):
logger.info("🚀 Запуск Wyoming LED Service")
load_brightness_config()
start_mqtt_client()
run_xvf_command("VERSION")
set_led_state("idle")
uri = f"tcp://{args.host}:{args.port}"
server = AsyncServer.from_uri(uri)
await server.run(partial(LedEventHandler))
def signal_handler(sig, frame):
logger.info(f"🛑 Сигнал {sig}")
cleanup_leds()
if mqtt_client: if mqtt_client:
try: try:
mqtt_client.publish(MQTT_TOPIC_AVAILABILITY, "offline", retain=True) mqtt_client.publish(MQTT_TOPIC_AVAILABILITY, "offline", retain=True)
mqtt_client.loop_stop() mqtt_client.loop_stop()
mqtt_client.disconnect() mqtt_client.disconnect()
logger.info("📡 MQTT остановлен") except:
except Exception as e: pass
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):
async def handle_event(self, event: Event) -> bool:
event_type = event.type
logger.debug(f"🔍 Событие: {event_type}")
if Detection.is_type(event_type):
logger.info("🎯 Detection → wake")
set_led_state("wake")
elif VoiceStarted.is_type(event_type):
logger.info("🎤 VoiceStarted → listening")
set_led_state("listening")
elif VoiceStopped.is_type(event_type):
logger.info("💭 VoiceStopped → thinking")
set_led_state("thinking")
elif event_type == "audio-start":
logger.info("🔊 audio-start → speaking")
set_led_state("speaking")
elif Played.is_type(event_type):
logger.info("✅ Played → idle")
set_led_state("idle")
elif event_type in ["transcribe", "transcript", "synthesize", "intent"]:
logger.debug(f"🧠 {event_type} → thinking")
elif event_type == "pause-satellite":
logger.info("⏸️ pause → paused")
set_led_state("paused")
elif SatelliteConnected.is_type(event_type) or event_type == "satellite-connected":
logger.info("🔌 connected → 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
async def on_connected(self) -> None:
logger.info("✅ TCP подключён")
async def on_disconnected(self) -> None:
logger.info("❌ TCP отключён")
set_led_state("idle")
# ============================================================
# 🚀 Main
# ============================================================
async def main(args):
logger.info(f"🚀 Запуск на {args.host}:{args.port}")
load_brightness_config()
start_mqtt_client()
run_xvf_command("VERSION")
set_led_state("idle")
uri = f"tcp://{args.host}:{args.port}"
server = AsyncServer.from_uri(uri)
logger.info(f"📡 Слушаем {uri}")
await server.run(partial(LedEventHandler))
def signal_handler(sig, frame):
logger.info(f"\n🛑 Сигнал {sig}")
cleanup_leds()
stop_mqtt_client()
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()