i2c fixes + server improvements

This commit is contained in:
2026-10-05 19:28:37 +02:00
parent 7094d05030
commit 02f3df8c41
9 changed files with 254 additions and 52 deletions
+50 -20
View File
@@ -1,5 +1,6 @@
import os
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
import click
from flask import Flask, jsonify, redirect, render_template, request, url_for
@@ -10,7 +11,6 @@ from flask_login import (
login_user,
logout_user,
)
from sqlalchemy import func
from models import Sensor, SensorReadout, User, db
@@ -137,37 +137,67 @@ def create_app():
def api_sensor_readouts_hourly(sensor_id):
type_ = request.args.get("type")
range_ = request.args.get("range", "all")
# Frontend wysyła strefę czasową przeglądarki (np. Europe/Warsaw),
# żeby "today" znaczyło "od północy czasu lokalnego", a nie UTC.
tz_name = request.args.get("tz", "Europe/Warsaw") or "Europe/Warsaw"
try:
local_tz = ZoneInfo(tz_name)
except (ZoneInfoNotFoundError, ValueError):
return jsonify({"error": f"unknown timezone: {tz_name}"}), 400
# Timestamps are stored as naive UTC, so the cutoff must be naive UTC too.
now = datetime.now(timezone.utc).replace(tzinfo=None)
now_utc = datetime.now(timezone.utc)
now_local = now_utc.astimezone(local_tz)
if range_ == "today":
cutoff = now.replace(hour=0, minute=0, second=0, microsecond=0)
midnight_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
cutoff = midnight_local.astimezone(timezone.utc).replace(tzinfo=None)
elif range_ == "24h":
cutoff = (now_utc - timedelta(hours=24)).replace(tzinfo=None)
elif range_ == "week":
cutoff = now - timedelta(days=7)
cutoff = (now_utc - timedelta(days=7)).replace(tzinfo=None)
elif range_ == "month":
cutoff = now - timedelta(days=30)
cutoff = (now_utc - timedelta(days=30)).replace(tzinfo=None)
elif range_ == "all":
cutoff = None
else:
return jsonify({"error": "range must be one of: today, week, month, all"}), 400
query = (
db.session.query(
func.strftime("%Y-%m-%dT%H:00:00", SensorReadout.timestamp).label("hour"),
func.avg(SensorReadout.value).label("avg_value"),
)
.filter(SensorReadout.sensor_id == sensor_id)
return jsonify({"error": "range must be one of: today, 24h, week, month, all"}), 400
query = db.session.query(SensorReadout.timestamp, SensorReadout.value).filter(
SensorReadout.sensor_id == sensor_id
)
if cutoff is not None:
query = query.filter(SensorReadout.timestamp >= cutoff)
if type_:
query = query.filter(SensorReadout.type == type_)
rows = (
query.group_by("hour")
.order_by("hour")
.all()
)
return jsonify(
[{"timestamp": r.hour, "value": round(r.avg_value, 2)} for r in rows]
)
rows = query.order_by(SensorReadout.timestamp).all()
# Zakresy today i 24h: wszystkie pomiary jako punkty, bez uśredniania.
if range_ in ("today", "24h"):
return jsonify(
[
{
"timestamp": ts.strftime("%Y-%m-%dT%H:%M:%S"),
"value": value,
}
for ts, value in rows
]
)
# Pozostałe zakresy: średnia godzinowa liczona po pełnej godzinie
# lokalnej (poprawne też przy zmianie DST), znacznik to początek
# godziny w UTC (frontend dokleja "Z").
buckets: dict[datetime, list[float]] = {}
for ts, value in rows:
ts_utc = ts.replace(tzinfo=timezone.utc)
hour_local = ts_utc.astimezone(local_tz).replace(
minute=0, second=0, microsecond=0
)
hour_utc = hour_local.astimezone(timezone.utc).replace(tzinfo=None)
buckets.setdefault(hour_utc, []).append(value)
result = [
{
"timestamp": hour.strftime("%Y-%m-%dT%H:00:00"),
"value": round(sum(values) / len(values), 2),
}
for hour, values in sorted(buckets.items())
]
return jsonify(result)
# --- API: device writes (intentionally public — called by devices/sensors) ---