Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a000fb6c44 | ||
|
|
be4c2c403c | ||
|
|
e636bc53ca | ||
|
|
fdb798a430 | ||
|
|
33cdb205b3 | ||
|
|
8069ab65f2 | ||
|
|
455d191ea1 | ||
|
|
0172b6bcf2 | ||
|
|
65cf547c6c | ||
|
|
741be479a6 | ||
|
|
958691a481 | ||
|
|
c22e8bc71d |
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
[version]
|
[version]
|
||||||
major = 1
|
major = 1
|
||||||
minor = 3
|
minor = 2
|
||||||
patch = 0
|
patch = 1
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
date = "2026-09-20"
|
date = "2026-09-20"
|
||||||
time = "14:17:42"
|
time = "11:28:15"
|
||||||
|
|||||||
-27
@@ -9,7 +9,6 @@ from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from pathlib import Path as FilePath
|
from pathlib import Path as FilePath
|
||||||
from app.qingping.service import QingpingService
|
from app.qingping.service import QingpingService
|
||||||
from app.metrics import PrometheusMetricsService
|
|
||||||
|
|
||||||
from app.my_dataclasses import (
|
from app.my_dataclasses import (
|
||||||
TION_MAC,
|
TION_MAC,
|
||||||
@@ -93,8 +92,6 @@ schedule_load_error: str | None = None
|
|||||||
auto_controller: AutoController | None = None
|
auto_controller: AutoController | None = None
|
||||||
auto_load_error: str | None = None
|
auto_load_error: str | None = None
|
||||||
|
|
||||||
metrics_service: PrometheusMetricsService | None = None
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# Application lifecycle
|
# Application lifecycle
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
@@ -105,7 +102,6 @@ async def lifespan(app: FastAPI):
|
|||||||
global schedule_load_error
|
global schedule_load_error
|
||||||
global auto_controller
|
global auto_controller
|
||||||
global auto_load_error
|
global auto_load_error
|
||||||
global metrics_service
|
|
||||||
|
|
||||||
await service.start()
|
await service.start()
|
||||||
await qingping_service.start()
|
await qingping_service.start()
|
||||||
@@ -171,32 +167,9 @@ async def lifespan(app: FastAPI):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
schedule_service = None
|
schedule_service = None
|
||||||
schedule_load_error = f"{type(exc).__name__}: {exc}"
|
schedule_load_error = f"{type(exc).__name__}: {exc}"
|
||||||
|
|
||||||
try:
|
|
||||||
metrics_service = PrometheusMetricsService(
|
|
||||||
tion_service=service,
|
|
||||||
qingping_service=qingping_service,
|
|
||||||
schedule_service=schedule_service,
|
|
||||||
auto_controller=auto_controller,
|
|
||||||
output_path=(
|
|
||||||
"/var/lib/prometheus/"
|
|
||||||
"node-exporter/"
|
|
||||||
"climatcontroller.prom"
|
|
||||||
),
|
|
||||||
interval=5.0,
|
|
||||||
)
|
|
||||||
await metrics_service.start()
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logging.getLogger(__name__).exception("Could not start Prometheus metrics service")
|
|
||||||
metrics_service = None
|
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if metrics_service is not None:
|
|
||||||
await metrics_service.stop()
|
|
||||||
|
|
||||||
if schedule_service is not None:
|
if schedule_service is not None:
|
||||||
await schedule_service.stop()
|
await schedule_service.stop()
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
from .service import PrometheusMetricsService
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class PrometheusMetricsService:
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
tion_service,
|
|
||||||
qingping_service,
|
|
||||||
schedule_service=None,
|
|
||||||
auto_controller=None,
|
|
||||||
output_path: str | Path = (
|
|
||||||
"/var/lib/prometheus/node-exporter/"
|
|
||||||
"climatcontroller.prom"
|
|
||||||
),
|
|
||||||
interval: float = 5.0,
|
|
||||||
):
|
|
||||||
self._tion = tion_service
|
|
||||||
self._qingping = qingping_service
|
|
||||||
self._schedule = schedule_service
|
|
||||||
self._auto = auto_controller
|
|
||||||
|
|
||||||
self._output_path = Path(output_path)
|
|
||||||
self._interval = interval
|
|
||||||
|
|
||||||
self._running = False
|
|
||||||
self._task = None
|
|
||||||
self._last_error: str | None = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def running(self) -> bool:
|
|
||||||
return self._running
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_error(self) -> str | None:
|
|
||||||
return self._last_error
|
|
||||||
|
|
||||||
async def start(self) -> None:
|
|
||||||
|
|
||||||
if self._running:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._running = True
|
|
||||||
|
|
||||||
await self._write_metrics()
|
|
||||||
|
|
||||||
self._task = asyncio.create_task(
|
|
||||||
self._loop()
|
|
||||||
)
|
|
||||||
|
|
||||||
async def stop(self) -> None:
|
|
||||||
|
|
||||||
if not self._running:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._running = False
|
|
||||||
|
|
||||||
if self._task is not None:
|
|
||||||
self._task.cancel()
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self._task
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self._task = None
|
|
||||||
|
|
||||||
async def _loop(self) -> None:
|
|
||||||
|
|
||||||
while self._running:
|
|
||||||
|
|
||||||
await asyncio.sleep(
|
|
||||||
self._interval
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await self._write_metrics()
|
|
||||||
|
|
||||||
except Exception as exc:
|
|
||||||
self._last_error = f"{type(exc).__name__}: {exc}"
|
|
||||||
logger.exception("Prometheus metrics update failed")
|
|
||||||
|
|
||||||
async def _write_metrics(self) -> None:
|
|
||||||
|
|
||||||
lines: list[str] = []
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# Время последнего успешного обновления файла.
|
|
||||||
# Позволяет определить протухшие метрики.
|
|
||||||
# --------------------------------------------------
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_metrics_timestamp_seconds",
|
|
||||||
time.time(),
|
|
||||||
"Unix timestamp of the last "
|
|
||||||
"ClimatController metrics update",
|
|
||||||
)
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# Tion
|
|
||||||
# --------------------------------------------------
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_tion_online",
|
|
||||||
self._tion.online,
|
|
||||||
"Tion connection state",
|
|
||||||
)
|
|
||||||
|
|
||||||
tion_state = self._tion.state
|
|
||||||
|
|
||||||
if tion_state is not None:
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_tion_power",
|
|
||||||
tion_state.power,
|
|
||||||
"Tion power state",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_tion_heater",
|
|
||||||
tion_state.heater,
|
|
||||||
"Tion heater enabled state",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_tion_heating",
|
|
||||||
tion_state.heating,
|
|
||||||
"Tion active heating state",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_tion_fan_speed",
|
|
||||||
tion_state.fan_speed,
|
|
||||||
"Tion fan speed",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_tion_in_temperature_celsius",
|
|
||||||
tion_state.in_temp,
|
|
||||||
"Tion inlet temperature",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_tion_out_temperature_celsius",
|
|
||||||
tion_state.out_temp,
|
|
||||||
"Tion outside temperature",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_tion_target_temperature_celsius",
|
|
||||||
tion_state.target_temp,
|
|
||||||
"Tion target temperature",
|
|
||||||
)
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# Qingping
|
|
||||||
# --------------------------------------------------
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_qingping_online",
|
|
||||||
self._qingping.online,
|
|
||||||
"Qingping online state",
|
|
||||||
)
|
|
||||||
|
|
||||||
qingping_state = self._qingping.state
|
|
||||||
|
|
||||||
if qingping_state is not None:
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_qingping_mqtt_connected",
|
|
||||||
qingping_state.mqtt_connected,
|
|
||||||
"Qingping MQTT connection state",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_qingping_temperature_celsius",
|
|
||||||
qingping_state.temperature,
|
|
||||||
"Qingping temperature",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_qingping_humidity_percent",
|
|
||||||
qingping_state.humidity,
|
|
||||||
"Qingping relative humidity",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_qingping_co2_ppm",
|
|
||||||
qingping_state.co2,
|
|
||||||
"Qingping CO2 concentration",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_qingping_pm25_ug_m3",
|
|
||||||
qingping_state.pm25,
|
|
||||||
"Qingping PM2.5 concentration",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_qingping_pm10_ug_m3",
|
|
||||||
qingping_state.pm10,
|
|
||||||
"Qingping PM10 concentration",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_qingping_battery_percent",
|
|
||||||
qingping_state.battery,
|
|
||||||
"Qingping battery level",
|
|
||||||
)
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# Schedule / AUTO
|
|
||||||
# --------------------------------------------------
|
|
||||||
|
|
||||||
if self._schedule is not None:
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_schedule_paused",
|
|
||||||
self._schedule.paused,
|
|
||||||
"Schedule manual pause state",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
resolution = (
|
|
||||||
self._schedule.resolve()
|
|
||||||
)
|
|
||||||
|
|
||||||
self._add_gauge(
|
|
||||||
lines,
|
|
||||||
"climat_auto_active",
|
|
||||||
resolution.auto_active,
|
|
||||||
"AUTO schedule state",
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception(
|
|
||||||
"Schedule metrics resolve failed"
|
|
||||||
)
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# Atomic write
|
|
||||||
# --------------------------------------------------
|
|
||||||
|
|
||||||
self._output_path.parent.mkdir(
|
|
||||||
parents=True,
|
|
||||||
exist_ok=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
temp_path = (
|
|
||||||
self._output_path.with_suffix(
|
|
||||||
self._output_path.suffix
|
|
||||||
+ ".tmp"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
temp_path.write_text(
|
|
||||||
"\n".join(lines) + "\n",
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
|
|
||||||
temp_path.replace(
|
|
||||||
self._output_path
|
|
||||||
)
|
|
||||||
|
|
||||||
self._last_error = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _add_gauge(
|
|
||||||
lines: list[str],
|
|
||||||
name: str,
|
|
||||||
value,
|
|
||||||
help_text: str,
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
if value is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(value, bool):
|
|
||||||
value = 1 if value else 0
|
|
||||||
|
|
||||||
lines.append(f"# HELP {name} {help_text}")
|
|
||||||
|
|
||||||
lines.append(f"# TYPE {name} gauge")
|
|
||||||
|
|
||||||
lines.append(f"{name} {value}")
|
|
||||||
Reference in New Issue
Block a user