from __future__ import annotations import logging import os import threading import time from io import BytesIO from flask import Flask, Response, abort, render_template_string, request, send_file, url_for from .collector import Collector, HueClient, Settings from .rrd_store import RRDStore LOG = logging.getLogger("hue-collector") PERIODS = ("6h", "24h", "7d", "30d") PAGE = '''
No groups collected yet.
{% endfor %}{{ item.room or item.zones or 'No room' }} · {% if item.value is none %}unavailable{% else %}{{ '%.1f'|format(item.value) }} {{ item.unit }}{% endif %}
No sensors collected yet.
{% endfor %}''' class State: def __init__(self): self.snapshot = {"groups": [], "sensors": []} self.last_success: float | None = None self.error: str | None = None self.lock = threading.Lock() def collection_loop(collector: Collector, store: RRDStore, state: State, interval: int) -> None: while True: started = time.monotonic() try: snapshot = collector.collect() store.update(snapshot) with state.lock: state.snapshot, state.last_success, state.error = snapshot, time.time(), None except Exception as exc: LOG.exception("Hue collection failed") with state.lock: state.error = str(exc) time.sleep(max(1, interval - (time.monotonic() - started))) def create_app(settings: Settings | None = None, *, start_collector: bool = True) -> Flask: settings = settings or Settings.from_env() app = Flask(__name__) state = State() store = RRDStore(settings.data_dir, settings.interval) app.extensions["hue_state"] = state app.extensions["hue_rrd_store"] = store if start_collector: collector = Collector(HueClient(settings)) threading.Thread(target=collection_loop, args=(collector, store, state, settings.interval), daemon=True).start() @app.get("/") def dashboard(): period = request.args.get("range", "24h") if period not in PERIODS: period = "24h" with state.lock: groups = sorted(state.snapshot["groups"], key=lambda item: (item["type"], item["name"].lower())) sensors = sorted(state.snapshot["sensors"], key=lambda item: (item["type"], item["name"].lower())) last_success, error = state.last_success, state.error status = time.strftime("Last update: %Y-%m-%d %H:%M:%S", time.localtime(last_success)) if last_success else "Waiting for first collection" return render_template_string(PAGE, groups=groups, sensors=sensors, status=status, error=error, periods=PERIODS, period=period) @app.get("/graph/