Fix loglevel & discovery-subnet

This commit is contained in:
Артём Кокос
2026-02-24 21:21:20 +07:00
parent fd0b9d4d81
commit 450fd54b80
2 changed files with 54 additions and 41 deletions

View File

@@ -2,7 +2,7 @@ import asyncio
import json import json
import socket import socket
import logging import logging
import subprocess import os
import ipaddress import ipaddress
from typing import List, Dict from typing import List, Dict
@@ -14,21 +14,33 @@ class DiscoveryService:
self.port = port self.port = port
self.discover_msg = {"method": "getPilot", "params": {}} 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: try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
# Коннект не создает трафика, но заставляет ОС выбрать нужный интерфейс
s.connect(("8.8.8.8", 80)) s.connect(("8.8.8.8", 80))
local_ip = s.getsockname()[0] local_ip = s.getsockname()[0]
network = ipaddress.IPv4Network(f"{local_ip}/24", strict=False)
network = ipaddress.IPv4Network(f"{local_ip}/24", strict=False) return [str(network)]
return str(network)
except Exception as e: except Exception as e:
logger.error(f"Linux Discovery Error: Не удалось определить подсеть: {e}") logger.error(
return "192.168.1.0/24" f"Discovery Error: Не удалось определить подсеть автоматически: {e}"
)
return ["192.168.1.0/24"]
async def scan_network(self, timeout: float = 1.5) -> List[Dict]: async def scan_network(self, timeout: float = 2.0) -> List[Dict]:
subnet = self._get_local_subnet() subnets = self._get_target_subnets()
network = ipaddress.IPv4Network(subnet)
found_devices = [] found_devices = []
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -37,55 +49,56 @@ class DiscoveryService:
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
message = json.dumps(self.discover_msg).encode() 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: try:
sock.sendto(message, (str(ip), self.port)) network = ipaddress.IPv4Network(subnet)
except Exception: for ip in network.hosts():
continue try:
sock.sendto(message, (str(ip), self.port))
except Exception:
continue
except ValueError as e:
logger.error(f"❌ Неверный формат подсети {subnet}: {e}")
# Собираем ответы
start_time = loop.time() start_time = loop.time()
while (loop.time() - start_time) < timeout: while (loop.time() - start_time) < timeout:
try: try:
# Используем небольшой таймаут на чтение, чтобы успевать выходить из цикла
data, addr = await asyncio.wait_for( 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()) resp = json.loads(data.decode())
if "result" in resp: if "result" in resp:
res = resp["result"] res = resp["result"]
mac = res.get("mac") mac = res.get("mac")
ip_from_socket = addr[0]
if mac: if mac:
device_info = { found_devices.append(
"mac": mac, {
"ip": ip_from_socket, "mac": mac,
"state": { "ip": addr[0],
"on": res.get("state"), "state": {
"dimming": res.get("dimming"), "on": res.get("state"),
"temp": res.get("temp"), "dimming": res.get("dimming"),
}, "temp": res.get("temp"),
} },
found_devices.append(device_info) }
# Красивый лог с IP и MAC
logger.info(
f" [+] Найдена лампа: {ip_from_socket} | MAC: {mac}"
) )
logger.info(f" [+] Найдена лампа: {addr[0]} | MAC: {mac}")
except (asyncio.TimeoutError, json.JSONDecodeError): except (asyncio.TimeoutError, json.JSONDecodeError):
continue continue
except Exception as e: except Exception:
# На Linux Errno 11 (EAGAIN) тут может выскочить, если буфер пуст
await asyncio.sleep(0.01) await asyncio.sleep(0.01)
continue continue
sock.close() sock.close()
# Фильтруем дубликаты (если лампа ответила несколько раз) # Фильтруем дубликаты
unique_devices = list({d["mac"]: d for d in found_devices}.values()) return list({d["mac"]: d for d in found_devices}.values())
return unique_devices
async def start_background_discovery(self, state_manager, interval=600): async def start_background_discovery(self, state_manager, interval=600):
"""Запускает бесконечный цикл сканирования.""" """Запускает бесконечный цикл сканирования."""
@@ -98,5 +111,5 @@ class DiscoveryService:
f"📡 Discovery: онлайн {len(state_manager.devices)} устройств" f"📡 Discovery: онлайн {len(state_manager.devices)} устройств"
) )
except Exception as e: except Exception as e:
print(f"❌ Discovery error: {e}") logger.error(f"❌ Discovery background error: {e}")
await asyncio.sleep(interval) await asyncio.sleep(interval)

View File

@@ -1,5 +1,6 @@
import logging import logging
import asyncio import asyncio
import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@@ -11,10 +12,9 @@ from sqlalchemy import select
from app.models.device import GroupModel from app.models.device import GroupModel
from app.api.routes import devices, control, schedules from app.api.routes import devices, control, schedules
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
logging.basicConfig( logging.basicConfig(level=LOG_LEVEL, format="%(asctime)s | %(levelname)s | %(message)s")
level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s"
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)