diff --git a/zont-exporter/zont_exporter.py b/zont-exporter/zont_exporter.py index 7df101f..0ddcee0 100644 --- a/zont-exporter/zont_exporter.py +++ b/zont-exporter/zont_exporter.py @@ -108,18 +108,24 @@ HEATING_TARGET_TEMP = Gauge('zont_heating_target_temp_celsius', 'Heating circuit 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']) -# Фактическая температура контура отопления (подача теплоносителя) HEATING_FLOW_TEMP = Gauge('zont_heating_flow_temp_celsius', 'Heating circuit flow temperature (actual)', ['device_id', 'circuit_id', 'name']) -# Котёл / OpenTherm -BOILER_CS = Gauge('zont_boiler_cs_celsius', 'Boiler calculated CH water temp', ['device_id', 'adapter_id']) +# Котёл / OpenTherm / цифровая шина +BOILER_CS = Gauge('zont_boiler_cs_celsius', 'Boiler calculated CH water temp (setpoint from boiler)', ['device_id', 'adapter_id']) BOILER_DS = Gauge('zont_boiler_ds_celsius', 'Boiler DHW setpoint', ['device_id', 'adapter_id']) -BOILER_BT = Gauge('zont_boiler_bt_celsius', 'Boiler actual flow water temp (OpenTherm)', ['device_id', 'adapter_id']) -BOILER_DT = Gauge('zont_boiler_dt_celsius', 'Boiler actual DHW temp (OpenTherm)', ['device_id', 'adapter_id']) -BOILER_OT = Gauge('zont_boiler_outdoor_celsius', 'Boiler outdoor temperature (OpenTherm)', ['device_id', 'adapter_id']) +BOILER_BT = Gauge('zont_boiler_water_celsius', 'Boiler actual flow water temp (water)', ['device_id', 'adapter_id']) +BOILER_DT = Gauge('zont_boiler_dhw_celsius', 'Boiler actual DHW temp (dhw)', ['device_id', 'adapter_id']) +BOILER_RWT = Gauge('zont_boiler_return_celsius', 'Boiler return water temp (return)', ['device_id', 'adapter_id']) +BOILER_OT = Gauge('zont_boiler_outdoor_celsius', 'Boiler outdoor temperature', ['device_id', 'adapter_id']) +BOILER_MODULATION = Gauge('zont_boiler_modulation_percent', 'Boiler burner modulation level (modul), %', ['device_id', 'adapter_id']) +BOILER_PRESSURE = Gauge('zont_boiler_pressure_bar', 'Boiler water pressure (press), bar', ['device_id', 'adapter_id']) +BOILER_STATE = Gauge('zont_boiler_state', 'Boiler state (0=off, 1=running, 2=error)', ['device_id', 'adapter_id']) +BOILER_ERROR_CODE = Gauge('zont_boiler_error_code', 'Boiler OEM error code (err)', ['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']) BOILER_FLAME = Gauge('zont_boiler_flame', 'Boiler flame on (1=on,0=off)', ['device_id', 'adapter_id']) +BOILER_CH_ACTIVE = Gauge('zont_boiler_ch_active', 'Boiler CH active (1=on)', ['device_id', 'adapter_id']) +BOILER_DHW_ACTIVE = Gauge('zont_boiler_dhw_active', 'Boiler DHW active (1=on)', ['device_id', 'adapter_id']) # Дискретные датчики DOOR_OPEN = Gauge('zont_door_open', 'Door open (1=open, 0=closed)', ['device_id', 'input_id', 'name']) @@ -390,6 +396,21 @@ def build_analog_input_types(device): return result +# --- Вычисление состояния котла --- + +def compute_boiler_state(flags): + """Вычисляет состояние котла по массиву флагов REST API. + Возвращает: 2 = авария, 1 = работает (горит пламя), 0 = выключен. + """ + if not flags: + return 0 + if 'f' in flags: # авария + return 2 + if 'fl' in flags: # горелка работает + return 1 + return 0 + + # --- Описание событий --- def describe_event(ev_type, data): @@ -502,7 +523,7 @@ def collect_events(did): events = events[-EVENT_MAX:] _last_event_poll_time = now - + logging.debug(f"raw_events returned {len(events)} events for window [{mintime}, {now}]") if not events: @@ -630,50 +651,59 @@ def collect_metrics(): if 'internet_weather' in device: INTERNET_WEATHER.labels(device_id=did).set(device['internet_weather']) - # --- last-boiler-state (для ZONT H-1/H-2/H-1V с OpenTherm) --- + # --- last-boiler-state (для ZONT H-1/H-2/H-1V с OpenTherm/цифровой шиной) --- last_boiler = io.get('last-boiler-state', {}) if last_boiler: ot = last_boiler.get('ot', {}) or {} + adapter = 'boiler' - # Фактическая температура теплоносителя (подача) — то, что нужно + # Температуры if ot.get('bt') is not None: - HEATING_FLOW_TEMP.labels( - device_id=did, circuit_id='boiler', name='boiler_flow' - ).set(ot['bt']) - BOILER_BT.labels(device_id=did, adapter_id='boiler').set(ot['bt']) - - # Расчётная температура теплоносителя - if ot.get('cs') is not None: - BOILER_CS.labels(device_id=did, adapter_id='boiler').set(ot['cs']) - - # Фактическая температура ГВС + BOILER_BT.labels(device_id=did, adapter_id=adapter).set(ot['bt']) + HEATING_FLOW_TEMP.labels(device_id=did, circuit_id=adapter, name='boiler_flow').set(ot['bt']) if ot.get('dt') is not None: - BOILER_DT.labels(device_id=did, adapter_id='boiler').set(ot['dt']) - - # Уличная температура по данным котла + BOILER_DT.labels(device_id=did, adapter_id=adapter).set(ot['dt']) + if ot.get('rwt') is not None: + BOILER_RWT.labels(device_id=did, adapter_id=adapter).set(ot['rwt']) if ot.get('ot') is not None: - BOILER_OT.labels(device_id=did, adapter_id='boiler').set(ot['ot']) + BOILER_OT.labels(device_id=did, adapter_id=adapter).set(ot['ot']) + if ot.get('cs') is not None: + BOILER_CS.labels(device_id=did, adapter_id=adapter).set(ot['cs']) + if ot.get('ds') is not None: + BOILER_DS.labels(device_id=did, adapter_id=adapter).set(ot['ds']) - # Флаг работы горелки + # Модуляция горелки + if ot.get('rml') is not None: + BOILER_MODULATION.labels(device_id=did, adapter_id=adapter).set(ot['rml']) + + # Давление теплоносителя + if ot.get('wp') is not None: + BOILER_PRESSURE.labels(device_id=did, adapter_id=adapter).set(ot['wp']) + + # Флаги состояния flags = ot.get('s', []) or [] - BOILER_FLAME.labels(device_id=did, adapter_id='boiler').set( - 1 if 'fl' in flags else 0 - ) + BOILER_STATE.labels(device_id=did, adapter_id=adapter).set(compute_boiler_state(flags)) + BOILER_FLAME.labels(device_id=did, adapter_id=adapter).set(1 if 'fl' in flags else 0) + BOILER_CH_ACTIVE.labels(device_id=did, adapter_id=adapter).set(1 if 'ch' in flags else 0) + BOILER_DHW_ACTIVE.labels(device_id=did, adapter_id=adapter).set(1 if 'dhw' in flags else 0) - # Авария котла + # Авария (код + признак) ff = ot.get('ff', {}) or {} - BOILER_FAIL.labels(device_id=did, adapter_id='boiler').set( - 1 if (ff.get('c') or ff.get('f')) else 0 + err_code = ff.get('c') or 0 + err_flags = ff.get('f') or [] + BOILER_ERROR_CODE.labels(device_id=did, adapter_id=adapter).set(err_code) + BOILER_FAIL.labels(device_id=did, adapter_id=adapter).set( + 1 if (err_code or err_flags) else 0 ) # Целевая температура и время работы if last_boiler.get('target_temp') is not None: HEATING_TARGET_TEMP.labels( - device_id=did, circuit_id='boiler', name='boiler_target' + device_id=did, circuit_id=adapter, name='boiler_target' ).set(last_boiler['target_temp']) if last_boiler.get('boiler_work_time') is not None: HEATING_WORKTIME.labels( - device_id=did, circuit_id='boiler', name='boiler_worktime' + device_id=did, circuit_id=adapter, name='boiler_worktime' ).set(last_boiler['boiler_work_time']) # --- z3k-state --- @@ -726,31 +756,58 @@ def collect_metrics(): if flow_temp is not None: HEATING_FLOW_TEMP.labels(device_id=did, circuit_id=obj_id, name=name).set(flow_temp) - # Адаптер котла / OpenTherm + # Адаптер котла / OpenTherm / цифровая шина (z3k) 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']) + + # Температуры if ot.get('bt') is not None: BOILER_BT.labels(device_id=did, adapter_id=obj_id).set(ot['bt']) HEATING_FLOW_TEMP.labels(device_id=did, circuit_id=obj_id, name=name).set(ot['bt']) if ot.get('dt') is not None: BOILER_DT.labels(device_id=did, adapter_id=obj_id).set(ot['dt']) + if ot.get('rwt') is not None: + BOILER_RWT.labels(device_id=did, adapter_id=obj_id).set(ot['rwt']) if ot.get('ot') is not None: BOILER_OT.labels(device_id=did, adapter_id=obj_id).set(ot['ot']) + if ot.get('cs') is not None: + BOILER_CS.labels(device_id=did, adapter_id=obj_id).set(ot['cs']) + if ot.get('cs2') is not None: + BOILER_CS.labels(device_id=did, adapter_id=f"{obj_id}_ch2").set(ot['cs2']) + if ot.get('ds') is not None: + BOILER_DS.labels(device_id=did, adapter_id=obj_id).set(ot['ds']) + # Модуляция горелки + if ot.get('rml') is not None: + BOILER_MODULATION.labels(device_id=did, adapter_id=obj_id).set(ot['rml']) + if ot.get('mrml') is not None: + BOILER_MODULATION.labels(device_id=did, adapter_id=f"{obj_id}_max").set(ot['mrml']) + + # Давление теплоносителя + if ot.get('wp') is not None: + BOILER_PRESSURE.labels(device_id=did, adapter_id=obj_id).set(ot['wp']) + + # Флаги состояния flags = ot.get('s', []) or [] + BOILER_STATE.labels(device_id=did, adapter_id=obj_id).set(compute_boiler_state(flags)) BOILER_FLAME.labels(device_id=did, adapter_id=obj_id).set(1 if 'fl' in flags else 0) + BOILER_CH_ACTIVE.labels(device_id=did, adapter_id=obj_id).set(1 if 'ch' in flags else 0) + BOILER_DHW_ACTIVE.labels(device_id=did, adapter_id=obj_id).set(1 if 'dhw' in flags else 0) + # Авария + ff = ot.get('ff', {}) or {} + err_code = ff.get('c') or 0 + err_flags = ff.get('f') or [] + BOILER_ERROR_CODE.labels(device_id=did, adapter_id=obj_id).set(err_code) + BOILER_FAIL.labels(device_id=did, adapter_id=obj_id).set( + 1 if (err_code or err_flags) else 0 + ) + + # Статус адаптера 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}")