Реализация добавления/удаления/изменения заметок для точек доступа Wi-Fi по ssid
This commit is contained in:
parent
58fa7d1ee6
commit
3eff71bc97
4 changed files with 198 additions and 39 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -6,4 +6,5 @@ __pycache__
|
||||||
.wheels
|
.wheels
|
||||||
.vendor
|
.vendor
|
||||||
.python3-deb
|
.python3-deb
|
||||||
SRV/storage/devices/devices.json
|
SRV/storage/devices/devices.json
|
||||||
|
SRV/storage/wifi/wifi.json
|
||||||
|
|
@ -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)}")
|
||||||
|
|
@ -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):
|
||||||
|
|
@ -35,4 +37,4 @@ class WifiScanResult(BaseModel):
|
||||||
access_points (List[WifiAccessPoint]): Список найденных Wi-Fi точек доступа
|
access_points (List[WifiAccessPoint]): Список найденных Wi-Fi точек доступа
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_points: List[WifiAccessPoint]
|
access_points: List[WifiAccessPoint]
|
||||||
|
|
@ -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)}"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue