Tri non déterministe : `latest_for_site` départage désormais les égalités de timestamp par `reading_id` décroissant, comme `list_history`. `uq_reading_source` autorise deux lignes au même `site_id`+`timestamp` quand la `source` diffère, donc le `LIMIT 1` pouvait renvoyer l'une ou l'autre d'un appel à l'autre. Tests : trois tests `integration` sur `latest_for_site` (plus récente, égalité de timestamp, isolation par site). Le test d'égalité échoue sans le correctif ci-dessus. Duplication : `DataQuality` et le repli vers `critical` sortent dans `app/services/data_quality.py`, partagé par `stats.py`, `site.py` et `sensor.py`, qui en portaient trois copies indépendantes. Supprime au passage deux `# type: ignore[assignment]`.
83 lines
2.6 KiB
Python
83 lines
2.6 KiB
Python
from collections.abc import Sequence
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
|
|
from app.models.energy import Site
|
|
from app.repositories.reading import ReadingRepository
|
|
from app.repositories.site import SiteRepository
|
|
from app.services.data_quality import DataQuality, qualite_ou_critique
|
|
|
|
|
|
class SiteError(Exception):
|
|
pass
|
|
|
|
|
|
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, readings: ReadingRepository) -> None:
|
|
self._sites = sites
|
|
self._readings = readings
|
|
|
|
async def list_all(self) -> Sequence[Site]:
|
|
return await self._sites.list_all()
|
|
|
|
async def get_by_id(self, site_id: str) -> Site:
|
|
site = await self._sites.get_by_id(site_id)
|
|
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",
|
|
)
|
|
|
|
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_ou_critique(derniere.data_quality),
|
|
)
|