M8-R03: hand off channel binding during meter swap
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user