From 8d805067e61536584f7969ba488818420c42ecf6 Mon Sep 17 00:00:00 2001 From: Oliver Simon Date: Thu, 3 Nov 2022 12:05:52 +0000 Subject: [PATCH] initial commit for prometheus exporter --- tools/rpi/README.md | 52 +++++++++++++ tools/rpi/ahoy.yml.example | 5 ++ .../docker-supplementals/docker-compose.yml | 38 ++++++++++ .../prometheus/prometheus.yml | 30 ++++++++ tools/rpi/hoymiles/__main__.py | 16 +++- tools/rpi/hoymiles/outputs.py | 75 +++++++++++++++++++ tools/rpi/optional-requirements.txt | 1 + tools/rpi/systemd/hoymiles.service | 14 ++++ 8 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 tools/rpi/docker-supplementals/docker-compose.yml create mode 100644 tools/rpi/docker-supplementals/prometheus/prometheus.yml create mode 100644 tools/rpi/systemd/hoymiles.service diff --git a/tools/rpi/README.md b/tools/rpi/README.md index 1027ccb0..63fa5703 100644 --- a/tools/rpi/README.md +++ b/tools/rpi/README.md @@ -171,7 +171,59 @@ Use basic command line tools to get an idea what you recorded. For example: A brief example log is supplied in the `example-logs` folder. +Prometheus Exporter +------------------- +Python exporter for https://prometheus.io/ +install requirements: + +``` +pip install -r optional-requirements.txt +``` + +Systemd service: +---------------- + +in systemd you find a service file to (fe) run the exporter as a systemd service at boot time. + +Adjust the user in it and copy it to /lib/systemd/system + +Activate it: + +``` + sudo systemctl enable hoymiles.service +``` + +Start it + +``` + sudo service hoymiles start +``` + +After that, you should be able to open a website to http:/// and see some stats + +``` +# HELP python_gc_objects_collected_total Objects collected during gc +# TYPE python_gc_objects_collected_total counter +python_gc_objects_collected_total{generation="0"} 304.0 +python_gc_objects_collected_total{generation="1"} 124.0 +python_gc_objects_collected_total{generation="2"} 0.0 +``` +and on the bottom : + +``` +string_voltage{panel="panel_3"} 39.4 +# HELP string_current DC/Panel current +# TYPE string_current gauge +string_current{panel="panel_3"} 5.73 +# HELP string_power DC/Panel power +# TYPE string_power gauge +string_power{panel="panel_3"} 225.4 +# HELP string_energy_daily DC/Panel energy_daily +# TYPE string_energy_daily gauge +string_energy_daily{panel="panel_3"} 487.0 +``` + + more stats Todo diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index 644508e5..870b442f 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -19,6 +19,11 @@ ahoy: useTLS: False insecureTLS: False #set True for e.g. self signed certificates. + # adding prometheus exporter + prometheus: + disabled: false + port: 9233 + # Influx2 output influxdb: disabled: true diff --git a/tools/rpi/docker-supplementals/docker-compose.yml b/tools/rpi/docker-supplementals/docker-compose.yml new file mode 100644 index 00000000..308ef07d --- /dev/null +++ b/tools/rpi/docker-supplementals/docker-compose.yml @@ -0,0 +1,38 @@ +version: '3.3' + +services: + prometheus: + container_name: prometheus + image: prom/prometheus + volumes: + - ./prometheus:/etc/prometheus + networks: + hoymiles_net: + ipv4_address: 172.18.0.3 + ports: + - 9090:9090 + restart: always + + grafana: + container_name: grafana + image: grafana/grafana + ports: + - 3000:3000 + restart: always + volumes: + - grafana-storage:/var/lib/grafana + networks: + hoymiles_net: + ipv4_address: 172.18.0.2 + +volumes: + data: + grafana-storage: + +networks: + hoymiles_net: + ipam: + driver: default + config: + - subnet: "172.18.0.0/24" + diff --git a/tools/rpi/docker-supplementals/prometheus/prometheus.yml b/tools/rpi/docker-supplementals/prometheus/prometheus.yml new file mode 100644 index 00000000..36a35f27 --- /dev/null +++ b/tools/rpi/docker-supplementals/prometheus/prometheus.yml @@ -0,0 +1,30 @@ +global: + scrape_interval: 15s + scrape_timeout: 10s + evaluation_interval: 15s +alerting: + alertmanagers: + - static_configs: + - targets: [] + scheme: http + timeout: 10s + api_version: v1 +scrape_configs: + - job_name: prometheus + honor_timestamps: true + scrape_interval: 15s + scrape_timeout: 10s + metrics_path: /metrics + scheme: http + static_configs: + - targets: + - localhost:9090 + - job_name: hoymiles + honor_timestamps: true + scrape_interval: 15s + scrape_timeout: 10s + metrics_path: / + scheme: http + static_configs: + - targets: + - 172.18.0.1:9233 diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 0b558073..60ef11fc 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -135,6 +135,9 @@ def poll_inverter(inverter, do_init, retries=4): if volkszaehler_client: volkszaehler_client.store_status(result) + if prometheus_client: + prometheus_client.store_status(result) + def mqtt_send_status(broker, inverter_ser, data, topic=None): """ Publish StatusResponse object @@ -285,6 +288,13 @@ if __name__ == '__main__': bucket=influx_config.get('bucket', None), measurement=influx_config.get('measurement', 'hoymiles')) + prometheus_client = None + prometheus_config = ahoy_config.get('prometheus', {}) + if prometheus_config and not prometheus_config.get('disabled', False): + from .outputs import PrometheusOutputPlugin + prometheus_client = PrometheusOutputPlugin( + prometheus_config) + volkszaehler_client = None volkszaehler_config = ahoy_config.get('volkszaehler', {}) if volkszaehler_config and not volkszaehler_config.get('disabled', False): @@ -322,10 +332,8 @@ if __name__ == '__main__': print('', end='', flush=True) - time_to_sleep = loop_interval - (time.time() - t_loop_start) - - if loop_interval > 0 and time_to_sleep > 0: - time.sleep(time_to_sleep) + if loop_interval > 0 and (time.time() - t_loop_start) < loop_interval: + time.sleep(loop_interval - (time.time() - t_loop_start)) except KeyboardInterrupt: sys.exit() diff --git a/tools/rpi/hoymiles/outputs.py b/tools/rpi/hoymiles/outputs.py index 7223a287..84ab278b 100644 --- a/tools/rpi/hoymiles/outputs.py +++ b/tools/rpi/hoymiles/outputs.py @@ -8,6 +8,7 @@ Hoymiles output plugin library import socket from datetime import datetime, timezone from hoymiles.decoders import StatusResponse +from os import path try: from influxdb_client import InfluxDBClient @@ -36,6 +37,78 @@ class OutputPluginFactory: """ raise NotImplementedError('The current output plugin does not implement store_status') +try: + from os import path + import yaml + from prometheus_client.core import GaugeMetricFamily, REGISTRY, CounterMetricFamily + from prometheus_client import CollectorRegistry, Gauge + from prometheus_client import Info + from prometheus_client import start_http_server +except ModuleNotFoundError: + pass + +class PrometheusOutputPlugin(OutputPluginFactory): + current_data = None + + def __init__(self, config, **params): + super().__init__(**params) + + start_http_server(config.get('port')) + REGISTRY.register(self) + + def collect(self): + if self.current_data: + yield CounterMetricFamily('energy_total', 'Energy Total', value=self.current_data['energy_total']) + yield GaugeMetricFamily('temperature', 'Device Temperature', value=self.current_data['temperature']) + yield GaugeMetricFamily('pf', 'Power Factor', value=self.current_data['powerfactor']) + yield GaugeMetricFamily('frequency', 'Frequency', value=self.current_data['frequency']) + + # AC Data + phase_id = 0 + phase_gauge = {} + phase_types = [ 'power', 'voltage', 'current' ] + for phase_type in phase_types: + for phase in self.current_data['phases']: + phase_gauge[phase_type] = GaugeMetricFamily(f'phase_{phase_type}', f'AC/Phase {phase_id} Power', labels=['phase']) + phase_gauge[phase_type].add_metric(f'phase_{phase_id}', phase[phase_type]) + yield phase_gauge[phase_type] + phase_id = phase_id + 1 + + # DC Data + string_id = 0 + types = [ "voltage", "current", "power", "energy_daily" ] + + gauge={} + for string in self.current_data['strings']: + gauge['energy_total'] = CounterMetricFamily(f'string_energy_total', f'DC/Panel energy_total', labels=['panel']) + gauge['energy_total'].add_metric([f'panel_{string_id}'], string['energy_total']) + yield gauge['energy_total'] + for type in types: + gauge[type] = GaugeMetricFamily(f'string_{type}', f'DC/Panel {type}', labels=['panel']) + gauge[type].add_metric([f'panel_{string_id}'], string[type]) + yield gauge[type] + string_id = string_id + 1 + + def store_status(self, response, **params): + """ + Publish StatusResponse object + + :param hoymiles.decoders.StatusResponse response: StatusResponse object + :param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) + :type topic: str + + :raises ValueError: when response is not instance of StatusResponse + """ + + if not isinstance(response, StatusResponse): + raise ValueError('Data needs to be instance of StatusResponse') + + data = response.__dict__() + + self.current_data = response.__dict__() + serial = data["inverter_ser"] + self.collect() + class InfluxOutputPlugin(OutputPluginFactory): """ Influx2 output plugin """ api = None @@ -311,3 +384,5 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory): output.store_status(data, self.session) except ValueError as e: print('Could not send data to volkszaehler instance: %s' % e) + + diff --git a/tools/rpi/optional-requirements.txt b/tools/rpi/optional-requirements.txt index 19297f02..5e15daca 100644 --- a/tools/rpi/optional-requirements.txt +++ b/tools/rpi/optional-requirements.txt @@ -1 +1,2 @@ influxdb-client>=1.28.0 +prometheus_client diff --git a/tools/rpi/systemd/hoymiles.service b/tools/rpi/systemd/hoymiles.service new file mode 100644 index 00000000..cea0bbaa --- /dev/null +++ b/tools/rpi/systemd/hoymiles.service @@ -0,0 +1,14 @@ +[Unit] +Description=Hoymiles Exporter +After=network.target + +[Service] +WorkingDirectory=/home/rpi/hoymiles_exporter +ExecStart=/usr/bin/bash -c 'cd /home/rpi/hoymiles_exporter ; /usr/bin/python3 -um hoymiles --config ahoy.yml' +User=rpi +KillMode=process +Restart=on-failure + +[Install] +WantedBy=multi-user.target +Alias=hoymiles.service