Zont exporter init

This commit is contained in:
energys
2026-09-24 14:16:05 +09:00
parent 641d27cc65
commit 7c0a719167
4 changed files with 290 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
name: Build and Push
on:
push:
#branches:
# - main
# - 'feature/**'
jobs:
maintence:
runs-on: srvdnd
outputs:
date: ${{ steps.date.outputs.date }}
steps:
- name: Get current date
id: date
run: echo "date=$(TZ=":Asia/Yakutsk" date +'%FT%H-%M-%SR')" >> $GITHUB_OUTPUT
buildApp:
needs: [maintence]
runs-on: srvdnd
env:
TAG_NAME: ${{ needs.maintence.outputs.date }}${{ gitea.RUN_ID }}
steps:
- name: Check out repository code
uses: https://git.tlgrf.ru/actions/checkout@v4
- name: Create docker image!
run: docker build -t git.tlgrf.local/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:$TAG_NAME -t git.tlgrf.ru/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:$TAG_NAME -t git.tlgrf.ru/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:latest ./zont-exporter -f ./zont-exporter/Dockerfile
pushApp:
needs: [buildApp, maintence]
runs-on: srvdnd
env:
TAG_NAME: ${{ needs.maintence.outputs.date }}${{ gitea.RUN_ID }}
steps:
- name: Login to git.tlgrf.ru
uses: https://git.tlgrf.ru/actions/docker-login-action@v3
with:
registry: git.tlgrf.ru
username: gitea
password: ${{ secrets.GITEAPASSWORD }}
- name: Push docker image!
run: docker push git.tlgrf.ru/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:$TAG_NAME && docker push git.tlgrf.ru/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:latest
- name: Push notification
uses: https://git.tlgrf.ru/actions/rocketchat-notification-action@v3.0.0
with:
server: https://rc.tlgrf.ru
auth-token: ${{ secrets.ROCKETCHAT_AUTH_TOKEN }}
user-id: ${{ secrets.ROCKETCHAT_USER_ID }}
channel: python_rocketchat_api
message: |
Gitea | TLGRF
Repository: ${{ gitea.REPOSITORY }}
*${{ gitea.ACTOR }}* pushed to *${{ gitea.REF_NAME }}*
Commit message: ${{ gitea.event.commits[0].message }}
See changes: https://git.tlgrf.ru/${{ gitea.REPOSITORY }}/commit/${{gitea.SHA}}
```
docker pull git.tlgrf.ru/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:${{ env.TAG_NAME }}
```
```
${{ env.TAG_NAME }}
```
#- name: send telegram message on push
# uses: appleboy/telegram-action@master
# with:
# to: -4251930066
# token: ${{ secrets.TELEGRAM_TOKEN_MIKUPTIME }}
# format: markdown
# message: |
# Gitea | TLGRF
#
# Repository: ${{ gitea.REPOSITORY }}
# *${{ gitea.ACTOR }}* pushed to *${{ gitea.REF_NAME }}*
#
# Commit message: ${{ gitea.event.commits[0].message }}
# See changes: https://git.tlgrf.ru/${{ gitea.REPOSITORY }}/commit/${{gitea.SHA}}
# ```
# docker pull git.tlgrf.ru/${{ gitea.REPOSITORY }}/${{ gitea.REF_NAME }}:${{ env.TAG_NAME }}
# ```
# ```
# ${{ env.TAG_NAME }}
# ```
+29
View File
@@ -0,0 +1,29 @@
FROM python:3.12-slim
LABEL maintainer="zont-exporter"
# Отключаем создание .pyc файлов и включаем небуферизованный вывод
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# Устанавливаем зависимости
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копируем исходный код
COPY zont_exporter.py .
# Создаём непривилегированного пользователя
RUN useradd -m -u 1000 exporter && chown -R exporter:exporter /app
USER exporter
# Открываем порт экспортера
EXPOSE 8000
# Healthcheck — проверяем, что /metrics отвечает
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/metrics').read()" || exit 1
CMD ["python", "zont_exporter.py"]
+2
View File
@@ -0,0 +1,2 @@
requests>=2.31.0
prometheus-client>=0.19.0
+173
View File
@@ -0,0 +1,173 @@
import os
import time
import logging
import requests
from prometheus_client import start_http_server, Gauge
from prometheus_client.core import GaugeMetricFamily, REGISTRY
# --- Настройка логирования ---
logging.basicConfig(
level=os.getenv('LOG_LEVEL', 'INFO').upper(),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('zont-exporter')
# --- Конфигурация из переменных окружения ---
ZONT_LOGIN = os.getenv('ZONT_LOGIN')
ZONT_PASSWORD = os.getenv('ZONT_PASSWORD')
ZONT_CLIENT_EMAIL = os.getenv('ZONT_CLIENT_EMAIL', 'exporter@localhost')
API_URL = os.getenv('ZONT_API_URL', 'https://my.zont.online/api')
SCRAPE_INTERVAL = int(os.getenv('SCRAPE_INTERVAL', '60'))
EXPORTER_PORT = int(os.getenv('EXPORTER_PORT', '8000'))
# Проверка обязательных параметров
if not ZONT_LOGIN or not ZONT_PASSWORD:
logger.error("Не заданы ZONT_LOGIN и/или ZONT_PASSWORD")
raise SystemExit(1)
class ZontCollector(object):
"""Кастомный коллектор, который собирает метрики с API ZONT."""
def __init__(self):
self.token = None
def _get_auth_token(self):
"""Получает или обновляет токен авторизации."""
try:
response = requests.post(
f'{API_URL}/get_authtoken',
auth=(ZONT_LOGIN, ZONT_PASSWORD),
headers={'X-ZONT-Client': ZONT_CLIENT_EMAIL},
json={"client_name": "Prometheus-Exporter"},
timeout=15
)
response.raise_for_status()
data = response.json()
if data.get('ok'):
self.token = data['token']
logger.info("Токен успешно получен.")
return True
else:
logger.error(f"Ошибка получения токена: {data.get('error_ui')}")
except requests.exceptions.RequestException as e:
logger.error(f"Сетевая ошибка при получении токена: {e}")
except Exception as e:
logger.exception(f"Неожиданная ошибка при получении токена: {e}")
return False
def _fetch_data(self):
"""Запрашивает данные по всем устройствам."""
if not self.token:
if not self._get_auth_token():
return None
try:
response = requests.post(
f'{API_URL}/devices',
headers={
'X-ZONT-Client': ZONT_CLIENT_EMAIL,
'X-ZONT-Token': self.token
},
json={'load_io': True},
timeout=15
)
if response.status_code == 403:
logger.warning("Токен недействителен, обновляем...")
if self._get_auth_token():
return self._fetch_data()
return None
response.raise_for_status()
data = response.json()
if data.get('ok'):
return data.get('devices', [])
else:
logger.error(f"Ошибка в ответе devices: {data.get('error_ui')}")
except requests.exceptions.RequestException as e:
logger.error(f"Сетевая ошибка при запросе devices: {e}")
except Exception as e:
logger.exception(f"Неожиданная ошибка при запросе devices: {e}")
return None
def collect(self):
"""Основной метод, который вызывает Prometheus."""
devices = self._fetch_data()
if not devices:
logger.warning("Не удалось получить данные, пропускаем цикл.")
return
# --- Метрики температур ---
temp_gauge = GaugeMetricFamily(
'zont_temperature_celsius',
'Температура с датчиков ZONT',
labels=['device_id', 'device_name', 'sensor_name']
)
# --- Метрики состояния котла ---
boiler_work_gauge = GaugeMetricFamily(
'zont_boiler_working',
'Работает ли котел (1 - да, 0 - нет)',
labels=['device_id', 'device_name']
)
# --- Метрики уровня сигнала ---
signal_gauge = GaugeMetricFamily(
'zont_signal_level',
'Уровень сигнала (GSM или Wi-Fi)',
labels=['device_id', 'device_name', 'type']
)
# --- Метрики состояния охраны ---
guard_gauge = GaugeMetricFamily(
'zont_guard_state',
'Состояние охраны (1 - включена, 0 - выключена)',
labels=['device_id', 'device_name']
)
for device in devices:
dev_id = str(device.get('id'))
dev_name = device.get('name', 'unknown')
io_data = device.get('io', {})
if not io_data:
continue
# --- Обработка термодатчиков ---
for sensor in device.get('thermometers', []):
if sensor.get('is_assigned_to_slot') and sensor.get('last_value') is not None:
temp_gauge.add_metric(
[dev_id, dev_name, sensor.get('name', 'sensor')],
sensor['last_value']
)
# --- Обработка состояния котла ---
boiler_state = io_data.get('last-boiler-state')
if boiler_state:
is_working = 1 if boiler_state.get('boiler_work_time', 0) > 0 else 0
boiler_work_gauge.add_metric([dev_id, dev_name], is_working)
# --- Обработка уровня сигнала GSM ---
gsm_state = io_data.get('gsm-state')
if gsm_state and gsm_state.get('level') is not None:
signal_gauge.add_metric([dev_id, dev_name, 'gsm'], gsm_state['level'])
# --- Обработка состояния охраны ---
guard_state = io_data.get('guard-state')
if guard_state is not None:
is_guarded = 1 if guard_state == 'enabled' else 0
guard_gauge.add_metric([dev_id, dev_name], is_guarded)
yield temp_gauge
yield boiler_work_gauge
yield signal_gauge
yield guard_gauge
if __name__ == '__main__':
REGISTRY.register(ZontCollector())
start_http_server(EXPORTER_PORT)
logger.info(f"Экспортер запущен на порту {EXPORTER_PORT}")
while True:
time.sleep(10)