Реализация добавления/удаления/изменения заметок для точек доступа Wi-Fi по ssid

This commit is contained in:
parent 58fa7d1ee6
commit 3eff71bc97
4 changed files with 198 additions and 39 deletions

1
.gitignore vendored
View file

@ -7,3 +7,4 @@ __pycache__
.vendor .vendor
.python3-deb .python3-deb
SRV/storage/devices/devices.json SRV/storage/devices/devices.json
SRV/storage/wifi/wifi.json

View file

@ -8,7 +8,6 @@ from services.wifi_service import WifiService
router = APIRouter() router = APIRouter()
@router.get("/scan", response_model=SubnetScanResult) @router.get("/scan", response_model=SubnetScanResult)
def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None): def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None):
"""Сканирует локальную подсеть и возвращает данные об устройствах""" """Сканирует локальную подсеть и возвращает данные об устройствах"""
@ -17,9 +16,7 @@ def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None):
result = subnet_service.scan_subnet(subnet, ports) result = subnet_service.scan_subnet(subnet, ports)
return result return result
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, raise HTTPException(status_code=500, detail=f"Ошибка сканирования: {str(e)}")
detail=f"Ошибка сканирования: {str(e)}")
@router.get("/wifi", response_model=WifiScanResult) @router.get("/wifi", response_model=WifiScanResult)
def scan_wifi(timeout: int = 30): def scan_wifi(timeout: int = 30):
@ -29,9 +26,7 @@ def scan_wifi(timeout: int = 30):
result = wifi_service.scan_wifi(timeout=timeout) result = wifi_service.scan_wifi(timeout=timeout)
return result return result
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, raise HTTPException(status_code=500, detail=f"Ошибка сканирования Wi-Fi: {str(e)}")
detail=f"Ошибка сканирования Wi-Fi: {str(e)}")
@router.post("/devices/{mac}/note") @router.post("/devices/{mac}/note")
def add_note(mac: str, request: NoteRequest): def add_note(mac: str, request: NoteRequest):
@ -44,16 +39,11 @@ def add_note(mac: str, request: NoteRequest):
raise HTTPException(status_code=404, detail=error_message) raise HTTPException(status_code=404, detail=error_message)
elif "уже существует" in error_message: elif "уже существует" in error_message:
raise HTTPException(status_code=400, detail=error_message) raise HTTPException(status_code=400, detail=error_message)
return { return {"message": f"Заметка '{request.note}' добавлена для устройства {mac}"}
"message":
f"Заметка '{request.note}' добавлена для устройства {mac}"
}
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, raise HTTPException(status_code=500, detail=f"Ошибка добавления заметки: {str(e)}")
detail=f"Ошибка добавления заметки: {str(e)}")
@router.put("/devices/{mac}/note") @router.put("/devices/{mac}/note")
def update_note(mac: str, request: NoteRequest): def update_note(mac: str, request: NoteRequest):
@ -66,16 +56,11 @@ def update_note(mac: str, request: NoteRequest):
raise HTTPException(status_code=404, detail=error_message) raise HTTPException(status_code=404, detail=error_message)
elif "не существует" in error_message: elif "не существует" in error_message:
raise HTTPException(status_code=400, detail=error_message) raise HTTPException(status_code=400, detail=error_message)
return { return {"message": f"Заметка '{request.note}' обновлена для устройства {mac}"}
"message":
f"Заметка '{request.note}' обновлена для устройства {mac}"
}
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, raise HTTPException(status_code=500, detail=f"Ошибка обновления заметки: {str(e)}")
detail=f"Ошибка обновления заметки: {str(e)}")
@router.delete("/devices/{mac}/note") @router.delete("/devices/{mac}/note")
def delete_note(mac: str): def delete_note(mac: str):
@ -92,5 +77,55 @@ def delete_note(mac: str):
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, raise HTTPException(status_code=500, detail=f"Ошибка удаления заметки: {str(e)}")
detail=f"Ошибка удаления заметки: {str(e)}")
@router.post("/wifi/{ssid}/note")
def add_wifi_note(ssid: str, request: NoteRequest):
"""Добавляет заметку для сети Wi-Fi по SSID"""
try:
wifi_service = WifiService()
success, error_message = wifi_service.add_note(ssid, request.note)
if not success:
if "не найдена" in error_message:
raise HTTPException(status_code=404, detail=error_message)
elif "уже существует" in error_message:
raise HTTPException(status_code=400, detail=error_message)
return {"message": f"Заметка '{request.note}' добавлена для сети с SSID {ssid}"}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка добавления заметки: {str(e)}")
@router.put("/wifi/{ssid}/note")
def update_wifi_note(ssid: str, request: NoteRequest):
"""Обновляет заметку для сети Wi-Fi по SSID"""
try:
wifi_service = WifiService()
success, error_message = wifi_service.update_note(ssid, request.note)
if not success:
if "не найдена" in error_message:
raise HTTPException(status_code=404, detail=error_message)
elif "не существует" in error_message:
raise HTTPException(status_code=400, detail=error_message)
return {"message": f"Заметка '{request.note}' обновлена для сети с SSID {ssid}"}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка обновления заметки: {str(e)}")
@router.delete("/wifi/{ssid}/note")
def delete_wifi_note(ssid: str):
"""Удаляет заметку для сети Wi-Fi по SSID"""
try:
wifi_service = WifiService()
success, error_message = wifi_service.delete_note(ssid)
if not success:
if "не найдена" in error_message:
raise HTTPException(status_code=404, detail=error_message)
elif "не существует" in error_message:
raise HTTPException(status_code=400, detail=error_message)
return {"message": f"Заметка удалена для сети с SSID {ssid}"}
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка удаления заметки: {str(e)}")

View file

@ -15,6 +15,7 @@ class WifiAccessPoint(BaseModel):
frequency (Optional[str]): Частота вещания (2.4GHz/5GHz) frequency (Optional[str]): Частота вещания (2.4GHz/5GHz)
vendor (Optional[str]): Производитель точки доступа (определяется по MAC-адресу) vendor (Optional[str]): Производитель точки доступа (определяется по MAC-адресу)
hidden (bool): Флаг скрытия названия точки доступа (ssid) hidden (bool): Флаг скрытия названия точки доступа (ssid)
note (Optional[str]): Заметка для точки доступа
""" """
ssid: Optional[str] = None ssid: Optional[str] = None
@ -25,6 +26,7 @@ class WifiAccessPoint(BaseModel):
frequency: Optional[str] = None frequency: Optional[str] = None
vendor: Optional[str] = None vendor: Optional[str] = None
hidden: bool = False hidden: bool = False
note: Optional[str] = None
class WifiScanResult(BaseModel): class WifiScanResult(BaseModel):

View file

@ -3,17 +3,48 @@ import logging
import subprocess import subprocess
import re import re
from functools import lru_cache from functools import lru_cache
from typing import List, Optional, Dict from typing import List, Optional, Dict, Tuple
from mac_vendor_lookup import MacLookup from mac_vendor_lookup import MacLookup
import pywifi import pywifi
from pywifi import Profile from pywifi import Profile
from models.wifi import WifiAccessPoint, WifiScanResult from models.wifi import WifiAccessPoint, WifiScanResult
from pathlib import Path
import json
from datetime import datetime
logging.basicConfig( logging.basicConfig(
level=logging.DEBUG, level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WifiStorage:
"""Класс для хранения данных о Wi-Fi сетях и их заметках по SSID."""
def __init__(self, storage_path: str = "storage/wifi/wifi.json"):
self.storage_file = Path(storage_path)
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
def load_notes(self) -> Dict[str, Optional[str]]:
"""Загружает заметки по SSID из файла хранения."""
try:
if self.storage_file.exists():
with open(self.storage_file, "r", encoding="utf-8") as f:
data = json.load(f)
return {item["ssid"]: item.get("note") for item in data.get("networks", [])}
return {}
except Exception as e:
logger.error(f"Ошибка при загрузке заметок: {str(e)}")
return {}
def save_notes(self, notes: Dict[str, Optional[str]]) -> None:
"""Сохраняет заметки по SSID в файл."""
try:
data = {"networks": [{"ssid": ssid, "note": note} for ssid, note in notes.items()]}
with open(self.storage_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
logger.info(f"Сохранено {len(notes)} заметок в {self.storage_file}")
except Exception as e:
logger.error(f"Ошибка при сохранении заметок: {str(e)}")
class WifiService: class WifiService:
"""Сервис для сканирования текущих Wi-Fi точек доступа""" """Сервис для сканирования текущих Wi-Fi точек доступа"""
@ -22,6 +53,7 @@ class WifiService:
self.mac_lookup = MacLookup() self.mac_lookup = MacLookup()
self.wifi = pywifi.PyWiFi() self.wifi = pywifi.PyWiFi()
self.interface = self._get_wifi_interface() self.interface = self._get_wifi_interface()
self.storage = WifiStorage()
def _get_wifi_interface(self) -> Optional[object]: def _get_wifi_interface(self) -> Optional[object]:
"""Автоматически определяет активный Wi-Fi-интерфейс""" """Автоматически определяет активный Wi-Fi-интерфейс"""
@ -37,9 +69,7 @@ class WifiService:
for iface in interfaces: for iface in interfaces:
iface_name = iface.name() iface_name = iface.name()
if ( if "p2p" in iface_name.lower():
"p2p" in iface_name.lower()
): # Исключаю сетевые интерфейсы типа p2p из выбора, возможно сделать решение получше нежели исключать интерфейсы
logger.debug(f"Пропущен интерфейс {iface_name} (P2P)") logger.debug(f"Пропущен интерфейс {iface_name} (P2P)")
continue continue
try: try:
@ -208,21 +238,16 @@ class WifiService:
f"Начало сканирования Wi-Fi на интерфейсе {self.interface.name()}" f"Начало сканирования Wi-Fi на интерфейсе {self.interface.name()}"
) )
self.interface.scan() self.interface.scan()
time.sleep( time.sleep(10)
10
) # Нативное значение для wifi интерфейса предназначенное на сканирование и сбор результата. Оптимальное решение от 5 до 15 секунд
py_results = self.interface.scan_results() py_results = self.interface.scan_results()
logger.debug(f"Найдено {len(py_results)} точек через pywifi") logger.debug(f"Найдено {len(py_results)} точек через pywifi")
iw_results = self._get_iw_scan_data() iw_results = self._get_iw_scan_data()
iw_by_bssid = { iw_by_bssid = {ap["bssid"]: ap for ap in iw_results if ap.get("bssid")}
ap["bssid"]: ap
for ap in iw_results if ap.get("bssid") best_aps_by_ssid: Dict[str, WifiAccessPoint] = {}
} saved_notes = self.storage.load_notes()
best_aps_by_ssid: Dict[str, WifiAccessPoint] = (
{}
) # Сделал словарь, чтобы агрегировать точки доступа у сетей для выявления лучшего
for ap in py_results: for ap in py_results:
bssid = ap.bssid.lower() if ap.bssid else None bssid = ap.bssid.lower() if ap.bssid else None
if not bssid: if not bssid:
@ -257,9 +282,14 @@ class WifiService:
if ssid not in best_aps_by_ssid or ( if ssid not in best_aps_by_ssid or (
current_ap.signal and best_aps_by_ssid[ssid].signal current_ap.signal and best_aps_by_ssid[ssid].signal
and current_ap.signal > best_aps_by_ssid[ssid].signal): and current_ap.signal > best_aps_by_ssid[ssid].signal):
# При обновлении или добавлении точки сохраняем заметку из saved_notes
current_ap.note = saved_notes.get(ssid)
best_aps_by_ssid[ssid] = current_ap best_aps_by_ssid[ssid] = current_ap
aps = list(best_aps_by_ssid.values()) aps = list(best_aps_by_ssid.values())
# Сохраняем заметки для всех найденных SSID
notes_to_save = {ap.ssid: ap.note for ap in aps if ap.ssid}
self.storage.save_notes(notes_to_save)
logger.info( logger.info(
f"Сканирование Wi-Fi завершено за {time.time() - start_time:.2f} секунд, " f"Сканирование Wi-Fi завершено за {time.time() - start_time:.2f} секунд, "
@ -269,3 +299,94 @@ class WifiService:
except Exception as e: except Exception as e:
logger.error(f"Ошибка при сканировании Wi-Fi: {str(e)}") logger.error(f"Ошибка при сканировании Wi-Fi: {str(e)}")
return WifiScanResult(access_points=aps) return WifiScanResult(access_points=aps)
def add_note(self, ssid: str, note: str) -> Tuple[bool, Optional[str]]:
"""Добавляет заметку к сети Wi-Fi по SSID.
Args:
ssid (str): SSID сети Wi-Fi.
note (str): Текст заметки.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None).
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
"""
try:
saved_notes = self.storage.load_notes()
if ssid not in saved_notes or saved_notes[ssid] is None:
logger.info(f"Добавлена новая заметка для сети с SSID {ssid}")
elif saved_notes[ssid] is not None:
logger.error(f"Заметка для сети с SSID {ssid} уже существует")
return False, f"Заметка для сети с SSID {ssid} уже существует"
saved_notes[ssid] = note
self.storage.save_notes(saved_notes)
logger.info(f"Добавлена заметка '{note}' для сети с SSID {ssid}")
return True, None
except Exception as e:
logger.error(f"Ошибка при добавлении заметки для SSID {ssid}: {str(e)}")
return False, f"Внутренняя ошибка сервера: {str(e)}"
def update_note(self, ssid: str, note: str) -> Tuple[bool, Optional[str]]:
"""Обновляет заметку сети Wi-Fi по SSID.
Args:
ssid (str): SSID сети Wi-Fi.
note (str): Новый текст заметки.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None).
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
"""
try:
saved_notes = self.storage.load_notes()
if ssid not in saved_notes:
logger.warning(f"Сеть с SSID {ssid} не найдена")
return False, f"Сеть с SSID {ssid} не найдена"
if saved_notes[ssid] is None:
logger.error(f"Заметка для сети с SSID {ssid} не существует")
return False, f"Заметка для сети с SSID {ssid} не существует"
saved_notes[ssid] = note
self.storage.save_notes(saved_notes)
logger.info(f"Обновлена заметка '{note}' для сети с SSID {ssid}")
return True, None
except Exception as e:
logger.error(f"Ошибка при обновлении заметки для SSID {ssid}: {str(e)}")
return False, f"Внутренняя ошибка сервера: {str(e)}"
def delete_note(self, ssid: str) -> Tuple[bool, Optional[str]]:
"""Удаляет заметку сети Wi-Fi по SSID.
Args:
ssid (str): SSID сети Wi-Fi.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None).
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
"""
try:
saved_notes = self.storage.load_notes()
if ssid not in saved_notes:
logger.warning(f"Сеть с SSID {ssid} не найдена")
return False, f"Сеть с SSID {ssid} не найдена"
if saved_notes[ssid] is None:
logger.error(f"Заметка для сети с SSID {ssid} не существует")
return False, f"Заметка для сети с SSID {ssid} не существует"
saved_notes[ssid] = None
self.storage.save_notes(saved_notes)
logger.info(f"Удалена заметка для сети с SSID {ssid}")
return True, None
except Exception as e:
logger.error(f"Ошибка при удалении заметки для SSID {ssid}: {str(e)}")
return False, f"Внутренняя ошибка сервера: {str(e)}"