This commit is contained in:
2025-12-18 21:13:49 +03:00
parent 84b934036b
commit 030af7ca83
45 changed files with 3106 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
"""
Фоновая задача для проверки потери связи с объектами.
Создаёт события CONNECTION_LOST если нет данных более N минут.
"""
import asyncio
from datetime import datetime, timedelta
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import async_session_maker
from app.models import Vehicle, Position, Event
from app.config import settings
from app.services.websocket_manager import manager
async def check_connections():
"""Проверить все объекты на потерю связи"""
async with async_session_maker() as db:
# Получить все объекты
result = await db.execute(select(Vehicle))
vehicles = result.scalars().all()
now = datetime.utcnow()
threshold = now - timedelta(minutes=settings.connection_lost_minutes)
for vehicle in vehicles:
# Получить последнюю позицию
pos_result = await db.execute(
select(Position)
.where(Position.vehicle_id == vehicle.id)
.order_by(desc(Position.timestamp))
.limit(1)
)
last_pos = pos_result.scalar_one_or_none()
if not last_pos:
continue
# Проверить, прошло ли достаточно времени
if last_pos.timestamp < threshold:
# Проверить, не было ли уже события CONNECTION_LOST за последние N минут
event_result = await db.execute(
select(Event)
.where(Event.vehicle_id == vehicle.id)
.where(Event.type == "CONNECTION_LOST")
.where(Event.timestamp > threshold)
.limit(1)
)
existing_event = event_result.scalar_one_or_none()
if not existing_event:
# Создать событие
minutes_ago = (now - last_pos.timestamp).total_seconds() / 60
event = Event(
vehicle_id=vehicle.id,
timestamp=now,
type="CONNECTION_LOST",
payload={
"last_seen": last_pos.timestamp.isoformat(),
"minutes_ago": round(minutes_ago, 1),
"lat": last_pos.lat,
"lon": last_pos.lon
}
)
db.add(event)
await db.commit()
await db.refresh(event)
# Отправить через WebSocket
await manager.broadcast_event(event)
print(f"⚠️ CONNECTION_LOST: {vehicle.name} (нет данных {minutes_ago:.0f} мин)")
async def connection_checker_loop():
"""Бесконечный цикл проверки соединений"""
print("🔍 Connection checker started")
while True:
try:
await check_connections()
except Exception as e:
print(f"Connection checker error: {e}")
await asyncio.sleep(60) # Проверять каждую минуту

View File

@@ -0,0 +1,55 @@
from datetime import datetime, timedelta
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import Vehicle, Position, Event
from app.config import settings
async def detect_events(
db: AsyncSession,
vehicle: Vehicle,
current: Position,
previous: Optional[Position]
) -> list[Event]:
"""Обнаружение событий на основе новой позиции"""
events = []
# Check for overspeed
if current.speed > settings.overspeed_limit:
event = Event(
vehicle_id=vehicle.id,
timestamp=current.timestamp,
type="OVERSPEED",
payload={
"speed": current.speed,
"limit": settings.overspeed_limit,
"lat": current.lat,
"lon": current.lon
}
)
db.add(event)
await db.commit()
await db.refresh(event)
events.append(event)
# Check for long stop (if speed is 0 and was 0 for a while)
if previous and current.speed < 2 and previous.speed < 2:
time_diff = current.timestamp - previous.timestamp
if time_diff >= timedelta(minutes=settings.long_stop_minutes):
event = Event(
vehicle_id=vehicle.id,
timestamp=current.timestamp,
type="LONG_STOP",
payload={
"duration_minutes": time_diff.total_seconds() / 60,
"lat": current.lat,
"lon": current.lon
}
)
db.add(event)
await db.commit()
await db.refresh(event)
events.append(event)
return events

View File

@@ -0,0 +1,63 @@
import json
from datetime import datetime
from typing import Set
from fastapi import WebSocket
from app.models import Position, Event
class ConnectionManager:
def __init__(self):
self.active_connections: Set[WebSocket] = set()
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.add(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.discard(websocket)
async def broadcast(self, message: dict):
"""Отправить сообщение всем подключенным клиентам"""
disconnected = set()
for connection in self.active_connections:
try:
await connection.send_json(message)
except Exception:
disconnected.add(connection)
# Remove disconnected clients
self.active_connections -= disconnected
async def broadcast_position(self, position: Position):
"""Отправить обновление позиции"""
message = {
"type": "position_update",
"data": {
"vehicle_id": position.vehicle_id,
"lat": position.lat,
"lon": position.lon,
"speed": position.speed,
"heading": position.heading,
"timestamp": position.timestamp.isoformat()
}
}
await self.broadcast(message)
async def broadcast_event(self, event: Event):
"""Отправить событие"""
message = {
"type": "event",
"data": {
"id": event.id,
"vehicle_id": event.vehicle_id,
"type": event.type,
"payload": event.payload,
"timestamp": event.timestamp.isoformat()
}
}
await self.broadcast(message)
# Global instance
manager = ConnectionManager()