feat(backend): expose GET /api/v1/sites/{site_id}/current pour l'issue #29
Ajoute la dernière mesure d'un site (SiteService.current), en réutilisant
la vérification d'existence déjà en place pour GET /sites/{site_id} :
SiteService gagne une dépendance ReadingRepository, sur le modèle de
composition déjà utilisé par StatsService/SensorService. Un site connu
sans lecture rend 200 avec les champs de mesure à null et
data_quality="critical" ; seul un site_id absent rend 404.
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from app.models.energy import Site
|
||||
from app.repositories.reading import ReadingRepository
|
||||
from app.repositories.site import SiteRepository
|
||||
|
||||
DataQuality = Literal["good", "partial", "degraded", "critical"]
|
||||
|
||||
QUALITES_CONNUES: frozenset[str] = frozenset({"good", "partial", "degraded", "critical"})
|
||||
|
||||
|
||||
class SiteError(Exception):
|
||||
pass
|
||||
@@ -12,9 +20,26 @@ class SiteNotFoundError(SiteError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SiteCurrentReading:
|
||||
timestamp: datetime | None
|
||||
site_id: str
|
||||
site_type: str
|
||||
consumption_kw: float | None
|
||||
consumption_kwh: float | None
|
||||
voltage_v: float | None
|
||||
current_a: float | None
|
||||
power_factor: float | None
|
||||
temperature_celsius: float | None
|
||||
humidity_percent: float | None
|
||||
null_reasons: list[str]
|
||||
data_quality: DataQuality
|
||||
|
||||
|
||||
class SiteService:
|
||||
def __init__(self, *, sites: SiteRepository) -> None:
|
||||
def __init__(self, *, sites: SiteRepository, readings: ReadingRepository) -> None:
|
||||
self._sites = sites
|
||||
self._readings = readings
|
||||
|
||||
async def list_all(self) -> Sequence[Site]:
|
||||
return await self._sites.list_all()
|
||||
@@ -24,3 +49,41 @@ class SiteService:
|
||||
if site is None:
|
||||
raise SiteNotFoundError(site_id)
|
||||
return site
|
||||
|
||||
async def current(self, site_id: str) -> SiteCurrentReading:
|
||||
site = await self.get_by_id(site_id)
|
||||
derniere = await self._readings.latest_for_site(site_id)
|
||||
|
||||
if derniere is None:
|
||||
return SiteCurrentReading(
|
||||
timestamp=None,
|
||||
site_id=site.site_id,
|
||||
site_type=site.site_type,
|
||||
consumption_kw=None,
|
||||
consumption_kwh=None,
|
||||
voltage_v=None,
|
||||
current_a=None,
|
||||
power_factor=None,
|
||||
temperature_celsius=None,
|
||||
humidity_percent=None,
|
||||
null_reasons=[],
|
||||
data_quality="critical",
|
||||
)
|
||||
|
||||
qualite: DataQuality = (
|
||||
derniere.data_quality if derniere.data_quality in QUALITES_CONNUES else "critical"
|
||||
)
|
||||
return SiteCurrentReading(
|
||||
timestamp=derniere.timestamp,
|
||||
site_id=site.site_id,
|
||||
site_type=site.site_type,
|
||||
consumption_kw=derniere.consumption_kw,
|
||||
consumption_kwh=derniere.consumption_kwh,
|
||||
voltage_v=derniere.voltage_v,
|
||||
current_a=derniere.current_a,
|
||||
power_factor=derniere.power_factor,
|
||||
temperature_celsius=derniere.temperature_celsius,
|
||||
humidity_percent=derniere.humidity_percent,
|
||||
null_reasons=derniere.null_reasons or [],
|
||||
data_quality=qualite,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user