M8-R03: hand off channel binding during meter swap

This commit is contained in:
2026-08-24 06:45:31 +02:00
parent 231c340ea6
commit 631b14e2ec
6 changed files with 669 additions and 24 deletions
+34 -15
View File
@@ -61,7 +61,12 @@ from app.schemas.meter import (
MeterPatchRequest,
MeterResponse,
)
from app.services.meter_sources import ChannelNotFoundError, MeterSourceError, create_binding
from app.services.meter_sources import (
ChannelNotFoundError,
MeterSourceError,
create_binding,
create_binding_for_meter_swap,
)
from app.services import timezone as _tz_mod
from app.services.auth import AuthenticatedSession
from app.services.energy_cost import recompute_range
@@ -231,6 +236,9 @@ def declare_energy_meter(
started_at_utc = _localize_started_at(body.started_at)
try:
old_meter = db.execute(
select(Meter).where(Meter.commodity == body.commodity, Meter.ended_at.is_(None))
).scalar_one_or_none()
new_meter = declare_meter(
db,
label=body.label,
@@ -246,28 +254,39 @@ def declare_energy_meter(
).scalar_one_or_none()
if channel is None:
raise ChannelNotFoundError("Meter source channel was not found.")
create_binding(
db,
meter_id=new_meter.id,
channel_id=channel.id,
started_at=started_at_utc,
)
if body.reason.value == "meter_swap":
create_binding_for_meter_swap(
db,
old_meter_id=old_meter.id if old_meter is not None else None,
new_meter_id=new_meter.id,
channel_id=channel.id,
started_at=started_at_utc,
)
else:
create_binding(
db,
meter_id=new_meter.id,
channel_id=channel.id,
started_at=started_at_utc,
)
# Keep recompute in this transaction: a failure must not leave a new
# meter, its predecessor, or either binding at a half-applied boundary.
now = datetime.now(UTC)
if started_at_utc < now:
_trigger_recompute(db, started_at_utc, "POST /api/energy/meters")
db.commit()
except (MeterOverlapError, MeterSourceError) as exc:
# declare_meter may already have closed the previous epoch. Rolling back
# here makes Meter + binding declaration genuinely atomic.
db.rollback()
raise HTTPException(
status_code=(status.HTTP_404_NOT_FOUND if isinstance(exc, ChannelNotFoundError)
else status.HTTP_422_UNPROCESSABLE_ENTITY),
detail=str(exc),
)
except Exception:
db.rollback()
raise
# Retroactive recompute: re-judge attribution from the new boundary onward.
now = datetime.now(UTC)
if started_at_utc < now:
_trigger_recompute(db, started_at_utc, "POST /api/energy/meters")
db.commit()
db.refresh(new_meter)
# Trigger HA discovery re-publish so the new active meter's energy-cost