f1e311a0b1
- Added GET /main/verify?count=N to spot-check auto estimations against temp history — recomputes expected gallons from HDD since last fill and reports drift per customer; warnings flagged when drift > 15 gal - estimated_gallons_left and estimated_gallons_left_prev_day altered from INTEGER to NUMERIC(6,2) in both dev and prod DBs to eliminate ~0.3 gal/day rounding drift that accumulated from integer truncation on each daily update Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
174 lines
6.1 KiB
Python
174 lines
6.1 KiB
Python
import logging
|
|
from decimal import Decimal
|
|
from datetime import date, timedelta
|
|
from typing import Optional
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from database import session
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
from app.script.fuel_estimator import FuelEstimator
|
|
from app.script.temp_getter import fetch_and_store_daily_temp
|
|
from app.script.fuel_estimator_customer import FuelEstimatorCustomer
|
|
from app.models.auto import Auto_Delivery, Auto_Temp
|
|
from app.constants import TANK_MAX_FILLS, DEFAULT_MAX_FILL_GALLONS
|
|
|
|
router = APIRouter(
|
|
prefix="/main",
|
|
tags=["main"],
|
|
responses={404: {"description": "Not found"}},
|
|
)
|
|
|
|
|
|
@router.get("/temp", status_code=200)
|
|
def update_temp_manually():
|
|
"""
|
|
Manually triggers the fetch and storage of today's temperature.
|
|
This is useful for testing or for manual intervention if the cron job fails.
|
|
"""
|
|
try:
|
|
success = fetch_and_store_daily_temp()
|
|
if success:
|
|
session.commit()
|
|
return {"ok": True, "message": "Temperature updated or already exists."}
|
|
else:
|
|
# The function already rolled back, so just return an error
|
|
return HTTPException(status_code=500, detail="Failed to fetch temperature from the weather API.")
|
|
except Exception as e:
|
|
session.rollback()
|
|
raise HTTPException(status_code=500, detail=f"An unexpected server error occurred: {str(e)}")
|
|
|
|
|
|
|
|
@router.get("/update/auto", status_code=200)
|
|
def update_all_customer_fuel_levels_auto():
|
|
"""
|
|
This endpoint triggers the daily update for all customers.
|
|
It should be called once per day by a cron job or scheduler.
|
|
"""
|
|
try:
|
|
estimator = FuelEstimator(session)
|
|
result = estimator.run_daily_update()
|
|
session.commit()
|
|
return result
|
|
except Exception as e:
|
|
session.rollback()
|
|
# Log the exception e
|
|
logger.error(str(e))
|
|
return {"ok": False, "message": "An internal error occurred."}
|
|
|
|
|
|
@router.get("/update/normal", status_code=200)
|
|
def update_all_customer_fuel_levels_normal():
|
|
"""
|
|
This endpoint triggers the daily update for all customers.
|
|
It should be called once per day by a cron job or scheduler.
|
|
"""
|
|
try:
|
|
estimator = FuelEstimatorCustomer(session)
|
|
result = estimator.run_daily_update()
|
|
session.commit()
|
|
return result
|
|
except Exception as e:
|
|
session.rollback()
|
|
# Log the exception e
|
|
logger.error(str(e))
|
|
return {"ok": False, "message": "An internal error occurred."}
|
|
|
|
|
|
@router.get("/verify", status_code=200)
|
|
def verify_auto_estimations(count: int = Query(default=10, ge=1, le=50)):
|
|
"""
|
|
Spot-check auto delivery estimations against temperature history.
|
|
|
|
For each sampled customer, recomputes expected gallons from the last fill
|
|
date using daily HDD records, then compares to the stored estimate.
|
|
Drift > 15 gal is flagged as a warning.
|
|
"""
|
|
try:
|
|
today = date.today()
|
|
|
|
import random
|
|
customers = (
|
|
session.query(Auto_Delivery)
|
|
.filter(Auto_Delivery.auto_status == 1, Auto_Delivery.last_fill.isnot(None))
|
|
.all()
|
|
)
|
|
sample = random.sample(customers, min(count, len(customers)))
|
|
|
|
# Build a date→degree_day lookup using raw temp_avg (matches estimator logic)
|
|
temps = session.query(Auto_Temp).all()
|
|
hdd_by_date: dict[date, Decimal] = {
|
|
t.todays_date: Decimal(str(max(0, 65 - float(t.temp_avg))))
|
|
for t in temps
|
|
if t.temp_avg is not None
|
|
}
|
|
|
|
HOT_WATER_DAILY = Decimal("1.0")
|
|
|
|
results = []
|
|
warnings = 0
|
|
|
|
for c in sample:
|
|
tank_size = float(Decimal(c.tank_size)) if c.tank_size else 275.0
|
|
max_fill = Decimal(str(TANK_MAX_FILLS.get(tank_size, DEFAULT_MAX_FILL_GALLONS)))
|
|
k = Decimal(str(c.house_factor)) if c.house_factor else Decimal("0.12")
|
|
hot_water = c.hot_water_summer == 1
|
|
|
|
# Accumulate usage from day after last fill through today
|
|
total_hdd = Decimal("0")
|
|
total_days = 0
|
|
cursor = c.last_fill + timedelta(days=1)
|
|
missing_temps = []
|
|
while cursor <= today:
|
|
dd = hdd_by_date.get(cursor)
|
|
if dd is None:
|
|
missing_temps.append(str(cursor))
|
|
else:
|
|
total_hdd += dd
|
|
total_days += 1
|
|
cursor += timedelta(days=1)
|
|
|
|
heating_used = k * total_hdd
|
|
hot_water_used = HOT_WATER_DAILY * total_days if hot_water else Decimal("0")
|
|
expected = max(Decimal("0"), max_fill - heating_used - hot_water_used)
|
|
actual = Decimal(str(c.estimated_gallons_left)) if c.estimated_gallons_left is not None else Decimal("0")
|
|
drift = float(actual - expected)
|
|
is_warning = abs(drift) > 15
|
|
|
|
if is_warning:
|
|
warnings += 1
|
|
|
|
results.append({
|
|
"customer_id": c.customer_id,
|
|
"name": c.customer_full_name,
|
|
"town": c.customer_town,
|
|
"tank_size": c.tank_size,
|
|
"k_factor": float(k),
|
|
"k_factor_source": c.k_factor_source,
|
|
"hot_water": hot_water,
|
|
"last_fill": str(c.last_fill),
|
|
"days_since_fill": total_days,
|
|
"total_hdd": float(total_hdd),
|
|
"max_fill": float(max_fill),
|
|
"expected_gallons": round(float(expected), 1),
|
|
"actual_gallons": float(actual),
|
|
"drift_gallons": round(drift, 1),
|
|
"status": "WARNING" if is_warning else "OK",
|
|
"missing_temp_dates": missing_temps,
|
|
})
|
|
|
|
results.sort(key=lambda r: abs(r["drift_gallons"]), reverse=True)
|
|
|
|
return {
|
|
"ok": True,
|
|
"checked": len(results),
|
|
"warnings": warnings,
|
|
"summary": "All OK" if warnings == 0 else f"{warnings} customer(s) have drift > 15 gal",
|
|
"customers": results,
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"verify endpoint error: {e}")
|
|
raise HTTPException(status_code=500, detail=str(e))
|