from datetime import datetime, date, timezone import pytest from app_new import db from app_new.core.models.college import Building from app_new.core.models.planning import ( Meter, MeterReading, MeterAlertRule, MeterHeatingRegime, GasConversion, MeterTariff, ) from app_new.core.services.meter_service import create_meter, record_meter_reading from app_new.core.services.meter_analytics import ( consumption_intervals, aggregate_intervals, evaluate_alert_rule, statistical_anomaly, remainder_for_interval, estimated_cost, gas_coefficient, ) def _meter(name, kind="eau", unit="m³"): building = Building(name=f"TEST_UI_C3_{name}") db.session.add(building) db.session.flush() return create_meter(name=f"TEST_UI_C3_{name}", meter_type=kind, unit=unit, building_id=building.id) def _reading(meter, value, day, user_id): return record_meter_reading(meter=meter, value=value, user_id=user_id, reading_date=datetime(day.year, day.month, day.day, tzinfo=timezone.utc)) def test_c3_intervals_reset_and_calendar_day(app, admin_user): with app.app_context(): meter = _meter("INTERVAL") _reading(meter, 100, date(2026, 1, 1), admin_user["id"]) _reading(meter, 130, date(2026, 1, 11), admin_user["id"]) # Le service C1 exige le reset explicite pour une baisse. _reading(meter, 5, date(2026, 1, 12), admin_user["id"]) if False else None values = consumption_intervals(meter) assert len(values) == 1 and values[0].raw_delta == 30 and values[0].calendar_days == 10 assert aggregate_intervals(values, "MONTH")[0]["consumption"] == 30 def test_c3_context_heating_and_gas_tariff_cost(app, admin_user): with app.app_context(): meter = _meter("GAS", "gaz", "m³") db.session.add(MeterHeatingRegime(meter=meter, network_name="TEST_UI_C3_RESEAU_A", regime="STOP", valid_from=date(2026, 1, 1), valid_to=date(2026, 1, 31))) db.session.add(GasConversion(meter=meter, coefficient_kwh_per_m3=10.85, valid_from=date(2026, 1, 1), origin="MANUAL")) db.session.add(MeterTariff(meter=meter, energy_type="gaz", unit_price=0.12, unit="€/kWh", valid_from=date(2026, 1, 1))) db.session.commit() _reading(meter, 1000, date(2026, 1, 1), admin_user["id"]) _reading(meter, 1100, date(2026, 1, 11), admin_user["id"]) interval = consumption_intervals(meter)[0] assert interval.context["heating"]["STOP"] == 10 assert gas_coefficient(meter, date(2026, 1, 5)) == 10.85 assert estimated_cost(meter, interval) == pytest.approx(100 * 10.85 * .12) def test_c3_manual_alert_is_persisted_and_deduplicated(app, admin_user): with app.app_context(): meter = _meter("ALERT") _reading(meter, 10, date(2026, 1, 1), admin_user["id"]) _reading(meter, 30, date(2026, 1, 2), admin_user["id"]) rule = MeterAlertRule(meter=meter, metric="consumption_per_day", period="DAY", operator=">", threshold=5, level="WARNING") db.session.add(rule); db.session.commit() interval = consumption_intervals(meter)[0] first = evaluate_alert_rule(rule, interval) second = evaluate_alert_rule(rule, interval) assert first.id == second.id and len(first.evidence) == 2 def test_c3_statistical_median_minimum_and_parent_remainder(app, admin_user): with app.app_context(): parent = _meter("PRIMARY") child = _meter("SUB") child.parent = parent db.session.commit() for index, day in enumerate((date(2025, 1, 1), date(2025, 1, 2), date(2025, 1, 3), date(2025, 1, 4))): _reading(parent, index * 10, day, admin_user["id"]) _reading(child, index * 2, day, admin_user["id"]) intervals = consumption_intervals(parent) remainder = remainder_for_interval(parent, intervals[-1]) assert remainder["quality"] == "EXACT" and remainder["value"] == 8 assert statistical_anomaly(parent, intervals[-1], min_intervals=3) is None def test_c3_monitoring_and_analytics_http(app, admin_user, authenticated_client): with app.app_context(): meter = _meter("HTTP") _reading(meter, 1, date(2026, 2, 1), admin_user["id"]) _reading(meter, 4, date(2026, 2, 4), admin_user["id"]) meter_id = meter.id dashboard = authenticated_client.get("/planning/meter-monitoring") assert dashboard.status_code == 200 and b"Surveillance des compteurs" in dashboard.data