mirror of
https://github.com/CCOSTAN/Home-AssistantConfig.git
synced 2026-04-25 17:53:00 +00:00
- Updated proxmox.yaml to include state_class for disk usage sensors and improved availability checks. - Modified space.yaml to add state_class for the Earth distance sensor. - Enhanced stats.yaml with state_class for various command line sensors and template sensors to support long-term trend rollups. - Updated recorder.yaml to refine notes and exclude additional MariaDB snapshot sensors from recording. - Revised README.md in scripts to correct package paths and add a new monthly log hygiene review automation. - Introduced infrastructure.yaml for comprehensive observability and monitoring of WAN, DNS, and website states, including automated repairs for uptime breaches. - Added mariadb_snapshot.py script to collect telemetry snapshots for MariaDB, supporting Home Assistant command line sensors.
157 lines
4.8 KiB
Python
157 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Collect MariaDB telemetry snapshots for Home Assistant command_line sensors."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from sqlalchemy import create_engine, text
|
|
|
|
SECRETS_PATH = Path("/config/secrets.yaml")
|
|
RECORDER_DB_URL_KEY = "recorder_db_url"
|
|
|
|
QUERIES = {
|
|
"live": """
|
|
SELECT
|
|
'running' AS status,
|
|
ROUND(
|
|
MAX(
|
|
CASE
|
|
WHEN VARIABLE_NAME = 'Queries' THEN CAST(VARIABLE_VALUE AS DECIMAL(20, 0))
|
|
END
|
|
) /
|
|
NULLIF(
|
|
MAX(
|
|
CASE
|
|
WHEN VARIABLE_NAME = 'Uptime' THEN CAST(VARIABLE_VALUE AS DECIMAL(20, 0))
|
|
END
|
|
),
|
|
0
|
|
),
|
|
0
|
|
) AS performance,
|
|
MAX(
|
|
CASE
|
|
WHEN VARIABLE_NAME = 'Threads_connected' THEN CAST(VARIABLE_VALUE AS UNSIGNED)
|
|
END
|
|
) AS connections,
|
|
MAX(
|
|
CASE
|
|
WHEN VARIABLE_NAME = 'Questions' THEN CAST(VARIABLE_VALUE AS UNSIGNED)
|
|
END
|
|
) AS questions,
|
|
MAX(
|
|
CASE
|
|
WHEN VARIABLE_NAME = 'Uptime' THEN CAST(VARIABLE_VALUE AS UNSIGNED)
|
|
END
|
|
) AS uptime_seconds
|
|
FROM information_schema.GLOBAL_STATUS
|
|
WHERE VARIABLE_NAME IN ('Queries', 'Questions', 'Threads_connected', 'Uptime');
|
|
""",
|
|
"recorder": """
|
|
WITH state_stats AS (
|
|
SELECT
|
|
MIN(last_updated_ts) AS min_last_updated_ts,
|
|
COUNT(*) AS total_records
|
|
FROM states
|
|
)
|
|
SELECT
|
|
ROUND(SUM(t.data_length + t.index_length) / 1024 / 1024, 2) AS database_size_mib,
|
|
COUNT(*) AS database_tables_count,
|
|
DATE_FORMAT(
|
|
FROM_UNIXTIME(ss.min_last_updated_ts),
|
|
'%Y-%m-%d'
|
|
) AS database_oldest_record,
|
|
ss.total_records AS database_total_records,
|
|
ROUND(
|
|
ss.total_records /
|
|
GREATEST(DATEDIFF(NOW(), FROM_UNIXTIME(ss.min_last_updated_ts)), 1),
|
|
0
|
|
) AS database_records_per_day
|
|
FROM information_schema.tables t
|
|
CROSS JOIN state_stats ss
|
|
WHERE t.table_schema = 'homeassistant';
|
|
""",
|
|
"admin": """
|
|
SELECT
|
|
@@version AS version,
|
|
ROUND(@@innodb_buffer_pool_size / 1024 / 1024 / 1024, 1) AS buffer_pool_gib,
|
|
@@max_connections AS max_connections,
|
|
ROUND(@@innodb_log_file_size / 1024 / 1024, 0) AS log_file_size_mib,
|
|
ROUND(@@tmp_table_size / 1024 / 1024, 0) AS tmp_table_size_mib,
|
|
@@innodb_io_capacity AS io_capacity,
|
|
@@innodb_read_io_threads AS io_threads_read,
|
|
@@innodb_write_io_threads AS io_threads_write,
|
|
@@table_open_cache AS table_cache,
|
|
ROUND(@@sort_buffer_size / 1024 / 1024, 0) AS sort_buffer_mib,
|
|
ROUND(@@read_buffer_size / 1024 / 1024, 0) AS read_buffer_mib,
|
|
ROUND(@@join_buffer_size / 1024 / 1024, 0) AS join_buffer_mib;
|
|
""",
|
|
}
|
|
|
|
|
|
def _load_db_url() -> str:
|
|
"""Read recorder_db_url from Home Assistant secrets.yaml."""
|
|
secrets_text = SECRETS_PATH.read_text(encoding="utf-8")
|
|
match = re.search(
|
|
rf"^{re.escape(RECORDER_DB_URL_KEY)}:\s*[\"']?(.*?)[\"']?\s*$",
|
|
secrets_text,
|
|
re.MULTILINE,
|
|
)
|
|
if match is None:
|
|
raise RuntimeError(f"Missing {RECORDER_DB_URL_KEY} in {SECRETS_PATH}")
|
|
return match.group(1)
|
|
|
|
|
|
def _json_safe(value: Any) -> Any:
|
|
"""Convert SQLAlchemy result values into JSON-serializable values."""
|
|
if isinstance(value, Decimal):
|
|
return float(value)
|
|
return value
|
|
|
|
|
|
def main() -> int:
|
|
"""Run the requested query mode and emit a compact JSON payload."""
|
|
mode = sys.argv[1].strip().lower() if len(sys.argv) > 1 else ""
|
|
|
|
if len(sys.argv) != 2 or mode not in QUERIES:
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"error": "usage",
|
|
"message": "expected one mode: admin, live, recorder",
|
|
},
|
|
separators=(",", ":"),
|
|
),
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
|
|
engine = create_engine(_load_db_url(), pool_pre_ping=True)
|
|
|
|
try:
|
|
with engine.connect() as connection:
|
|
row = connection.execute(text(QUERIES[mode])).mappings().one()
|
|
except Exception as err: # pragma: no cover - runtime safety path
|
|
print(
|
|
json.dumps(
|
|
{"error": "query_failed", "message": str(err)},
|
|
separators=(",", ":"),
|
|
),
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
payload = {key: _json_safe(value) for key, value in row.items()}
|
|
print(json.dumps(payload, separators=(",", ":")))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|