-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add zont_prom_exporter and a couple of examples (#3)
* Add zont_prom_exporter, minor refactoring * fix: make python 3.8/3.9 linters happier
- Loading branch information
Showing
11 changed files
with
697 additions
and
200 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import sys | ||
import json | ||
from zont_api import ZontAPI | ||
|
||
|
||
def main(): | ||
zapi = ZontAPI() | ||
|
||
device_data = [] | ||
devices = zapi.get_devices() | ||
|
||
for device in devices: | ||
device_data.append(device.data) | ||
|
||
print(json.dumps(device_data, ensure_ascii=False)) | ||
|
||
|
||
if __name__ == "__main__": | ||
sys.exit(main()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import sys | ||
import json | ||
from zont_api import ZontAPI | ||
|
||
|
||
def main(): | ||
zapi = ZontAPI() | ||
|
||
sensors_data = [] | ||
devices = zapi.get_devices() | ||
|
||
for device in devices: | ||
sensors_data.extend(device.get_analog_inputs()) | ||
sensors_data.extend(device.get_analog_temperature_sensors()) | ||
sensors_data.extend(device.get_boiler_adapters()) | ||
|
||
print(json.dumps(sensors_data, ensure_ascii=False)) | ||
|
||
|
||
if __name__ == "__main__": | ||
sys.exit(main()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
FROM python:3.12-slim | ||
|
||
ARG ZONT_API_VERSION | ||
|
||
WORKDIR /app | ||
|
||
COPY examples/zont_prom_exporter/requirements.txt . | ||
RUN pip install --no-cache-dir -r requirements.txt | ||
|
||
COPY dist/zont_api-${ZONT_API_VERSION}-py3-none-any.whl /tmp | ||
RUN pip install /tmp/zont_api-${ZONT_API_VERSION}-py3-none-any.whl | ||
|
||
COPY examples/zont_prom_exporter/zont_prom_exporter.py . | ||
|
||
CMD [ "python", "./zont_prom_exporter.py" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
services: | ||
zont_prom_exporter: | ||
container_name: zont_prom_exporter | ||
image: zont_prom_exporter:1.0.0 | ||
secrets: | ||
- zont_api_client | ||
- zont_api_token | ||
environment: | ||
- PYTHONUNBUFFERED=1 | ||
- ZONT_API_CLIENT_FILE=/run/secrets/zont_api_client | ||
- ZONT_API_TOKEN_FILE=/run/secrets/zont_api_token | ||
ports: | ||
- 6000:6000 | ||
|
||
secrets: | ||
zont_api_client: | ||
file: ~/.env.zont-api-client | ||
zont_api_token: | ||
file: ~/.env.zont-api-token |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
requests>=2.32.3 | ||
Flask>=3.0.3 | ||
prometheus_client>=0.21.0 | ||
APScheduler>=3.10.4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import sys | ||
import logging | ||
from flask import Flask | ||
from apscheduler.schedulers.background import BackgroundScheduler | ||
from prometheus_client import make_wsgi_app, Gauge | ||
from werkzeug.middleware.dispatcher import DispatcherMiddleware | ||
from zont_api import ZontAPI, ZontAPIException | ||
|
||
APP_NAME = "zont_prom_exporter" | ||
|
||
logging.basicConfig( | ||
level=logging.INFO, | ||
format="%(asctime)s [%(levelname)s] %(name)s %(threadName)s: %(message)s", | ||
) | ||
|
||
logger = logging.getLogger(APP_NAME) | ||
|
||
""" | ||
urllib3_log = logging.getLogger('urllib3') | ||
urllib3_log.setLevel(logging.DEBUG) | ||
from http.client import HTTPConnection | ||
HTTPConnection.debuglevel = 1 | ||
""" | ||
|
||
app = Flask(APP_NAME) | ||
|
||
app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {"/metrics": make_wsgi_app()}) | ||
|
||
# напряжение питания | ||
# input voltage | ||
VOLTAGE = Gauge("zont_input_voltage", "Input voltage", ["device_id", "sensor_id"]) | ||
|
||
# температура воздуха в помещении | ||
# indoor air temperature | ||
INDOOR_AIR_TEMP = Gauge( | ||
"zont_indoor_air_temp", "Indoor air temperature", ["device_id", "sensor_id"] | ||
) | ||
|
||
# уличная температура | ||
# outdoor air temperature | ||
BOILER_OTA_OT = Gauge( | ||
"zont_boiler_outdoor_air_temp", | ||
"Outdoor air temperature", | ||
["device_id", "sensor_id"], | ||
) | ||
|
||
# расчетная температура теплоносителя | ||
# heat-transfer fluid design temperature | ||
BOILER_OTA_CS = Gauge( | ||
"zont_boiler_htf_design_temp", | ||
"Heat-transfer fluid design temperature", | ||
["device_id", "sensor_id"], | ||
) | ||
|
||
# фактическая температура теплоносителя | ||
# heat-transfer fluid actual temperature | ||
BOILER_OTA_BT = Gauge( | ||
"zont_boiler_htf_actual_temp", | ||
"Heat-transfer fluid actual temperature", | ||
["device_id", "sensor_id"], | ||
) | ||
|
||
# расчетная температура ГВС | ||
# DHW design temperature | ||
BOILER_OTA_DS = Gauge( | ||
"zont_boiler_dhw_design_temp", | ||
"Domestic hot water design temperature", | ||
["device_id", "sensor_id"], | ||
) | ||
|
||
# фактическая температура ГВС | ||
# DHW actual temperature | ||
BOILER_OTA_DT = Gauge( | ||
"zont_boiler_dhw_actual_temp", | ||
"Domestic hot water actual temperature", | ||
["device_id", "sensor_id"], | ||
) | ||
|
||
# уровень модуляции горелки | ||
# burner modulation level | ||
BOILER_OTA_RML = Gauge( | ||
"zont_boiler_rml", "Burner modulation level", ["device_id", "sensor_id"] | ||
) | ||
|
||
|
||
zapi = None | ||
zdevice = None | ||
|
||
|
||
def update_metrics() -> bool: | ||
""" | ||
Update Prometheus metrics based on recent data obtained | ||
from Zont API | ||
:return: bool - status of operation | ||
""" | ||
global zdevice | ||
|
||
voltage_inputs = zdevice.get_analog_inputs() | ||
temperature_sensors = zdevice.get_analog_temperature_sensors() | ||
boiler_adapters = zdevice.get_boiler_adapters() | ||
|
||
z3k = zdevice.data.get("io").get("z3k-state") | ||
|
||
if not z3k: | ||
logger.error( | ||
'z3k-state subtree not found for "%s" (%d})', zdevice.name, zdevice.id | ||
) | ||
return False | ||
|
||
rc = True | ||
|
||
for voltage_input in voltage_inputs: | ||
sensor_id = str(voltage_input.get("id")) | ||
sensor_name = voltage_input.get("name") | ||
try: | ||
VOLTAGE.labels(zdevice.id, sensor_id).set(z3k.get(sensor_id).get("voltage")) | ||
logger.info("%s/%s (%s) updated", zdevice.id, sensor_id, sensor_name) | ||
except Exception as e: | ||
logger.error( | ||
"%s/%s (%s) update failed: %s", | ||
zdevice.id, | ||
sensor_id, | ||
sensor_name, | ||
str(e), | ||
) | ||
rc = False | ||
|
||
for temperature_sensor in temperature_sensors: | ||
sensor_id = str(temperature_sensor.get("id")) | ||
sensor_name = temperature_sensor.get("name") | ||
try: | ||
INDOOR_AIR_TEMP.labels(zdevice.id, sensor_id).set( | ||
z3k.get(sensor_id).get("curr_temp") | ||
) | ||
logger.info("%s/%s (%s) updated", zdevice.id, sensor_id, sensor_name) | ||
except Exception as e: | ||
logger( | ||
"%s/%s (%s) update failed: %s", | ||
zdevice.id, | ||
sensor_id, | ||
sensor_name, | ||
str(e), | ||
) | ||
rc = False | ||
|
||
for boiler_adapter in boiler_adapters: | ||
adapter_id = str(boiler_adapter.get("id")) | ||
adapter_name = boiler_adapter.get("name") | ||
try: | ||
z3k_ot = z3k.get(adapter_id).get("ot") | ||
BOILER_OTA_OT.labels(zdevice.id, adapter_id).set(z3k_ot.get("ot")) | ||
BOILER_OTA_CS.labels(zdevice.id, adapter_id).set(z3k_ot.get("cs")) | ||
BOILER_OTA_BT.labels(zdevice.id, adapter_id).set(z3k_ot.get("bt")) | ||
BOILER_OTA_DS.labels(zdevice.id, adapter_id).set(z3k_ot.get("ds")) | ||
BOILER_OTA_DT.labels(zdevice.id, adapter_id).set(z3k_ot.get("dt")) | ||
BOILER_OTA_RML.labels(zdevice.id, adapter_id).set(z3k_ot.get("rml")) | ||
logger.info("%s/%s (%s) updated", zdevice.id, adapter_id, adapter_name) | ||
except Exception as e: | ||
logger.error( | ||
"%s/%s (%s) update failed: %s", | ||
zdevice.id, | ||
adapter_id, | ||
adapter_name, | ||
str(e), | ||
) | ||
rc = False | ||
|
||
return rc | ||
|
||
|
||
def initialize_zont_device() -> bool: | ||
""" | ||
Gather initial information on monitored device | ||
:return: bool - status of operation | ||
""" | ||
global zapi, zdevice | ||
|
||
try: | ||
zapi = ZontAPI() | ||
zdevice = zapi.get_devices()[0] | ||
|
||
except ZontAPIException as e: | ||
logger.error("error while initializing ZontAPI: %s", str(e)) | ||
return False | ||
|
||
except IndexError: | ||
logger.error("no devices found") | ||
return False | ||
|
||
logger.info('found device "%s" (%d)', zdevice.name, zdevice.id) | ||
return True | ||
|
||
|
||
def update_zont_data(): | ||
""" | ||
Auxiliary function that is intended to be called periodically | ||
from a scheduler with a goal of polling fresh device data from | ||
Zont API and update Prometheus metrics | ||
""" | ||
global zdevice | ||
|
||
if not zdevice.update_info(): | ||
logger.error('failed to update data for "%s" (%d)', zdevice.name, zdevice.id) | ||
return | ||
|
||
logger.info( | ||
'refreshed data for "%s" (%d) as of %s (%d seconds ago)', | ||
zdevice.name, | ||
zdevice.id, | ||
zdevice.last_seen.strftime("%Y-%m-%d %H:%M:%S"), | ||
zdevice.last_seen_relative, | ||
) | ||
|
||
if not update_metrics(): | ||
logger.error('failed to update metrics for "%s" (%d)', zdevice.name, zdevice.id) | ||
return | ||
|
||
|
||
def main(): | ||
""" | ||
Entrypoint function | ||
""" | ||
|
||
if not initialize_zont_device(): | ||
logger.error("failed to initialize device") | ||
sys.exit(1) | ||
|
||
if not update_metrics(): | ||
logger.error("failed to do initial metrics update") | ||
sys.exit(1) | ||
|
||
scheduler = BackgroundScheduler() | ||
scheduler.add_job(update_zont_data, "interval", seconds=60) | ||
scheduler.start() | ||
|
||
app.run(host="0.0.0.0", port=6000) | ||
|
||
|
||
@app.route("/") | ||
def default(): | ||
""" | ||
Placeholder for the default route | ||
""" | ||
return "Try /metrics!\n" | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
Oops, something went wrong.