init
This commit is contained in:
0
transport/backend/app/services/__init__.py
Normal file
0
transport/backend/app/services/__init__.py
Normal file
83
transport/backend/app/services/connection_checker.py
Normal file
83
transport/backend/app/services/connection_checker.py
Normal 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) # Проверять каждую минуту
|
||||
55
transport/backend/app/services/event_detector.py
Normal file
55
transport/backend/app/services/event_detector.py
Normal 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
|
||||
63
transport/backend/app/services/websocket_manager.py
Normal file
63
transport/backend/app/services/websocket_manager.py
Normal 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()
|
||||
Reference in New Issue
Block a user