From df5ce8a68385a85356d337eadf3644fa29a9e5a9 Mon Sep 17 00:00:00 2001 From: sirmrak Date: Tue, 19 May 2026 15:27:02 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B0=D0=BD=20MQTT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/wyoming-led.py | 511 ++++++++++++++-------------------------- 1 file changed, 177 insertions(+), 334 deletions(-) diff --git a/examples/wyoming-led.py b/examples/wyoming-led.py index 4393c1c..473882b 100644 --- a/examples/wyoming-led.py +++ b/examples/wyoming-led.py @@ -1,13 +1,12 @@ #!/usr/bin/env python3 """ -Controls the LEDs on the ReSpeaker Mic Array via xvf_host.py -С поддержкой MQTT Discovery для Home Assistant (без эхо-петли) +Wyoming LED Control - Простая надёжная версия для HA """ + import argparse import asyncio import logging import subprocess -import os import sys import signal import json @@ -16,487 +15,339 @@ from pathlib import Path import paho.mqtt.client as mqtt from wyoming.event import Event -from wyoming.satellite import ( - SatelliteConnected, - SatelliteDisconnected, - StreamingStarted, - StreamingStopped, -) +from wyoming.satellite import SatelliteConnected from wyoming.server import AsyncEventHandler, AsyncServer from wyoming.snd import Played from wyoming.vad import VoiceStarted, VoiceStopped from wyoming.wake import Detection -# 🔹 Настройка логирования +# ====================== НАСТРОЙКИ ====================== logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)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 XVF_HOST_PATH = str(SCRIPT_DIR / "xvf_host.py") CONFIG_FILE = SCRIPT_DIR / "led_brightness_config.json" -# 🔹 MQTT конфигурация MQTT_BROKER = "192.168.200.100" MQTT_PORT = 1883 MQTT_USER = "mqtt" MQTT_PASS = "mqtt" -# 🔹 Разделённые топики (команды ≠ состояния) -MQTT_TOPIC_COMMAND = "wyoming/led/command" # HA → Скрипт (команды) -MQTT_TOPIC_BRIGHTNESS_SET = "wyoming/led/brightness/set" # HA → Скрипт (установка) -MQTT_TOPIC_BRIGHTNESS_STATE = "wyoming/led/brightness/state" # Скрипт → HA (отчёт) -MQTT_TOPIC_STATE = "wyoming/led/state" # Скрипт → HA (состояние) -MQTT_TOPIC_AVAILABILITY = "wyoming/led/availability" # Статус онлайн/офлайн +# Топики +MQTT_TOPIC_COMMAND = "wyoming/led/command" +MQTT_TOPIC_BRIGHTNESS_SET = "wyoming/led/brightness/set" +MQTT_TOPIC_STATE = "wyoming/led/state" # ON/OFF +MQTT_TOPIC_BRIGHTNESS_STATE = "wyoming/led/brightness/state" # яркость +MQTT_TOPIC_SATELLITE_STATE = "wyoming/satellite/state" +MQTT_TOPIC_AVAILABILITY = "wyoming/led/availability" +MQTT_DISCOVERY_PREFIX = "homeassistant" MQTT_CLIENT_ID = "wyoming-led-service" -# 🔹 MQTT Discovery -MQTT_DISCOVERY_PREFIX = "homeassistant" MQTT_DISCOVERY_DEVICE = { "identifiers": ["wyoming-led-repeaker"], "name": "Wyoming LED ReSpeaker", "manufacturer": "ReSpeaker", - "model": "XVF3800 USB 4-MIC Array", - "sw_version": "1.0", - "connections": [["mac", "unknown"]] + "model": "XVF3800", } -# 🔹 Глобальные настройки BRIGHTNESS_CONFIG = { "listening": 120, "wake": 200, "thinking": 150, "speaking": 140, "idle": 80, "paused": 40, "error": 255, "global_multiplier": 1.0 } -# 🔹 Глобальные переменные -shutdown_event = asyncio.Event() mqtt_client = None current_state = "idle" -last_mqtt_brightness = None # 🔹 Защита от эха: последнее значение из MQTT +last_mqtt_brightness = None +ha_disabled = False -# ============================================================ -# 💾 Настройки яркости -# ============================================================ +# ====================== CONFIG ====================== def load_brightness_config(): global BRIGHTNESS_CONFIG if CONFIG_FILE.exists(): try: - with open(CONFIG_FILE, "r") as f: - saved = json.load(f) - BRIGHTNESS_CONFIG.update(saved) - logger.info(f"📂 Загружены настройки из {CONFIG_FILE}") + with open(CONFIG_FILE) as f: + BRIGHTNESS_CONFIG.update(json.load(f)) + logger.info("✅ Загружены настройки яркости") except Exception as e: - logger.warning(f"⚠️ Ошибка загрузки конфига: {e}") + logger.warning(f"Ошибка загрузки config: {e}") else: save_brightness_config() - def save_brightness_config(): try: with open(CONFIG_FILE, "w") as f: json.dump(BRIGHTNESS_CONFIG, f, indent=2) - logger.debug(f"💾 Сохранены настройки яркости") except Exception as e: - logger.error(f"❌ Ошибка сохранения: {e}") + logger.error(f"Ошибка сохранения config: {e}") -# ============================================================ -# 🎛️ Управление LED -# ============================================================ +# ====================== LED ====================== def run_xvf_command(command: str, values=None): args = [sys.executable, XVF_HOST_PATH, command] if values: args.extend(["--values"] + [str(v) for v in values]) try: - result = subprocess.run(args, capture_output=True, text=True, check=True, timeout=5) - if result.stdout.strip(): - logger.debug(f"{command} output: {result.stdout.strip()}") + subprocess.run(args, capture_output=True, text=True, check=True, timeout=5) return True except Exception as e: - logger.error(f"❌ Ошибка {command}: {e}") + logger.error(f"LED error {command}: {e}") return False def get_effective_brightness(state: str) -> int: base = BRIGHTNESS_CONFIG.get(state, 100) - multiplier = BRIGHTNESS_CONFIG.get("global_multiplier", 1.0) - return int(base * multiplier) + return int(base * BRIGHTNESS_CONFIG.get("global_multiplier", 1.0)) def set_led_state(state: str, skip_publish=False): - """Устанавливает состояние LED. Если skip_publish=True — не публикуем в MQTT (избегаем эха)""" global current_state current_state = state - brightness = get_effective_brightness(state) - + if state == "listening": - run_xvf_command("LED_EFFECT", [0]) run_xvf_command("LED_EFFECT", [1]) run_xvf_command("LED_COLOR", ["0x00BCD4"]) run_xvf_command("LED_BRIGHTNESS", [brightness]) run_xvf_command("LED_SPEED", [2]) - logger.info(f"🔵 Listening: бирюзовое дыхание ({brightness})") - elif state == "wake": run_xvf_command("LED_EFFECT", [1]) run_xvf_command("LED_COLOR", ["0xFF0000"]) run_xvf_command("LED_BRIGHTNESS", [brightness]) - logger.info(f"🔴 Wake: красный ({brightness})") - elif state == "thinking": run_xvf_command("LED_EFFECT", [2]) run_xvf_command("LED_BRIGHTNESS", [brightness]) run_xvf_command("LED_SPEED", [4]) - logger.info(f"🌈 Thinking: радуга ({brightness})") - elif state == "speaking": run_xvf_command("LED_EFFECT", [1]) run_xvf_command("LED_COLOR", ["0x4CAF50"]) run_xvf_command("LED_BRIGHTNESS", [brightness]) - run_xvf_command("LED_SPEED", [2]) - logger.info(f"🟢 Speaking: зелёный ({brightness})") - elif state == "idle": run_xvf_command("LED_EFFECT", [4]) run_xvf_command("LED_DOA_COLOR", ["0x303030", "0x00BCD4"]) run_xvf_command("LED_BRIGHTNESS", [brightness]) - logger.info(f"⚪ Idle: DoA ({brightness})") - elif state == "paused": run_xvf_command("LED_EFFECT", [1]) run_xvf_command("LED_COLOR", ["0x404040"]) run_xvf_command("LED_BRIGHTNESS", [brightness]) - logger.info(f"⚫ Paused: серый ({brightness})") - elif state == "error": run_xvf_command("LED_EFFECT", [1]) run_xvf_command("LED_COLOR", ["0xFF00FF"]) run_xvf_command("LED_BRIGHTNESS", [brightness]) - logger.info(f"🟣 Error: маджента ({brightness})") - elif state == "off": run_xvf_command("LED_EFFECT", [0]) run_xvf_command("LED_BRIGHTNESS", [0]) - logger.info("⬛ Off: выключено") - else: - run_xvf_command("LED_EFFECT", [0]) - logger.info("⬛ Unknown") - # 🔹 Публикуем состояние только если не скипаем if not skip_publish: publish_state(state, brightness) -# ============================================================ -# 🔍 MQTT Discovery -# ============================================================ +# ====================== MQTT ====================== def publish_discovery_config(): - if not mqtt_client: + if not mqtt_client: return - try: - # 🔹 Number (слайдер) — читаем из STATE, пишем в SET - 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 = { + discovery = { "name": "Wyoming LED", "unique_id": "wyoming_led_light", "state_topic": MQTT_TOPIC_STATE, "command_topic": MQTT_TOPIC_COMMAND, - "brightness_state_topic": MQTT_TOPIC_BRIGHTNESS_STATE, "brightness_command_topic": MQTT_TOPIC_BRIGHTNESS_SET, - "brightness_scale": 255, + "brightness_state_topic": MQTT_TOPIC_BRIGHTNESS_STATE, "availability_topic": MQTT_TOPIC_AVAILABILITY, "payload_available": "online", "payload_not_available": "offline", - "payload_on": '{"type": "set_state", "state": "speaking"}', - "payload_off": '{"type": "set_state", "state": "idle"}', + "payload_on": "ON", + "payload_off": "OFF", + "brightness_scale": 100, # ← ИЗМЕНИЛИ НА 100 + "optimistic": False, + "qos": 1, "device": MQTT_DISCOVERY_DEVICE, - "object_id": "wyoming_led", - "schema": "json" } - - # Публикуем с retain=True для автообнаружения - mqtt_client.publish(f"{MQTT_DISCOVERY_PREFIX}/number/wyoming_led/config", - json.dumps(discovery_number), retain=True) - mqtt_client.publish(f"{MQTT_DISCOVERY_PREFIX}/sensor/wyoming_led/config", - json.dumps(discovery_sensor), retain=True) - mqtt_client.publish(f"{MQTT_DISCOVERY_PREFIX}/light/wyoming_led/config", - json.dumps(discovery_light), retain=True) - - logger.info("🔍 MQTT Discovery: конфигурации опубликованы") - + + mqtt_client.publish( + f"{MQTT_DISCOVERY_PREFIX}/light/wyoming_led/config", + json.dumps(discovery), + retain=True, + qos=1 + ) + logger.info("✅ Discovery опубликован (scale = 100)") 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): if rc == 0: - logger.info(f"✅ MQTT подключён к {MQTT_BROKER}:{MQTT_PORT}") - client.publish(MQTT_TOPIC_AVAILABILITY, "online", retain=True) - - # 🔹 Подписываемся ТОЛЬКО на командные топики (не на state!) + logger.info("✅ MQTT подключён") + client.publish(MQTT_TOPIC_AVAILABILITY, "online", retain=True, qos=1) client.subscribe(MQTT_TOPIC_COMMAND) client.subscribe(MQTT_TOPIC_BRIGHTNESS_SET) 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: - logger.error(f"❌ MQTT ошибка подключения: {rc}") - - -def on_mqtt_disconnect(client, userdata, rc, properties=None): - logger.warning(f"⚠️ MQTT отключён (код: {rc})") + logger.error(f"❌ MQTT ошибка: {rc}") def on_mqtt_message(client, userdata, msg): - """Обработчик входящих MQTT сообщений""" - global last_mqtt_brightness - + global ha_disabled, last_mqtt_brightness, current_state try: - topic = msg.topic - payload = msg.payload.decode('utf-8') - logger.debug(f"📩 MQTT {topic}: {payload}") + payload = msg.payload.decode('utf-8').strip() + logger.info(f"📩 {msg.topic}: {payload}") - # 🔹 Обработка яркости (только из _SET топика!) - if topic == MQTT_TOPIC_BRIGHTNESS_SET: - brightness = int(payload) - brightness = max(0, min(255, brightness)) - - # 🔹 Защита от эха: если значение не изменилось — игнорируем - if brightness == last_mqtt_brightness: - logger.debug(f"⏭️ Эхо: яркость {brightness} уже обработана") - return - - last_mqtt_brightness = brightness - BRIGHTNESS_CONFIG["global_multiplier"] = brightness / 255.0 - save_brightness_config() - - logger.info(f"🔆 Яркость: {brightness} (×{BRIGHTNESS_CONFIG['global_multiplier']:.2f})") - - # 🔹 Применяем к текущему состоянию, но НЕ публикуем обратно (skip_publish=True) - set_led_state(current_state, skip_publish=True) - - # 🔹 Публикуем отчёт в отдельный _STATE топик (без эха) - publish_brightness(brightness) + # ==================== ЯРКОСТЬ ==================== + if msg.topic == MQTT_TOPIC_BRIGHTNESS_SET: + try: + ha_value = max(0, min(100, int(payload))) # 0-100 + new_multiplier = ha_value / 100.0 + + last_mqtt_brightness = ha_value + BRIGHTNESS_CONFIG["global_multiplier"] = new_multiplier + save_brightness_config() + + logger.info(f"🔆 HA установил яркость: {ha_value}% → multiplier = {new_multiplier:.2f}") + + if not ha_disabled: + # Применяем новую яркость к текущему состоянию + set_led_state(current_state, skip_publish=False) + + except ValueError: + logger.warning(f"Неверное значение яркости: {payload}") + # ==================== КОМАНДА (ON / OFF) ==================== + elif msg.topic == MQTT_TOPIC_COMMAND: + ha_state = payload.upper() - # 🔹 Обработка команд - elif topic == MQTT_TOPIC_COMMAND: - command = json.loads(payload) - cmd_type = command.get("type", "") - - if cmd_type == "set_brightness": - brightness = max(0, min(255, command.get("value", 100))) - if brightness != last_mqtt_brightness: - last_mqtt_brightness = brightness - BRIGHTNESS_CONFIG["global_multiplier"] = brightness / 255.0 - save_brightness_config() - publish_brightness(brightness) - set_led_state(current_state, skip_publish=True) - logger.info(f"🔆 Яркость из команды: {brightness}") - - 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"🎨 Эффект применён") + if ha_state in ["ON", "OFF"]: + if ha_state == "OFF": + logger.info("🔴 HA → OFF") + 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 ha_state == "OFF": + ha_disabled = True + run_xvf_command("LED_EFFECT", [0]) + run_xvf_command("LED_BRIGHTNESS", [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() + set_led_state(current_state) + except: + logger.warning(f"Неизвестная команда: {payload}") - except json.JSONDecodeError: - logger.warning(f"⚠️ Неверный JSON: {payload}") except Exception as e: - logger.error(f"❌ Ошибка MQTT: {e}") - - -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}") + logger.error(f"❌ Ошибка обработки MQTT: {e}", exc_info=True) +# ====================== START ====================== def start_mqtt_client(): global mqtt_client - - mqtt_client = mqtt.Client( - callback_api_version=mqtt.CallbackAPIVersion.VERSION2, - client_id=MQTT_CLIENT_ID, - protocol=mqtt.MQTTv311 - ) - + mqtt_client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2, client_id=MQTT_CLIENT_ID) mqtt_client.username_pw_set(MQTT_USER, MQTT_PASS) mqtt_client.on_connect = on_mqtt_connect - mqtt_client.on_disconnect = on_mqtt_disconnect mqtt_client.on_message = on_mqtt_message mqtt_client.will_set(MQTT_TOPIC_AVAILABILITY, "offline", retain=True, qos=1) - try: - mqtt_client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60) - mqtt_client.loop_start() - logger.info("📡 MQTT клиент запущен") - except Exception as e: - logger.error(f"❌ Ошибка подключения MQTT: {e}") - - return mqtt_client + mqtt_client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60) + mqtt_client.loop_start() -def stop_mqtt_client(): - global mqtt_client +class LedEventHandler(AsyncEventHandler): + 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: 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): - 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() + except: + pass sys.exit(0) 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("--port", type=int, default=10701) args = parser.parse_args() @@ -505,16 +356,8 @@ if __name__ == "__main__": signal.signal(signal.SIGTERM, signal_handler) try: - logger.info("💡 Ctrl+C для остановки") asyncio.run(main(args)) except KeyboardInterrupt: - logger.info("⌨️ KeyboardInterrupt") - cleanup_leds() - stop_mqtt_client() - except Exception as e: - logger.error(f"❌ Ошибка: {e}") - cleanup_leds() - stop_mqtt_client() - sys.exit(1) + pass finally: - logger.info("👋 Завершено") \ No newline at end of file + cleanup_leds() \ No newline at end of file