12 Commits
Author SHA1 Message Date
Dmitriy a000fb6c44 work : изменил расписание на реальное. 2026-09-20 13:42:34 +03:00
Dmitriy be4c2c403c work : переименовал проект 2026-09-20 12:55:06 +03:00
Fedorov Dmitriy e636bc53ca Merge branch 'release' into dev_fedorov 2026-09-20 11:29:58 +03:00
Fedorov Dmitriy fdb798a430 work : виджет стал более "горизонтальным" 2026-09-20 11:28:48 +03:00
Fedorov Dmitriy 33cdb205b3 Merge branch 'release' into dev_fedorov 2026-09-20 02:02:34 +03:00
Fedorov Dmitriy 8069ab65f2 work : добавил web в отслеживание 2026-09-20 02:01:44 +03:00
Fedorov Dmitriy 455d191ea1 Merge branch 'release' into dev_fedorov 2026-09-20 02:00:40 +03:00
Fedorov Dmitriy 0172b6bcf2 work: добавлин web интерфейс. 2026-09-20 01:59:36 +03:00
Fedorov Dmitriy 65cf547c6c Merge branch 'release' into dev_fedorov 2026-09-19 23:05:03 +03:00
Fedorov Dmitriy 741be479a6 work: исправил порядок объявления классов в app/auto/config TemperatureConfig объявлялся после AutoConfig в котором использовался. 2026-09-19 22:51:36 +03:00
Fedorov Dmitriy 958691a481 doc: поправил зависимости и расписание 2026-09-19 19:14:08 +03:00
Fedorov Dmitriy c22e8bc71d work: добавил режим ручного управления. Теперь можно отключать расписание! 2026-09-19 19:04:45 +03:00
4 changed files with 3 additions and 340 deletions
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -1 +0,0 @@
from .service import PrometheusMetricsService
-309
View File
@@ -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}")