diff --git a/zont-exporter/zont_exporter.py b/zont-exporter/zont_exporter.py index 6d5db68..f432fde 100644 --- a/zont-exporter/zont_exporter.py +++ b/zont-exporter/zont_exporter.py @@ -15,6 +15,8 @@ Mega SX, ZTC и др.) в формате Prometheus. ZONT_POLL_INTERVAL — интервал опроса, сек (по умолчанию 60) EXPORTER_PORT — порт HTTP-сервера метрик (по умолчанию 9101) LOG_LEVEL — DEBUG/INFO/WARNING/ERROR (по умолчанию INFO) + ZONT_DOOR_INVERT — инвертировать логику двери (1=да, 0=нет, по умолчанию 0) + ZONT_DOOR_SENSOR_TYPES — через запятую, sensor_type дискретных датчиков (по умолчанию 20) """ import os import time @@ -31,7 +33,13 @@ 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() +LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper() + +# Логика двери +DOOR_INVERT = os.getenv('ZONT_DOOR_INVERT', '0') == '1' +DOOR_SENSOR_TYPES = set( + int(x) for x in os.getenv('ZONT_DOOR_SENSOR_TYPES', '20').split(',') if x.strip() +) # --- Метрики Prometheus --- @@ -73,8 +81,28 @@ BOILER_DS = Gauge('zont_boiler_ds_celsius', 'Boiler DHW setpoint', ['device_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']) +# Дискретные датчики (дверь, окно и т.п.) на аналоговых входах +DOOR_OPEN = Gauge( + 'zont_door_open', + 'Door open (1=open, 0=closed)', + ['device_id', 'input_id', 'name'] +) +DOOR_VOLTAGE = Gauge( + 'zont_door_voltage_volts', + 'Voltage on door sensor analog input', + ['device_id', 'input_id', 'name'] +) + +# Охранные зоны (помещения) +GUARD_ZONE_ONGUARD = Gauge( + 'zont_guard_zone_onguard', + 'Guard zone on guard (1=yes, 0=no)', + ['device_id', 'zone_id', 'name'] +) + # Справочники ID -> имя для читаемых меток _name_cache = {} +_analog_input_types = {} # {input_id: sensor_type} auth_token = None @@ -115,9 +143,19 @@ def build_name_cache(device): 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']}" + for gz in z3k.get('guard_zones', []): + cache[gz['id']] = gz.get('name') or f"zone_{gz['id']}" return cache +def build_analog_input_types(device): + """Возвращает {input_id: sensor_type} для всех аналоговых входов.""" + result = {} + for ai in device.get('z3k_config', {}).get('analog_inputs', []): + result[ai['id']] = ai.get('sensor_type') + return result + + GSM_STATE_MAP = { 'not-registered': 0, 'home-network': 1, @@ -128,7 +166,7 @@ GSM_STATE_MAP = { def collect_metrics(): - global auth_token, _name_cache + global auth_token, _name_cache, _analog_input_types if not auth_token: get_auth_token() @@ -161,8 +199,9 @@ def collect_metrics(): logging.warning(f"Device {DEVICE_ID} not found in account.") return - # Обновляем кэш имён + # Обновляем кэши _name_cache = build_name_cache(device) + _analog_input_types = build_analog_input_types(device) did = str(DEVICE_ID) @@ -214,11 +253,29 @@ def collect_metrics(): 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) - # Аналоговый вход (напряжение) + # Аналоговый вход для контроля напряжения (id=20550) — оставляем отдельную метрику 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']) + # Дискретные датчики (дверь/окно) — есть поля voltage и value, + # а в конфиге sensor_type входит в DOOR_SENSOR_TYPES + if (isinstance(obj, dict) and 'value' in obj and 'voltage' in obj + and _analog_input_types.get(int(obj_id)) in DOOR_SENSOR_TYPES): + if obj.get('voltage') is not None: + DOOR_VOLTAGE.labels(device_id=did, input_id=obj_id, name=name).set(obj['voltage']) + if obj.get('value') is not None: + door_value = obj['value'] + if DOOR_INVERT: + door_value = 1 - door_value + DOOR_OPEN.labels(device_id=did, input_id=obj_id, name=name).set(door_value) + + # Охранные зоны (есть поле onguard) + if isinstance(obj, dict) and 'onguard' in obj: + GUARD_ZONE_ONGUARD.labels(device_id=did, zone_id=obj_id, name=name).set( + 1 if obj['onguard'] else 0 + ) + # Отопительные контуры if isinstance(obj, dict) and 'target_temp' in obj and 'worktime' in obj: if obj.get('target_temp') is not None: @@ -257,7 +314,7 @@ if __name__ == '__main__': exit(1) get_auth_token() - start_http_server(EXPORTER_PORT) # <-- было 9101 + start_http_server(EXPORTER_PORT) logging.info(f"Prometheus exporter started on port {EXPORTER_PORT}") while True: