213 lines
9.3 KiB
Python
213 lines
9.3 KiB
Python
"""
|
|
Smart Odometer Service - Adminisztrátor által paraméterezhető kilométeróra becslés.
|
|
|
|
A szolgáltatás a járművek kilométeróra állását becsüli a költségbejegyzések alapján,
|
|
figyelembe véve a rendszerparamétereket (ODOMETER_MIN_DAYS_FOR_AVG, ODOMETER_CONFIDENCE_THRESHOLD).
|
|
Ha az admin beállított manuális átlagot (manual_override_avg), akkor azt használja.
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import Optional, Tuple
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func, and_
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from app.models.vehicle import VehicleOdometerState, VehicleCost
|
|
from app.models.system import SystemParameter
|
|
from app.models.vehicle_definitions import VehicleModelDefinition
|
|
|
|
|
|
class OdometerService:
|
|
"""Kilométeróra becslési szolgáltatás adminisztrációs kontrollal."""
|
|
|
|
@staticmethod
|
|
async def get_system_param(db: AsyncSession, key: str, default_value):
|
|
"""Rendszerparaméter lekérése a system.system_parameters táblából."""
|
|
stmt = select(SystemParameter).where(
|
|
SystemParameter.key == key,
|
|
SystemParameter.scope_level == 'global',
|
|
SystemParameter.is_active == True
|
|
)
|
|
result = await db.execute(stmt)
|
|
param = result.scalars().first()
|
|
if param and 'value' in param.value:
|
|
return param.value['value']
|
|
return default_value
|
|
|
|
@staticmethod
|
|
async def update_vehicle_stats(db: AsyncSession, vehicle_id: int) -> Optional[VehicleOdometerState]:
|
|
"""
|
|
Frissíti a jármű kilométeróra statisztikáit.
|
|
|
|
Algoritmus:
|
|
1. Ha van manual_override_avg, használja azt.
|
|
2. Különben számol átlagot a vehicle.costs bejegyzésekből.
|
|
3. Figyelembe veszi az ODOMETER_MIN_DAYS_FOR_AVG paramétert.
|
|
4. Kiszámolja a confidence_score-t a minták száma alapján.
|
|
5. Frissíti vagy létrehozza a VehicleOdometerState rekordot.
|
|
"""
|
|
# Rendszerparaméterek lekérése
|
|
min_days = await OdometerService.get_system_param(db, 'ODOMETER_MIN_DAYS_FOR_AVG', 7)
|
|
confidence_threshold = await OdometerService.get_system_param(db, 'ODOMETER_CONFIDENCE_THRESHOLD', 0.5)
|
|
|
|
# Meglévő állapot lekérése
|
|
stmt = select(VehicleOdometerState).where(VehicleOdometerState.vehicle_id == vehicle_id)
|
|
result = await db.execute(stmt)
|
|
odometer_state = result.scalars().first()
|
|
|
|
# Költségbejegyzések lekérése dátum és odometer szerint rendezve
|
|
cost_stmt = select(VehicleCost).where(
|
|
VehicleCost.vehicle_id == vehicle_id,
|
|
VehicleCost.odometer.isnot(None)
|
|
).order_by(VehicleCost.date.asc())
|
|
|
|
cost_result = await db.execute(cost_stmt)
|
|
costs = cost_result.scalars().all()
|
|
|
|
if not costs:
|
|
# Nincs adat, alapértelmezett értékek
|
|
if odometer_state:
|
|
odometer_state.daily_avg_distance = 0
|
|
odometer_state.confidence_score = 0
|
|
odometer_state.estimated_current_odometer = odometer_state.last_recorded_odometer
|
|
else:
|
|
# Jármű alapadatok lekérése
|
|
vehicle_stmt = select(VehicleModelDefinition).where(VehicleModelDefinition.id == vehicle_id)
|
|
vehicle_result = await db.execute(vehicle_stmt)
|
|
vehicle = vehicle_result.scalars().first()
|
|
|
|
if not vehicle:
|
|
return None
|
|
|
|
odometer_state = VehicleOdometerState(
|
|
vehicle_id=vehicle_id,
|
|
last_recorded_odometer=0,
|
|
last_recorded_date=datetime.now(),
|
|
daily_avg_distance=0,
|
|
estimated_current_odometer=0,
|
|
confidence_score=0,
|
|
manual_override_avg=None
|
|
)
|
|
db.add(odometer_state)
|
|
|
|
await db.commit()
|
|
await db.refresh(odometer_state)
|
|
return odometer_state
|
|
|
|
# Utolsó rögzített adatok
|
|
last_cost = costs[-1]
|
|
last_recorded_odometer = last_cost.odometer
|
|
last_recorded_date = last_cost.date
|
|
|
|
# Manuális átlag ellenőrzése
|
|
if odometer_state and odometer_state.manual_override_avg is not None:
|
|
daily_avg = float(odometer_state.manual_override_avg)
|
|
confidence = 1.0 # Manuális beállítás esetén teljes bizalom
|
|
else:
|
|
# Átlag számítása a költségbejegyzésekből
|
|
valid_pairs = []
|
|
for i in range(1, len(costs)):
|
|
prev = costs[i-1]
|
|
curr = costs[i]
|
|
|
|
days_diff = (curr.date - prev.date).days
|
|
km_diff = curr.odometer - prev.odometer
|
|
|
|
if days_diff >= min_days and km_diff > 0:
|
|
daily_avg = km_diff / days_diff
|
|
valid_pairs.append((daily_avg, days_diff))
|
|
|
|
if valid_pairs:
|
|
# Súlyozott átlag (hosszabb időszakok nagyobb súllyal)
|
|
total_weighted = sum(avg * weight for avg, weight in valid_pairs)
|
|
total_days = sum(weight for _, weight in valid_pairs)
|
|
daily_avg = total_weighted / total_days if total_days > 0 else 0
|
|
|
|
# Confidence score: érvényes párok száma / összes lehetséges párok
|
|
confidence = min(len(valid_pairs) / max(len(costs) - 1, 1), 1.0)
|
|
else:
|
|
daily_avg = 0
|
|
confidence = 0
|
|
|
|
# Becsült jelenlegi kilométer
|
|
days_since_last = (datetime.now(last_recorded_date.tzinfo) - last_recorded_date).days
|
|
estimated_odometer = last_recorded_odometer + (daily_avg * max(days_since_last, 0))
|
|
|
|
# Állapot frissítése vagy létrehozása
|
|
if odometer_state:
|
|
odometer_state.last_recorded_odometer = last_recorded_odometer
|
|
odometer_state.last_recorded_date = last_recorded_date
|
|
odometer_state.daily_avg_distance = daily_avg
|
|
odometer_state.estimated_current_odometer = estimated_odometer
|
|
odometer_state.confidence_score = confidence
|
|
else:
|
|
odometer_state = VehicleOdometerState(
|
|
vehicle_id=vehicle_id,
|
|
last_recorded_odometer=last_recorded_odometer,
|
|
last_recorded_date=last_recorded_date,
|
|
daily_avg_distance=daily_avg,
|
|
estimated_current_odometer=estimated_odometer,
|
|
confidence_score=confidence,
|
|
manual_override_avg=None
|
|
)
|
|
db.add(odometer_state)
|
|
|
|
await db.commit()
|
|
await db.refresh(odometer_state)
|
|
return odometer_state
|
|
|
|
@staticmethod
|
|
async def get_estimated_odometer(db: AsyncSession, vehicle_id: int) -> Tuple[Optional[float], float]:
|
|
"""
|
|
Visszaadja a jármű becsült jelenlegi kilométeróra állását és a bizalom pontszámot.
|
|
|
|
Returns:
|
|
Tuple[estimated_odometer, confidence_score]
|
|
"""
|
|
stmt = select(VehicleOdometerState).where(VehicleOdometerState.vehicle_id == vehicle_id)
|
|
result = await db.execute(stmt)
|
|
odometer_state = result.scalars().first()
|
|
|
|
if not odometer_state:
|
|
# Ha nincs állapot, frissítsük
|
|
odometer_state = await OdometerService.update_vehicle_stats(db, vehicle_id)
|
|
if not odometer_state:
|
|
return None, 0.0
|
|
|
|
return odometer_state.estimated_current_odometer, odometer_state.confidence_score
|
|
|
|
@staticmethod
|
|
async def set_manual_override(db: AsyncSession, vehicle_id: int, daily_avg: Optional[float]) -> Optional[VehicleOdometerState]:
|
|
"""
|
|
Adminisztrátori manuális átlag beállítása.
|
|
|
|
Args:
|
|
daily_avg: Napi átlagos kilométer (km/nap). Ha None, törli a manuális beállítást.
|
|
"""
|
|
stmt = select(VehicleOdometerState).where(VehicleOdometerState.vehicle_id == vehicle_id)
|
|
result = await db.execute(stmt)
|
|
odometer_state = result.scalars().first()
|
|
|
|
if not odometer_state:
|
|
# Ha nincs állapot, hozzuk létre
|
|
odometer_state = VehicleOdometerState(
|
|
vehicle_id=vehicle_id,
|
|
last_recorded_odometer=0,
|
|
last_recorded_date=datetime.now(),
|
|
daily_avg_distance=0,
|
|
estimated_current_odometer=0,
|
|
confidence_score=0,
|
|
manual_override_avg=daily_avg
|
|
)
|
|
db.add(odometer_state)
|
|
else:
|
|
odometer_state.manual_override_avg = daily_avg
|
|
# Frissítsük a becslést a manuális átlaggal
|
|
if daily_avg is not None:
|
|
days_since_last = (datetime.now(odometer_state.last_recorded_date.tzinfo) - odometer_state.last_recorded_date).days
|
|
odometer_state.estimated_current_odometer = odometer_state.last_recorded_odometer + (daily_avg * max(days_since_last, 0))
|
|
odometer_state.confidence_score = 1.0
|
|
|
|
await db.commit()
|
|
await db.refresh(odometer_state)
|
|
return odometer_state |