M8-T15: add thermal meter cost engine
This commit is contained in:
+38
-14
@@ -2,6 +2,7 @@ import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from fastapi.responses import FileResponse
|
||||
@@ -38,6 +39,7 @@ from app.services.modbus_poll import poll_all_enabled_devices, BASE_POLL_TICK_SE
|
||||
from app.services.ha_discovery import publish_discovery, publish_states
|
||||
from app.services.tibber_prices import refresh_prices
|
||||
from app.services.energy_cost import compute_closed_periods
|
||||
from app.services.meter_cost import compute_closed_periods as compute_closed_meter_cost_periods
|
||||
from app.services.warmtelink_worker import warmtelink_worker_manager
|
||||
from app.services.timezone import local_tz
|
||||
from scripts.app_db_adopt import AppDatabaseAdoptionError, validate_app_runtime_db
|
||||
@@ -112,22 +114,44 @@ def _run_scheduled_energy_cost() -> None:
|
||||
does not crash the scheduler or affect the other background jobs.
|
||||
"""
|
||||
session_local = get_session_local()
|
||||
session: Session = session_local()
|
||||
try:
|
||||
compute_closed_periods(session)
|
||||
# After billing periods are computed, push fresh energy-cost state values
|
||||
# to MQTT/HA. publish_states is internally guarded by _should_publish
|
||||
# (MQTT disabled / not connected → no-op), so this never raises due to
|
||||
# unconfigured MQTT and does not block the billing job.
|
||||
|
||||
def run_scope(label: str, operation: Callable[[Session], None]) -> None:
|
||||
"""Run one best-effort scope in an isolated transaction/session."""
|
||||
session: Session | None = None
|
||||
try:
|
||||
from app.services.ha_discovery import publish_states
|
||||
publish_states(session)
|
||||
session = session_local()
|
||||
operation(session)
|
||||
except Exception:
|
||||
logger.exception("_run_scheduled_energy_cost: publish_states failed (non-fatal)")
|
||||
except Exception:
|
||||
logger.exception("_run_scheduled_energy_cost: unexpected error")
|
||||
finally:
|
||||
session.close()
|
||||
logger.exception("_run_scheduled_energy_cost: %s failed", label)
|
||||
if session is not None:
|
||||
try:
|
||||
session.rollback()
|
||||
except Exception:
|
||||
# A failed cleanup must not replace the operation/factory
|
||||
# error or prevent the following independent scope.
|
||||
logger.exception("_run_scheduled_energy_cost: %s rollback failed", label)
|
||||
finally:
|
||||
if session is not None:
|
||||
try:
|
||||
session.close()
|
||||
except Exception:
|
||||
# Sessions are intentionally isolated; close failures are
|
||||
# diagnostic only and must remain best-effort too.
|
||||
logger.exception("_run_scheduled_energy_cost: %s close failed", label)
|
||||
|
||||
# Electricity, thermal and HA publishing must not share failed transaction
|
||||
# state or accidentally commit each other's partially-flushed changes.
|
||||
run_scope("electricity computation", compute_closed_periods)
|
||||
run_scope("thermal computation", compute_closed_meter_cost_periods)
|
||||
|
||||
def publish(session: Session) -> None:
|
||||
# publish_states is internally guarded by _should_publish (MQTT
|
||||
# disabled / disconnected -> no-op), but gets a clean Session anyway.
|
||||
from app.services.ha_discovery import publish_states
|
||||
|
||||
publish_states(session)
|
||||
|
||||
run_scope("publish_states (non-fatal)", publish)
|
||||
|
||||
|
||||
def _run_scheduled_ha_state_publish() -> None:
|
||||
|
||||
Reference in New Issue
Block a user