Ajoute le resume instantane de consommation du parc attendu par le frontend (deja developpe contre ce contrat en mode mock). Nouveaux SiteRepository et ReadingRepository (derniere lecture par site via DISTINCT ON), StatsService pour l'agregation et les cas de repli (capacite nulle, absence de lecture, data_quality inconnue), et le endpoint lecteur-seul correspondant. Documentation des routes et du schema des couches mises a jour.
108 lines
3.2 KiB
Python
108 lines
3.2 KiB
Python
from dataclasses import dataclass
|
|
|
|
from app.services.stats import StatsService
|
|
|
|
|
|
@dataclass
|
|
class FauxSite:
|
|
site_id: str
|
|
site_name: str
|
|
capacity_kw: float | None
|
|
|
|
|
|
@dataclass
|
|
class FauxLecture:
|
|
site_id: str
|
|
consumption_kw: float | None
|
|
data_quality: str | None
|
|
|
|
|
|
class FauxDepotSites:
|
|
def __init__(self, sites: list[FauxSite]) -> None:
|
|
self._sites = sites
|
|
|
|
async def list_all(self) -> list[FauxSite]:
|
|
return self._sites
|
|
|
|
|
|
class FauxDepotLectures:
|
|
def __init__(self, lectures: list[FauxLecture]) -> None:
|
|
self._lectures = lectures
|
|
|
|
async def latest_by_site(self) -> list[FauxLecture]:
|
|
return self._lectures
|
|
|
|
|
|
async def test_summary_computes_totals_and_the_average_load() -> None:
|
|
service = StatsService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A", 200), FauxSite("B", "Site B", 800)]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures( # type: ignore[arg-type]
|
|
[
|
|
FauxLecture("A", 100, "good"),
|
|
FauxLecture("B", 400, "good"),
|
|
]
|
|
),
|
|
)
|
|
|
|
resume = await service.summary()
|
|
|
|
assert resume.total_sites == 2
|
|
assert resume.total_consumption_kw == 500
|
|
assert resume.total_capacity_kw == 1000
|
|
assert resume.average_load_percent == 50
|
|
par_site = {site.site_id: site for site in resume.sites}
|
|
assert par_site["A"].load_percent == 50
|
|
assert par_site["B"].load_percent == 50
|
|
|
|
|
|
async def test_summary_treats_a_site_without_any_reading_as_critical() -> None:
|
|
service = StatsService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A", 200)]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures([]), # type: ignore[arg-type]
|
|
)
|
|
|
|
resume = await service.summary()
|
|
|
|
site = resume.sites[0]
|
|
assert site.data_quality == "critical"
|
|
assert site.current_consumption_kw is None
|
|
assert site.load_percent is None
|
|
|
|
|
|
async def test_summary_treats_a_reading_with_an_unknown_quality_as_critical() -> None:
|
|
service = StatsService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A", 200)]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures([FauxLecture("A", 50, None)]), # type: ignore[arg-type]
|
|
)
|
|
|
|
resume = await service.summary()
|
|
|
|
site = resume.sites[0]
|
|
assert site.data_quality == "critical"
|
|
assert site.current_consumption_kw is None
|
|
|
|
|
|
async def test_summary_exposes_a_missing_capacity_as_zero_without_dividing_by_it() -> None:
|
|
service = StatsService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A", None)]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures([FauxLecture("A", 50, "good")]), # type: ignore[arg-type]
|
|
)
|
|
|
|
resume = await service.summary()
|
|
|
|
site = resume.sites[0]
|
|
assert site.capacity_kw == 0
|
|
assert site.current_consumption_kw == 50
|
|
assert site.load_percent is None
|
|
|
|
|
|
async def test_summary_returns_zero_average_load_when_no_site_has_a_capacity() -> None:
|
|
service = StatsService(
|
|
sites=FauxDepotSites([FauxSite("A", "Site A", None)]), # type: ignore[arg-type]
|
|
readings=FauxDepotLectures([]), # type: ignore[arg-type]
|
|
)
|
|
|
|
resume = await service.summary()
|
|
|
|
assert resume.average_load_percent == 0
|