From 450fd54b80ad1d6c4e31db9914de88e86c36ce71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=9A=D0=BE=D0=BA=D0=BE?= =?UTF-8?q?=D1=81?= Date: Tue, 24 Feb 2026 21:21:20 +0700 Subject: [PATCH] Fix loglevel & discovery-subnet --- app/core/discovery.py | 89 +++++++++++++++++++++++++------------------ main.py | 6 +-- 2 files changed, 54 insertions(+), 41 deletions(-) diff --git a/app/core/discovery.py b/app/core/discovery.py index 5dad275..ba4368b 100644 --- a/app/core/discovery.py +++ b/app/core/discovery.py @@ -2,7 +2,7 @@ import asyncio import json import socket import logging -import subprocess +import os import ipaddress from typing import List, Dict @@ -14,21 +14,33 @@ class DiscoveryService: self.port = port self.discover_msg = {"method": "getPilot", "params": {}} - def _get_local_subnet(self) -> str: + def _get_target_subnets(self) -> List[str]: + """ + Определяет список подсетей для сканирования. + Приоритет: + 1. Переменная окружения SCAN_NETWORK (можно через запятую: "192.168.0.0/24,192.168.1.0/24") + 2. Автоопределение по дефолтному шлюзу + """ + env_network = os.getenv("SCAN_NETWORK") + if env_network: + return [s.strip() for s in env_network.split(",")] + + # Автоопределение (твой старый метод) try: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + # Коннект не создает трафика, но заставляет ОС выбрать нужный интерфейс s.connect(("8.8.8.8", 80)) local_ip = s.getsockname()[0] - - network = ipaddress.IPv4Network(f"{local_ip}/24", strict=False) - return str(network) + network = ipaddress.IPv4Network(f"{local_ip}/24", strict=False) + return [str(network)] except Exception as e: - logger.error(f"Linux Discovery Error: Не удалось определить подсеть: {e}") - return "192.168.1.0/24" + logger.error( + f"Discovery Error: Не удалось определить подсеть автоматически: {e}" + ) + return ["192.168.1.0/24"] - async def scan_network(self, timeout: float = 1.5) -> List[Dict]: - subnet = self._get_local_subnet() - network = ipaddress.IPv4Network(subnet) + async def scan_network(self, timeout: float = 2.0) -> List[Dict]: + subnets = self._get_target_subnets() found_devices = [] sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -37,55 +49,56 @@ class DiscoveryService: loop = asyncio.get_running_loop() message = json.dumps(self.discover_msg).encode() - logger.info(f"🚀 Начинаю сканирование сети {subnet}...") + logger.debug(f"🚀 Начинаю сканирование сетей: {', '.join(subnets)}...") - # Рассылаем запросы - for ip in network.hosts(): + # Рассылаем запросы по всем целевым сетям + for subnet in subnets: try: - sock.sendto(message, (str(ip), self.port)) - except Exception: - continue + network = ipaddress.IPv4Network(subnet) + for ip in network.hosts(): + try: + sock.sendto(message, (str(ip), self.port)) + except Exception: + continue + except ValueError as e: + logger.error(f"❌ Неверный формат подсети {subnet}: {e}") + # Собираем ответы start_time = loop.time() while (loop.time() - start_time) < timeout: try: + # Используем небольшой таймаут на чтение, чтобы успевать выходить из цикла data, addr = await asyncio.wait_for( - loop.run_in_executor(None, sock.recvfrom, 1024), timeout=0.1 + loop.run_in_executor(None, sock.recvfrom, 1024), timeout=0.2 ) resp = json.loads(data.decode()) if "result" in resp: res = resp["result"] mac = res.get("mac") - ip_from_socket = addr[0] - if mac: - device_info = { - "mac": mac, - "ip": ip_from_socket, - "state": { - "on": res.get("state"), - "dimming": res.get("dimming"), - "temp": res.get("temp"), - }, - } - found_devices.append(device_info) - # Красивый лог с IP и MAC - logger.info( - f" [+] Найдена лампа: {ip_from_socket} | MAC: {mac}" + found_devices.append( + { + "mac": mac, + "ip": addr[0], + "state": { + "on": res.get("state"), + "dimming": res.get("dimming"), + "temp": res.get("temp"), + }, + } ) + logger.info(f" [+] Найдена лампа: {addr[0]} | MAC: {mac}") except (asyncio.TimeoutError, json.JSONDecodeError): continue - except Exception as e: - # На Linux Errno 11 (EAGAIN) тут может выскочить, если буфер пуст + except Exception: await asyncio.sleep(0.01) continue sock.close() - # Фильтруем дубликаты (если лампа ответила несколько раз) - unique_devices = list({d["mac"]: d for d in found_devices}.values()) - return unique_devices + # Фильтруем дубликаты + return list({d["mac"]: d for d in found_devices}.values()) async def start_background_discovery(self, state_manager, interval=600): """Запускает бесконечный цикл сканирования.""" @@ -98,5 +111,5 @@ class DiscoveryService: f"📡 Discovery: онлайн {len(state_manager.devices)} устройств" ) except Exception as e: - print(f"❌ Discovery error: {e}") + logger.error(f"❌ Discovery background error: {e}") await asyncio.sleep(interval) diff --git a/main.py b/main.py index 58f392f..b8f71cb 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ import logging import asyncio +import os from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.staticfiles import StaticFiles @@ -11,10 +12,9 @@ from sqlalchemy import select from app.models.device import GroupModel from app.api.routes import devices, control, schedules +LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper() -logging.basicConfig( - level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s" -) +logging.basicConfig(level=LOG_LEVEL, format="%(asctime)s | %(levelname)s | %(message)s") logger = logging.getLogger(__name__)