M6-T05: add Tibber GraphQL client + price refresh service + schedule
- app/integrations/tibber/client.py: fetch_price_range/fetch_current_price (priceInfoRange QUARTER_HOURLY, startsAt->UTC, no fixed node count assumption), TibberError/TibberAuthError; token never logged or put in exception text. - app/services/tibber_prices.py: refresh_prices upserts by starts_at (idempotent), no-op unless an active kind=tibber contract + token exist. - main.py: hourly tibber-refresh job (existing jobs/MQTT untouched). Tests added.
This commit is contained in:
+39
@@ -31,6 +31,7 @@ from app.services.config_page import build_runtime_settings, seed_missing_config
|
||||
from app.services.public_ip import check_public_ipv4_and_notify
|
||||
from app.services.modbus_poll import poll_all_enabled_devices, BASE_POLL_TICK_SECONDS
|
||||
from app.services.ha_discovery import publish_discovery, publish_states
|
||||
from app.services.tibber_prices import refresh_prices
|
||||
from scripts.app_db_adopt import AppDatabaseAdoptionError, validate_app_runtime_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -63,6 +64,33 @@ def _run_scheduled_modbus_poll() -> None:
|
||||
session.close()
|
||||
|
||||
|
||||
def _run_scheduled_tibber_refresh() -> None:
|
||||
"""Scheduled job: fetch Tibber 15-minute prices and upsert into tibber_price.
|
||||
|
||||
Runs every hour so that:
|
||||
- Today's prices are available from startup.
|
||||
- Tomorrow's prices (published by Tibber around 13:00 CET / 11:00 UTC) are
|
||||
picked up within an hour of publication without requiring a server restart.
|
||||
|
||||
The job is a no-op when:
|
||||
- No active energy contract with kind="tibber" exists.
|
||||
- The Tibber API token is empty in the runtime settings.
|
||||
|
||||
Any client exceptions (auth failures, network errors) are caught and logged
|
||||
so that a single failed fetch does not crash the scheduler or affect the
|
||||
other background jobs.
|
||||
"""
|
||||
session_local = get_session_local()
|
||||
session: Session = session_local()
|
||||
try:
|
||||
runtime_settings = build_runtime_settings(session, get_settings())
|
||||
refresh_prices(session, runtime_settings)
|
||||
except Exception:
|
||||
logger.exception("_run_scheduled_tibber_refresh: unexpected error")
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def _run_scheduled_ha_state_publish() -> None:
|
||||
"""Periodic job: publish discovery configs + state + availability for all enabled exposed entities.
|
||||
|
||||
@@ -148,6 +176,17 @@ async def lifespan(_: FastAPI):
|
||||
max_instances=1,
|
||||
coalesce=True,
|
||||
)
|
||||
# Tibber price refresh: fetch today + tomorrow every hour.
|
||||
# The job is a no-op when no active tibber contract or token is configured,
|
||||
# so it is safe to register unconditionally.
|
||||
scheduler.add_job(
|
||||
_run_scheduled_tibber_refresh,
|
||||
trigger=IntervalTrigger(hours=1),
|
||||
id="tibber-refresh",
|
||||
replace_existing=True,
|
||||
max_instances=1,
|
||||
coalesce=True,
|
||||
)
|
||||
scheduler.start()
|
||||
|
||||
# MQTT: connect using DB-merged runtime settings so broker configured via UI
|
||||
|
||||
Reference in New Issue
Block a user