Fix
Build and Push / maintence (push) Successful in 1s
Build and Push / buildApp (push) Successful in 6s
Build and Push / pushApp (push) Failing after 4s

This commit is contained in:
energys
2026-09-25 21:20:34 +09:00
parent 70ceac792e
commit fc8693d74f
2 changed files with 510 additions and 321 deletions
+342 -248
View File
@@ -1,135 +1,218 @@
# ZONT Prometheus Exporter
Экспортер метрик для устройств **ZONT** и **Mega SX** в формате, совместимом с **Prometheus**. Позволяет собирать данные о температуре, состоянии котла, уровне сигнала, охране и других параметрах через официальный API ZONT.
Экспортер телеметрии отопительных контроллеров **ZONT** (H-1V, H-1V.02, H1V02_PRO, H-1000, H-2000 и др.) в формате Prometheus.
## 📋 Возможности
Собирает текущее состояние устройства через облачное API `my.zont.online` и отдаёт метрики на HTTP-эндпоинт `/metrics`. Подходит для мониторинга в Grafana, алертинга в Alertmanager и любого стека, совместимого с Prometheus.
- 🔐 Аутентификация через официальный API ZONT (`my.zont.online/api`)
- 🌡️ Сбор температуры с проводных и радиодатчиков
- 🔥 Мониторинг работы котла (время работы, аварии)
- 📶 Уровень сигнала GSM/Wi-Fi
- 🛡️ Состояние охраны и сирены
- 🚗 Данные об автомобиле (ZTC): зажигание, двигатель, автозапуск
- 🐳 Готов к запуску в Docker
- ⚙️ Полностью настраивается через переменные окружения
- 🩺 Встроенный healthcheck
---
## 🚀 Быстрый старт
## 📋 Содержание
### 1. Клонирование проекта
- [Возможности](#-возможности)
- [Требования](#-требования)
- [Быстрый старт](#-быстрый-старт)
- [Переменные окружения](#-переменные-окружения)
- [Метрики](#-метрики)
- [Интеграция с Prometheus](#-интеграция-с-prometheus)
- [Пример алертов](#-пример-алертов)
- [Диагностика](#-диагностика)
- [Ограничения](#-ограничения)
- [Безопасность](#-безопасность)
- [Лицензия](#-лицензия)
---
## 🚀 Возможности
- **Текущее состояние контроллера**: онлайн-статус, напряжение питания, память, источник питания.
- **Связь**: уровень GSM, состояние регистрации в сети, RSSI Wi-Fi, наличие связи с сервером ZONT.
- **Температуры**: проводные и аналоговые датчики (в т.ч. «Погода из интернета»), статус исправности.
- **Отопление**: целевая и заданная температура по каждому отопительному контуру, время работы.
- **Котёл / OpenTherm**: расчётная температура теплоносителя, уставка ГВС, наличие связи с котлом, аварии.
- **Погода**: температура с внешнего источника.
- **Аналоговые входы**: напряжение на входах (например, контроль питания).
Работает как отдельный контейнер, не требует установки чего-либо на хост.
---
## ✅ Требования
- Docker (или Docker Compose)
- Аккаунт в личном кабинете [my.zont.online](https://my.zont.online) с добавленным устройством ZONT
- Сетевая доступность `my.zont.online:443` из контейнера
- Prometheus (опционально) для сбора метрик
---
## 🏁 Быстрый старт
### 1. Создайте файлы
Вам нужны два файла:
- `zont_exporter.py` — код экспортера
- `Dockerfile` — сборка образа
### 2. Соберите образ
```bash
git clone <ваш-репозиторий>/zont-exporter.git
cd zont-exporter
docker build -t zont-exporter .
```
### 2. Настройка окружения
Создайте файл `.env` на основе примера и заполните своими данными:
### 3. Запустите контейнер
```bash
cp .env.example .env
nano .env
```
Пример содержимого `.env`:
```env
ZONT_LOGIN=ваш_логин
ZONT_PASSWORD=ваш_пароль
ZONT_CLIENT_EMAIL=ваш@email.com
SCRAPE_INTERVAL=60
EXPORTER_PORT=8000
LOG_LEVEL=INFO
```
> ⚠️ **Важно**: `ZONT_CLIENT_EMAIL` — это контактный email, который вы указали при регистрации в ZONT. Он передаётся в заголовке `X-ZONT-Client` и используется производителем для связи с вами при изменениях в API.
### 3. Запуск
#### Вариант A: Docker Compose (рекомендуется)
```bash
docker compose up -d --build
```
#### Вариант B: Docker build + run
```bash
docker build -t zont-exporter:latest .
docker run -d \
--name zont-exporter \
--restart unless-stopped \
--env-file .env \
-p 8000:8000 \
zont-exporter:latest
-p 9101:9101 \
-e ZONT_LOGIN='your_login' \
-e ZONT_PASSWORD='your_password' \
-e ZONT_DEVICE_ID='554007' \
-e ZONT_CLIENT='you@example.com' \
zont-exporter
```
#### Вариант C: Локальный запуск (без Docker)
### 4. Проверьте
```bash
pip install -r requirements.txt
export $(cat .env | xargs)
python zont_exporter.py
curl http://localhost:9101/metrics | grep zont_
```
### 4. Проверка работы
```bash
# Логи
docker logs -f zont-exporter
# Метрики
curl http://localhost:8000/metrics
```
В ответе вы должны увидеть метрики вида:
Вы должны увидеть метрики вида:
```
# HELP zont_temperature_celsius Температура с датчиков ZONT
# TYPE zont_temperature_celsius gauge
zont_temperature_celsius{device_id="1580",device_name="Дом",sensor_name="Кухня"} 22.5
# HELP zont_boiler_working Работает ли котел (1 - да, 0 - нет)
# TYPE zont_boiler_working gauge
zont_boiler_working{device_id="1580",device_name="Дом"} 1
zont_device_online{device_id="554007"} 1.0
zont_voltage_volts{device_id="554007"} 12.1
zont_gsm_level{device_id="554007"} 17.0
zont_temperature_celsius{device_id="554007",name="Температура гаража 150",object_id="8200"} 18.8
zont_boiler_connected{adapter_id="4096",device_id="554007"} 0.0
```
## 📊 Доступные метрики
---
| Метрика | Тип | Описание | Метки |
## 🔧 Переменные окружения
| Переменная | Обязательна | По умолчанию | Описание |
|---|---|---|---|
| `zont_temperature_celsius` | Gauge | Температура с датчиков | `device_id`, `device_name`, `sensor_name` |
| `zont_boiler_working` | Gauge | Работа котла за последнюю минуту (1/0) | `device_id`, `device_name` |
| `zont_signal_level` | Gauge | Уровень сигнала GSM или Wi-Fi | `device_id`, `device_name`, `type` |
| `zont_guard_state` | Gauge | Состояние охраны (1 — включена, 0 — выключена) | `device_id`, `device_name` |
| `ZONT_LOGIN` | ✅ | — | Логин от личного кабинета `my.zont.online` |
| `ZONT_PASSWORD` | ✅ | — | Пароль от личного кабинета |
| `ZONT_DEVICE_ID` | ✅ | — | Числовой ID устройства (см. ниже, как узнать) |
| `ZONT_CLIENT` | ❌ | `my_exporter@example.com` | Контактный email для заголовка `X-ZONT-Client`. ZONT использует его для связи при изменениях API. Укажите свой. |
| `SCRAPE_INTERVAL` | ❌ | `60` | Интервал сбора метрик, секунды. Не имеет смысла ставить меньше 30 — API не успевает обновлять данные. |
| `EXPORTER_PORT` | ❌ | `9101` | Порт, на котором экспортер слушает `/metrics` внутри контейнера |
| `LOG_LEVEL` | ❌ | `INFO` | Уровень логирования: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`. Регистр не важен. |
## 🔗 Интеграция с Prometheus
### Как узнать `ZONT_DEVICE_ID`
Добавьте job в ваш `prometheus.yml`:
**Способ 1. Через API (рекомендуется)**
```bash
curl -s -X POST 'https://my.zont.online/api/devices' \
-u 'login:password' \
-H 'X-ZONT-Client: you@example.com' \
-H 'Content-Type: application/json' \
-d '{"load_io": true}' | python3 -m json.tool | grep -E '"id"|"name"|"serial"'
```
В ответе найдите блок своего устройства — поле `"id"` и есть `ZONT_DEVICE_ID`.
**Способ 2. Через личный кабинет**
Откройте устройство в веб-интерфейсе `my.zont.online` — ID обычно видно в URL страницы устройства.
### Пример запуска с нестандартным портом и отладкой
```bash
docker run -d \
--name zont-exporter \
--restart unless-stopped \
-p 9200:9200 \
-e ZONT_LOGIN='your_login' \
-e ZONT_PASSWORD='your_password' \
-e ZONT_DEVICE_ID='554007' \
-e ZONT_CLIENT='you@example.com' \
-e EXPORTER_PORT='9200' \
-e LOG_LEVEL='DEBUG' \
zont-exporter
```
> ⚠️ Номера портов в `-p <хост>:<контейнер>` и `EXPORTER_PORT` должны совпадать по контейнерной части. Пример выше: `EXPORTER_PORT=9200` и `-p 9200:9200`. Если хост-порт занят — используйте `-p 8080:9200`.
---
## 📊 Метрики
Все метрики имеют префикс `zont_` и label `device_id`. Для удобства читаемые имена объектов (датчиков, контуров) добавляются как label `name`.
### Общие
| Метрика | Тип | Labels | Описание |
|---|---|---|---|
| `zont_device_online` | gauge | `device_id` | 1 — устройство на связи, 0 — offline |
| `zont_device_last_receive_time` | gauge | `device_id` | Unix-время последнего пакета от устройства |
| `zont_voltage_volts` | gauge | `device_id` | Напряжение основного питания, В |
| `zont_power_source` | gauge | `device_id` | 1 — основное питание, 0 — иное |
| `zont_memory_used_percent` | gauge | `device_id` | Использование памяти контроллера, % |
| `zont_memory_used_bytes` | gauge | `device_id` | Использование памяти, байт |
| `zont_internet_weather_celsius` | gauge | `device_id` | Температура с внешнего источника погоды, °C |
### Связь
| Метрика | Тип | Labels | Описание |
|---|---|---|---|
| `zont_gsm_level` | gauge | `device_id` | Уровень сигнала GSM, 0–31 |
| `zont_gsm_state` | gauge | `device_id` | 0=not-registered, 1=home-network, 2=searching, 3=rejected, 5=roaming |
| `zont_wifi_rssi` | gauge | `device_id` | Уровень сигнала Wi-Fi |
| `zont_server_connected` | gauge | `device_id` | 1 — есть связь с сервером ZONT |
### Датчики температуры и аналоговые входы
| Метрика | Тип | Labels | Описание |
|---|---|---|---|
| `zont_temperature_celsius` | gauge | `device_id`, `object_id`, `name` | Текущая температура, °C |
| `zont_temperature_sensor_ok` | gauge | `device_id`, `object_id`, `name` | 1 — датчик исправен |
| `zont_analog_voltage_volts` | gauge | `device_id`, `object_id`, `name` | Напряжение на аналоговом входе, В |
### Отопление
| Метрика | Тип | Labels | Описание |
|---|---|---|---|
| `zont_heating_target_temp_celsius` | gauge | `device_id`, `circuit_id`, `name` | Фактическая целевая температура контура |
| `zont_heating_setpoint_temp_celsius` | gauge | `device_id`, `circuit_id`, `name` | Заданная температура (уставка) |
| `zont_heating_worktime_seconds` | gauge | `device_id`, `circuit_id`, `name` | Время работы контура за последнюю минуту, сек |
| `zont_heating_status` | gauge | `device_id`, `circuit_id`, `name` | Код статуса контура |
### Котёл / OpenTherm
| Метрика | Тип | Labels | Описание |
|---|---|---|---|
| `zont_boiler_cs_celsius` | gauge | `device_id`, `adapter_id` | Расчётная температура теплоносителя, °C |
| `zont_boiler_ds_celsius` | gauge | `device_id`, `adapter_id` | Уставка температуры ГВС, °C |
| `zont_boiler_connected` | gauge | `device_id`, `adapter_id` | 1 — котёл на связи, 0 — нет |
| `zont_boiler_fail` | gauge | `device_id`, `adapter_id` | 1 — авария котла |
---
## 🔌 Интеграция с Prometheus
Добавьте в `prometheus.yml`:
```yaml
scrape_configs:
- job_name: 'zont'
scrape_interval: 60s
scrape_timeout: 20s
static_configs:
- targets: ['zont-exporter:8000']
- targets: ['zont-exporter:9101']
labels:
instance: 'home-boiler'
```
Если Prometheus запущен на хосте (вне Docker), используйте `host.docker.internal` или IP вашего сервера:
Если Prometheus работает в том же Docker-сети, используйте имя контейнера. Если на хосте — `host.docker.internal:9101` (Windows/macOS) или IP хоста (Linux).
```yaml
scrape_configs:
- job_name: 'zont'
scrape_interval: 60s
static_configs:
- targets: ['host.docker.internal:8000']
```
## 📈 Полный стек мониторинга
Если у вас ещё нет Prometheus и Grafana, используйте готовый `docker-compose.yml`:
### Docker Compose (пример)
```yaml
services:
@@ -137,163 +220,174 @@ services:
build: .
container_name: zont-exporter
restart: unless-stopped
env_file:
- .env
networks:
- monitoring
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
ports:
- "9090:9090"
networks:
- monitoring
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
volumes:
- grafana-data:/var/lib/grafana
ports:
- "3000:3000"
networks:
- monitoring
networks:
monitoring:
driver: bridge
volumes:
prometheus-data:
grafana-data:
- "9101:9101"
environment:
ZONT_LOGIN: "your_login"
ZONT_PASSWORD: "your_password"
ZONT_DEVICE_ID: "554007"
ZONT_CLIENT: "you@example.com"
SCRAPE_INTERVAL: "60"
EXPORTER_PORT: "9101"
LOG_LEVEL: "INFO"
```
После запуска:
- **Prometheus**: http://localhost:9090
- **Grafana**: http://localhost:3000 (логин/пароль по умолчанию: `admin`/`admin`)
## ⚙️ Конфигурация
Все параметры задаются через переменные окружения:
| Переменная | Обязательна | По умолчанию | Описание |
|---|---|---|---|
| `ZONT_LOGIN` | ✅ | — | Логин от личного кабинета ZONT |
| `ZONT_PASSWORD` | ✅ | — | Пароль от личного кабинета ZONT |
| `ZONT_CLIENT_EMAIL` | ❌ | `exporter@localhost` | Контактный email (заголовок `X-ZONT-Client`) |
| `ZONT_API_URL` | ❌ | `https://my.zont.online/api` | Базовый URL API |
| `SCRAPE_INTERVAL` | ❌ | `60` | Интервал опроса в секундах |
| `EXPORTER_PORT` | ❌ | `8000` | Порт HTTP-сервера экспортера |
| `LOG_LEVEL` | ❌ | `INFO` | Уровень логирования (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
## 🐳 Сборка Docker-образа
```bash
# Сборка
docker build -t zont-exporter:latest .
# Запуск с проверкой переменных
docker run --rm --env-file .env zont-exporter:latest
```
### Healthcheck
Docker автоматически проверяет доступность эндпоинта `/metrics` каждые 30 секунд. Если экспортер перестанет отвечать, контейнер будет помечен как `unhealthy`.
Проверить статус:
```bash
docker inspect --format='{{.State.Health.Status}}' zont-exporter
```
## 🔧 Расширение
### Добавление новых метрик
API ZONT предоставляет много данных. Чтобы добавить новую метрику, откройте `zont_exporter.py` и расширьте метод `collect()`:
```python
# Пример: сбор пользовательских статусов (custom_controls_state)
custom_gauge = GaugeMetricFamily(
'zont_custom_status',
'Пользовательский статус',
labels=['device_id', 'device_name', 'status_id']
)
for device in devices:
custom_state = device.get('io', {}).get('custom_controls_state', {})
for status_id, value in custom_state.items():
custom_gauge.add_metric(
[str(device['id']), device['name'], str(status_id)],
value
)
yield custom_gauge
```
### Полезные разделы API
Согласно [официальной документации](https://lk.zont-online.ru/api/docs/), для расширения доступны:
- `last-boiler-state` — детальное состояние котла (OpenTherm, модуляция, давление)
- `custom_controls_state` — пользовательские статусы (ZTC-7xx, Mega SX)
- `ztc_state` — состояние контроллера (баланс SIM, напряжение, статус-флаги)
- `temperature` — история температур через `load_data`
## 🐛 Отладка
### Экспортер не запускается
Проверьте логи:
```bash
docker logs zont-exporter
```
Частые причины:
- Не заданы `ZONT_LOGIN` или `ZONT_PASSWORD`
- Неверный логин/пароль
- Отсутствует доступ к `my.zont.online`
### Метрики пустые
- Убедитесь, что к вашему аккаунту ZONT привязаны устройства.
- Проверьте `ZONT_CLIENT_EMAIL` — некоторые методы требуют корректного контакта.
- Увеличьте `LOG_LEVEL=DEBUG` для детальной диагностики.
### Проблемы с токеном
Экспортер автоматически обновляет токен при получении `403`. Если проблема повторяется:
- Убедитесь, что пароль не содержит спецсимволов, требующих экранирования.
- Проверьте, не отозван ли токен в личном кабинете ZONT.
## 📚 Документация API ZONT
- Официальная документация: https://lk.zont-online.ru/api/docs/
- Облегчённое Widget API v3: https://my.zont.online/api/widget/v3
- Сайт производителя: https://zont.online
## 📄 Лицензия
MIT
## 🤝 Вклад
Pull request'ы приветствуются. Для крупных изменений сначала откройте issue для обсуждения.
---
**Полезные ссылки:**
## 🚨 Пример алертов
- [Prometheus](https://prometheus.io/)
- [Grafana](https://grafana.com/)
- [prometheus-client (Python)](https://github.com/prometheus/client_python)
`prometheus/rules/zont.yml`:
```yaml
groups:
- name: zont
rules:
- alert: ZontDeviceOffline
expr: zont_device_online == 0
for: 10m
labels:
severity: critical
annotations:
summary: "ZONT {{ $labels.device_id }} offline"
description: "Устройство не выходит на связь более 10 минут."
- alert: ZontServerDisconnected
expr: zont_server_connected == 0
for: 5m
labels:
severity: warning
annotations:
summary: "ZONT {{ $labels.device_id }} потерял связь с сервером"
- alert: ZontBoilerDisconnected
expr: zont_boiler_connected == 0
for: 15m
labels:
severity: warning
annotations:
summary: "ZONT {{ $labels.device_id }}: нет связи с котлом (адаптер {{ $labels.adapter_id }})"
- alert: ZontBoilerFail
expr: zont_boiler_fail == 1
for: 5m
labels:
severity: critical
annotations:
summary: "ZONT {{ $labels.device_id }}: авария котла"
- alert: ZontLowVoltage
expr: zont_voltage_volts < 10
for: 5m
labels:
severity: warning
annotations:
summary: "ZONT {{ $labels.device_id }}: низкое напряжение питания {{ $value }} В"
- alert: ZontWeakGSM
expr: zont_gsm_level < 5
for: 15m
labels:
severity: warning
annotations:
summary: "ZONT {{ $labels.device_id }}: слабый сигнал GSM ({{ $value }})"
- alert: ZontHighMemory
expr: zont_memory_used_percent > 85
for: 30m
labels:
severity: info
annotations:
summary: "ZONT {{ $labels.device_id }}: высокая загрузка памяти {{ $value }}%"
```
---
## 🩺 Диагностика
### Метрики пустые или `/metrics` отдаёт только `python_*`
1. Проверьте логи:
```bash
docker logs zont-exporter --tail 100
```
2. Если непонятно, что происходит, включите подробное логирование:
```bash
docker run -d --name zont-exporter \
-p 9101:9101 \
-e ZONT_LOGIN='...' -e ZONT_PASSWORD='...' \
-e ZONT_DEVICE_ID='554007' \
-e ZONT_CLIENT='you@example.com' \
-e LOG_LEVEL='DEBUG' \
zont-exporter
```
Затем смотрите логи:
```bash
docker logs zont-exporter --tail 200
```
В режиме `DEBUG` видны детали ответов API и структура `io`-объектов.
3. Ищите в логах строки:
- `Successfully obtained ZONT auth token.` — токен получен.
- `Failed to get auth token: ...` — проблема с логином/паролем.
- `API error: ...` — API вернул ошибку.
- `Device 554007 not found in account.` — неверный `ZONT_DEVICE_ID`.
- `Metrics collected successfully for device ...` — всё ок.
### Ошибка `SyntaxError: name 'auth_token' is used prior to global declaration`
Объявление `global auth_token` должно стоять **в самом начале** функции, до любого использования переменной. В актуальной версии кода это уже учтено.
### Ошибка аутентификации
Проверьте логин/пароль руками:
```bash
curl -s -X POST 'https://my.zont.online/api/get_authtoken' \
-u 'login:password' \
-H 'X-ZONT-Client: you@example.com' \
-H 'Content-Type: application/json' \
-d '{"client_name": "debug"}' | python3 -m json.tool
```
Ожидаемый ответ: `{"ok": true, "token": "..."}`.
### Хочу больше данных (история, а не текущее состояние)
Метод `devices` отдаёт **снимок текущего состояния**. Историю (графики за период) можно получить через метод `load_data` с типами `z3k_temperature`, `z3k_heating_circuit`, `z3k_boiler_adapter`, `ztc_state`. Для этого требуется отдельная логика — при необходимости расширьте экспортер.
---
## ⚠️ Ограничения
- Экспортер собирает **только текущее состояние**, не историю. Для графиков используйте `load_data` (см. выше) или смотрите графики в личном кабинете ZONT.
- Работает только с **устройствами, привязанными к аккаунту my.zont.online**. Локальный доступ к контроллеру по USB/локальной сети не используется.
- Тип `thermostat_work` через `load_data` для модели **H1V02_PRO** (H-1V.02) возвращает пустые массивы. Используйте данные из `devices?load_io=true` — там вся актуальная телеметрия.
- Набор доступных полей в `io.z3k-state` зависит от модели, прошивки и конфигурации. Экспортер игнорирует отсутствующие поля — это нормально.
- Требуется HTTPS-доступ к `my.zont.online`. При использовании прокси задайте `HTTPS_PROXY` в окружении контейнера.
---
## 🔒 Безопасность
- **Не храните логин/пароль в открытом виде в Docker Compose / systemd unit.** Используйте:
- Docker secrets,
- переменные окружения из защищённого файла (`--env-file`),
- внешний секрет-менеджер (Vault, SOPS, Ansible Vault).
- **Заведите отдельного пользователя** в ZONT для API-интеграции. Не используйте основную учётную запись владельца.
- **Ротируйте пароль**, если он где-то засветился (переписка, скриншоты, git-история).
- Экспортер **не пишет** логин/пароль в логи, но может логировать ответ API. Проверьте уровень логирования, если ответы содержат чувствительные данные.
- Токен ZONT (`X-ZONT-Token`) хранится только в памяти процесса и перезапрашивается при истечении (`unauthorized` / `token_expired`).
- Эндпоинт `/metrics` **не имеет аутентификации**. Не выставляйте его в интернет. Используйте network policy / firewall.
---
## 📄 Лицензия
MIT. Используйте на свой риск. Проект не связан с ООО «Микро Лайн» / ZONT — это независимая интеграция с публичным API.
---
## 🙏 Благодарности
- Команде ZONT за публичное API и [документацию](https://my.zont.online/api).
- Сообществу Prometheus за `prometheus_client`.
+168 -73
View File
@@ -9,11 +9,11 @@ Mega SX, ZTC и др.) в формате Prometheus.
Переменные окружения:
ZONT_LOGIN — логин от my.zont.online (обязательно)
ZONT_PASSWORD — пароль (обязательно, если не задан ZONT_TOKEN)
ZONT_DEVICE_ID — ID устройства (0 = взять первое; по умолчанию 0)
ZONT_DEVICE_ID — ID устройства
ZONT_EMAIL — значение заголовка X-ZONT-Client (по умолчанию exporter@example.com)
ZONT_TOKEN — готовый auth-токен (опционально)
ZONT_POLL_INTERVAL — интервал опроса, сек (по умолчанию 60)
EXPORTER_PORT — порт HTTP-сервера метрик (по умолчанию 9877)
EXPORTER_PORT — порт HTTP-сервера метрик (по умолчанию 9101)
LOG_LEVEL — DEBUG/INFO/WARNING/ERROR (по умолчанию INFO)
"""
import os
@@ -22,27 +22,64 @@ import logging
import requests
from prometheus_client import start_http_server, Gauge
# Настройка логирования
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Конфигурация из переменных окружения
ZONT_LOGIN = os.getenv('ZONT_LOGIN')
ZONT_PASSWORD = os.getenv('ZONT_PASSWORD')
ZONT_CLIENT = os.getenv('ZONT_CLIENT', 'my_exporter@example.com')
DEVICE_ID = int(os.getenv('ZONT_DEVICE_ID'))
API_BASE = "https://my.zont.online/api"
SCRAPE_INTERVAL = int(os.getenv('SCRAPE_INTERVAL', 60))
EXPORTER_PORT = int(os.getenv('EXPORTER_PORT', 9101))
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
# Определение метрик Prometheus
TEMP_GAUGE = Gauge('zont_temperature_celsius', 'Current temperature', ['device_id', 'sensor_name', 'sensor_type'])
BOILER_WORK = Gauge('zont_boiler_work_seconds', 'Boiler work time in last minute', ['device_id'])
TARGET_TEMP = Gauge('zont_target_temperature_celsius', 'Target temperature', ['device_id'])
# --- Метрики Prometheus ---
# Общие
DEVICE_ONLINE = Gauge('zont_device_online', 'Device online status (1=online, 0=offline)', ['device_id'])
DEVICE_LAST_SEEN = Gauge('zont_device_last_receive_time', 'Unix timestamp of last data from device', ['device_id'])
VOLTAGE = Gauge('zont_voltage_volts', 'Supply voltage in volts', ['device_id'])
POWER_SOURCE = Gauge('zont_power_source', 'Power source (1=main, 0=other)', ['device_id'])
# GSM / WiFi
GSM_LEVEL = Gauge('zont_gsm_level', 'GSM signal level 0-31', ['device_id'])
GSM_STATE = Gauge('zont_gsm_state', 'GSM state (0=not-registered,1=home-network,2=searching,3=rejected,5=roaming)', ['device_id'])
WIFI_RSSI = Gauge('zont_wifi_rssi', 'WiFi RSSI', ['device_id'])
SERVER_CONNECTED = Gauge('zont_server_connected', 'Connected to ZONT server (1=yes)', ['device_id'])
# Память
MEMORY_PERCENT = Gauge('zont_memory_used_percent', 'Memory usage percent', ['device_id'])
MEMORY_BYTES = Gauge('zont_memory_used_bytes', 'Memory usage bytes', ['device_id'])
# Погода
INTERNET_WEATHER = Gauge('zont_internet_weather_celsius', 'Internet weather outside temperature', ['device_id'])
# Температуры (объекты из z3k-state)
TEMP_CURR = Gauge('zont_temperature_celsius', 'Current temperature', ['device_id', 'object_id', 'name'])
SENSOR_OK = Gauge('zont_temperature_sensor_ok', 'Temperature sensor OK (1=ok)', ['device_id', 'object_id', 'name'])
# Напряжение аналогового входа
ANALOG_VOLTAGE = Gauge('zont_analog_voltage_volts', 'Analog input voltage', ['device_id', 'object_id', 'name'])
# Отопление
HEATING_TARGET_TEMP = Gauge('zont_heating_target_temp_celsius', 'Heating circuit target temperature', ['device_id', 'circuit_id', 'name'])
HEATING_SETPOINT_TEMP = Gauge('zont_heating_setpoint_temp_celsius', 'Heating circuit setpoint temperature', ['device_id', 'circuit_id', 'name'])
HEATING_WORKTIME = Gauge('zont_heating_worktime_seconds', 'Heating circuit work time in last minute', ['device_id', 'circuit_id', 'name'])
HEATING_STATUS = Gauge('zont_heating_status', 'Heating circuit status code', ['device_id', 'circuit_id', 'name'])
# Котёл / OpenTherm
BOILER_CS = Gauge('zont_boiler_cs_celsius', 'Boiler calculated CH water temp', ['device_id', 'adapter_id'])
BOILER_DS = Gauge('zont_boiler_ds_celsius', 'Boiler DHW setpoint', ['device_id', 'adapter_id'])
BOILER_CONNECTED = Gauge('zont_boiler_connected', 'Boiler connected (1=yes,0=no)', ['device_id', 'adapter_id'])
BOILER_FAIL = Gauge('zont_boiler_fail', 'Boiler failure (1=fail)', ['device_id', 'adapter_id'])
# Справочники ID -> имя для читаемых меток
_name_cache = {}
# Глобальная переменная для токена
auth_token = None
def get_auth_token():
"""Получает токен аутентификации ZONT API."""
global auth_token
try:
resp = requests.post(
@@ -62,109 +99,167 @@ def get_auth_token():
except Exception as e:
logging.error(f"Exception while getting auth token: {e}")
def fetch_last_value(dta_list):
"""Извлекает последнее значение из Delta-time array."""
if not dta_list or not isinstance(dta_list, list):
return None
# Последний элемент массива имеет актуальное значение
last_entry = dta_list[-1]
if isinstance(last_entry, list) and len(last_entry) >= 2:
return last_entry[1]
return None
def build_name_cache(device):
"""Собираем человекочитаемые имена для ID объектов."""
cache = {}
z3k = device.get('z3k_config', {})
for s in z3k.get('wired_temperature_sensors', []):
cache[s['id']] = s.get('name') or f"wired_{s['id']}"
for s in z3k.get('analog_temperature_sensors', []):
cache[s['id']] = s.get('name') or f"analog_{s['id']}"
for s in z3k.get('analog_inputs', []):
cache[s['id']] = s.get('name') or f"input_{s['id']}"
for hc in z3k.get('heating_circuits', []):
cache[hc['id']] = hc.get('name') or f"hc_{hc['id']}"
for b in z3k.get('boiler_adapters', []):
cache[b['id']] = b.get('name') or f"boiler_{b['id']}"
return cache
GSM_STATE_MAP = {
'not-registered': 0,
'home-network': 1,
'searching': 2,
'rejected': 3,
'roaming': 5,
}
def collect_metrics():
"""Основная функция сбора метрик."""
global auth_token
global auth_token, _name_cache
if not auth_token:
logging.warning("No auth token available, trying to authenticate...")
get_auth_token()
if not auth_token:
return
now = int(time.time())
mintime = now - 3600
payload = {
"requests": [{
"device_id": DEVICE_ID,
"data_types": ["thermostat_work"],
"mintime": mintime,
"maxtime": now
}]
}
try:
resp = requests.post(
f"{API_BASE}/load_data",
f"{API_BASE}/devices",
headers={
'X-ZONT-Client': ZONT_CLIENT,
'X-ZONT-Token': auth_token,
'Content-Type': 'application/json'
'Content-Type': 'application/json',
},
json=payload,
json={'load_io': True},
timeout=15
)
resp.raise_for_status()
data = resp.json()
if not data.get('ok'):
logging.error(f"API returned error: {data.get('error_ui', data.get('error'))}")
if data.get('error') in ['unauthorized', 'token_expired']:
logging.error(f"API error: {data.get('error_ui', data.get('error'))}")
if data.get('error') in ('unauthorized', 'token_expired'):
auth_token = None
return
responses = data.get('responses', [])
if not responses:
logging.warning("Empty responses from API.")
devices = data.get('devices', [])
device = next((d for d in devices if d.get('id') == DEVICE_ID), None)
if not device:
logging.warning(f"Device {DEVICE_ID} not found in account.")
return
work_data = responses[0].get('thermostat_work')
if not work_data:
logging.info("No thermostat_work data in response.")
return
# Обновляем кэш имён
_name_cache = build_name_cache(device)
boiler_work = fetch_last_value(work_data.get('boiler_work_time'))
if boiler_work is not None:
BOILER_WORK.labels(device_id=str(DEVICE_ID)).set(boiler_work)
did = str(DEVICE_ID)
target_temp = fetch_last_value(work_data.get('target_temp'))
if target_temp is not None:
TARGET_TEMP.labels(device_id=str(DEVICE_ID)).set(target_temp)
# --- Общие ---
DEVICE_ONLINE.labels(device_id=did).set(1 if device.get('online') else 0)
if device.get('last_receive_time'):
DEVICE_LAST_SEEN.labels(device_id=did).set(device['last_receive_time'])
temps = work_data.get('temperature')
if temps and isinstance(temps, dict):
for sensor_id, sensor_info in temps.items():
if not isinstance(sensor_info, dict):
continue
name = sensor_info.get('name', sensor_id)
val = fetch_last_value(sensor_info.get('temperature'))
if val is not None:
TEMP_GAUGE.labels(
device_id=str(DEVICE_ID),
sensor_name=name,
sensor_type='thermostat_sensor'
).set(val)
io = device.get('io', {})
if 'voltage' in io:
VOLTAGE.labels(device_id=did).set(io['voltage'])
if io.get('power-source'):
POWER_SOURCE.labels(device_id=did).set(1 if io['power-source'] == 'main' else 0)
# --- GSM / WiFi / server ---
gsm = io.get('gsm-state', {})
if 'level' in gsm:
GSM_LEVEL.labels(device_id=did).set(gsm['level'])
if gsm.get('state'):
GSM_STATE.labels(device_id=did).set(GSM_STATE_MAP.get(gsm['state'], -1))
wifi = io.get('additional-wifi-state') or io.get('wifi-state') or {}
if 'rssi' in wifi:
WIFI_RSSI.labels(device_id=did).set(wifi['rssi'])
conn = io.get('connection-state', {})
SERVER_CONNECTED.labels(device_id=did).set(1 if conn.get('connected_to_server') else 0)
# --- Память ---
mem = io.get('memory-use-state', {})
if 'percents' in mem:
MEMORY_PERCENT.labels(device_id=did).set(mem['percents'])
if 'bytes' in mem:
MEMORY_BYTES.labels(device_id=did).set(mem['bytes'])
# --- Погода ---
if 'internet_weather' in device:
INTERNET_WEATHER.labels(device_id=did).set(device['internet_weather'])
# --- z3k-state ---
z3k_state = io.get('z3k-state', {})
for obj_id, obj in z3k_state.items():
name = _name_cache.get(int(obj_id), obj_id) if obj_id.isdigit() else obj_id
# Температурные датчики
if isinstance(obj, dict) and 'curr_temp' in obj:
if obj['curr_temp'] is not None:
TEMP_CURR.labels(device_id=did, object_id=obj_id, name=name).set(obj['curr_temp'])
if 'sensor_ok' in obj:
SENSOR_OK.labels(device_id=did, object_id=obj_id, name=name).set(1 if obj['sensor_ok'] else 0)
# Аналоговый вход (напряжение)
if isinstance(obj, dict) and 'voltage' in obj and obj_id == '20550':
if obj['voltage'] is not None:
ANALOG_VOLTAGE.labels(device_id=did, object_id=obj_id, name=name).set(obj['voltage'])
# Отопительные контуры
if isinstance(obj, dict) and 'target_temp' in obj and 'worktime' in obj:
if obj.get('target_temp') is not None:
HEATING_TARGET_TEMP.labels(device_id=did, circuit_id=obj_id, name=name).set(obj['target_temp'])
if obj.get('setpoint_temp') is not None:
HEATING_SETPOINT_TEMP.labels(device_id=did, circuit_id=obj_id, name=name).set(obj['setpoint_temp'])
if obj.get('worktime') is not None:
HEATING_WORKTIME.labels(device_id=did, circuit_id=obj_id, name=name).set(obj['worktime'])
if obj.get('status') is not None:
HEATING_STATUS.labels(device_id=did, circuit_id=obj_id, name=name).set(obj['status'])
# Адаптер котла / OpenTherm
if isinstance(obj, dict) and 'ot' in obj and 'status' in obj:
ot = obj.get('ot', {}) or {}
if ot.get('cs') is not None:
BOILER_CS.labels(device_id=did, adapter_id=obj_id).set(ot['cs'])
if ot.get('ds') is not None:
BOILER_DS.labels(device_id=did, adapter_id=obj_id).set(ot['ds'])
status = obj.get('status', {})
BOILER_CONNECTED.labels(device_id=did, adapter_id=obj_id).set(
0 if status.get('no_boiler_connection') else 1
)
BOILER_FAIL.labels(device_id=did, adapter_id=obj_id).set(
1 if status.get('boiler_fail') else 0
)
logging.info(f"Metrics collected successfully for device {DEVICE_ID}")
except Exception as e:
logging.error(f"Error during metric collection: {e}")
if __name__ == '__main__':
# Проверка обязательных переменных
if not all([ZONT_LOGIN, ZONT_PASSWORD, DEVICE_ID]):
logging.critical("Missing required environment variables: ZONT_LOGIN, ZONT_PASSWORD, ZONT_DEVICE_ID")
logging.critical("Missing required env vars: ZONT_LOGIN, ZONT_PASSWORD, ZONT_DEVICE_ID")
exit(1)
# Первичная аутентификация
get_auth_token()
start_http_server(EXPORTER_PORT) # <-- было 9101
logging.info(f"Prometheus exporter started on port {EXPORTER_PORT}")
# Запуск HTTP-сервера Prometheus на порту 9101
start_http_server(9101)
logging.info("Prometheus exporter started on port 9101")
# Основной цикл
while True:
collect_metrics()
time.sleep(SCRAPE_INTERVAL)