Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Prometheus exporter #35

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ GivTCP/settings.py
MQTT_TEST.py
REST_TEST.py
test.py
*.pyc
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,6 @@ ENV PALM_WEIGHT=35
ENV LOAD_HIST_WEIGHT="1"


EXPOSE 6345 1883 3000 6379 9181
EXPOSE 6345 1883 3000 6379 6711 9181

CMD ["python3", "/app/startup.py"]
100 changes: 100 additions & 0 deletions GivTCP/prometheus_exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import argparse
import json
import re
import time

from prometheus_client import start_http_server
from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily, REGISTRY

import read as rd
from GivLUT import GivLUT

logger = GivLUT.logger


class GivTcpCollector:
"""
Custom Prometheus exporter for GivTCP.
"""

def collect(self):
"""
Gather metrics from GivTCP.
"""
logger.info("Collecting metrics for export")

battery_temp_re = re.compile(r"^Battery_Cell_(?P<cell>[0-9]+)_Temperature$")
result = json.loads(rd.pubFromPickle())

if "Battery_Details" in result:
battery_labels = ["Serial"]
battery_capacity_total_gauge = GaugeMetricFamily("battery_capacity_total", "Battery capacity total", labels=battery_labels)
battery_capacity_remaining_gauge = GaugeMetricFamily("battery_capacity_remaining", "Battery capacity remaining", labels=battery_labels)
battery_capacity_percentage_gauge = GaugeMetricFamily("battery_capacity_percentage", "Battery capacity remaining as a percentage", labels=battery_labels)
battery_cell_temperature_gauge = GaugeMetricFamily("battery_cell_temperature", "Battery cell temperature", labels=["Serial", "Cell"])
battery_voltage_gauge = GaugeMetricFamily("battery_voltage", "Battery voltage", labels=battery_labels)
battery_temperature_gauge = GaugeMetricFamily("battery_temperature", "Battery temperature", labels=battery_labels)
battery_cycles_counter = CounterMetricFamily("battery_cyles", "Number of battery cycle", labels=battery_labels)

for serial, battery in result["Battery_Details"].items():
capacity_percent = battery["Battery_Remaining_Capacity"] / battery["Battery_Design_Capacity"]
if capacity_percent > 1:
capacity_percent = 1
battery_cycles_counter.add_metric([serial], battery["Battery_Cycles"])
battery_capacity_total_gauge.add_metric([serial], battery["Battery_Design_Capacity"])
battery_capacity_remaining_gauge.add_metric([serial], battery["Battery_Remaining_Capacity"])
battery_capacity_percentage_gauge.add_metric([serial], f"{capacity_percent:.03f}")
battery_temperature_gauge.add_metric([serial], battery["Battery_Temperature"])
battery_voltage_gauge.add_metric([serial], battery["Battery_Voltage"])

for key, value in battery.items():
temp_match = battery_temp_re.match(key)
if temp_match:
cell = temp_match.group("cell")
battery_cell_temperature_gauge.add_metric([serial, cell], value)

yield battery_cycles_counter
yield battery_capacity_total_gauge
yield battery_capacity_remaining_gauge
yield battery_capacity_percentage_gauge
yield battery_cell_temperature_gauge
yield battery_temperature_gauge
yield battery_voltage_gauge

if "Invertor_Details" in result:
yield GaugeMetricFamily("invertor_temperature", "Invertor temperature", value=result["Invertor_Details"]["Invertor_Temperature"])

if "Power" in result:
if "Flows" in result["Power"]:
power_flow_gauge = GaugeMetricFamily("power_flow", "Power flow", labels=["Flow"])
for flow, value in result["Power"]["Flows"].items():
power_flow_gauge.add_metric([flow], value)
yield power_flow_gauge

if "Power" in result["Power"]:
yield GaugeMetricFamily("charge_time_remaining", "Charge time remaining", value=result["Power"]["Power"]["Charge_Time_Remaining"])
yield GaugeMetricFamily("discharge_time_remaining", "Discharge time remaining", value=result["Power"]["Power"]["Discharge_Time_Remaining"])
yield GaugeMetricFamily("grid_voltage", "Grid voltage", value=result["Power"]["Power"]["Grid_Voltage"])
yield GaugeMetricFamily("soc", "State of charge", value=result["Power"]["Power"]["SOC"])

power_gauage = GaugeMetricFamily("power", "Power", labels=["Metric"])
power_re = re.compile(r"^(?P<metric>[\w]+)_Power$")

for key, value in result["Power"]["Power"].items():
power_match = power_re.match(key)
if power_match:
metric = power_match.group("metric")
power_gauage.add_metric([metric], value)

yield power_gauage

if __name__ == "__main__":

parser = argparse.ArgumentParser(description='Provides Prometheus metrics for GivTCP', add_help=True)
parser.add_argument('-p', '--port', help='Specify the port to listen on', dest='port', type=int, default=9111)
args = parser.parse_args()

start_http_server(args.port)
REGISTRY.register(GivTcpCollector())
while True:
time.sleep(1)
4 changes: 4 additions & 0 deletions GivTCP/settings_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class GiV_Settings:
influxBucket="GivEnergy" #Optional - name of Bucket to put data into
influxOrg="GivTCP" #Optional - Influx Organisation to use

# Prometheus Exporter Settings
Prometheus_Exporter= False #Optional - turns on the prometheus exporter. True or False
Prometheus_Port=6711 #Optional - the starting port number to listen on for the first invertor. Subsequent inverters will listen on the next port number in sequence.

# Home Assistant
HA_Auto_D=True #Optional - Bool - Publishes Home assistant MQTT Auto Discovery messages to push data into HA automagically (requires MQTT to be enabled below)
ha_device_prefix="" #Required - This is the prefix used at the start of every Home Assistant device created
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ This will populate HA with all devices and entities for control and monitoring.
| MQTT_TOPIC | GivEnergy/Data | Optional - default is Givenergy.<serial number>|
| MQTT_TOPIC_2 | GivEnergy/Data | Optional - Setting for second Inverter if configured. default is Givenergy.<serial number>|
| MQTT_TOPIC_2 | GivEnergy/Data | Optional - Setting for third Inverter if configured. default is Givenergy.<serial number>|
| PROMETHEUS_EXPORTER | True | Optional - Set to True to enable the prometheus exporter. default is False.|
| PROMETHEUS_PORT | 6711 | The port number to listen on for the first inverter. Subsequent inverters will be 6712 and so on.|
| LOG_LEVEL | Error | Optional - you can choose Error, Info or Debug. Output will be sent to the debug file location if specified, otherwise it is sent to stdout|
| DEBUG_FILE_LOCATION | /usr/pi/data | Optional |
| PRINT_RAW | False | Optional - If set to True the raw register values will be returned alongside the normal data |
Expand Down Expand Up @@ -151,3 +153,20 @@ Root topic for control is:
GivTCP provides a wrapper function REST.py which uses Flask to expose the read and control functions as RESTful http calls. To utilise this service you will need to either use a WSGI service such as gunicorn or use the pre-built Docker container.

If Docker is running in Host mode then the REST service is available on port 6345

### Prometheus Exporter
To make use of the Prometheus exporter you will need to have a [Prometheus](https://prometheus.io) server configured.

By setting `PROMETHEUS_EXPORTER` to `True` the `prometheus_exporter.py` script will be executed for each inverter, with the first listening on the value of `PROMETHEUS_PORT`.

You can see the metrics exported by visiting http://localhost:6711/metrics

You will need to update your Prometheus server's configuration to scrape the metrics from the exporter. An example is shown below:

```yaml
- job_name: 'giv_energy'
static_configs:
- targets: [ 'localhost:6711' ]
```

You can then visualise the metrics using [Grafana](https://grafana.com/) for example.
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ services:
- "3000:3000" # This should match the WEB_DASH_PORT ENV below
- "6379:6379" # Redis port for job queueing
- "9181:9181" # RQ Dashboard to view job scheduling
- "6711:6711" # Prometheus exporter port(s)
environment:
## Critical to update
- NUMINVERTORS=1 # Set this to the number of Inverters in your setup, then replicate the next two lines for each inverter (changing the last number of the ENV)
Expand Down Expand Up @@ -62,5 +63,7 @@ services:
- PALM_BATT_UTILISATION=0.85 # Usable proportion of battery (100% less reserve and any charge limit) on a scale of 0-1
- PALM_WEIGHT=35 # Weighting used for final target soc
- LOAD_HIST_WEIGHT=1 # Load History Weighting
- PROMETHEUS_EXPORTER=False # Set to True to enable the Prometheus exporter
- PROMETHEUS_PORT=6711 # Set the port for the first Prometheus exporter to listen on for the first inverter and so on
restart: always
privileged: true
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ pickle-mixin
flask-cors
rq
redis
prometheus-client
git+https://github.com/jace/rq-dashboard.git
15 changes: 15 additions & 0 deletions startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
selfRun={}
mqttClient={}
gunicorn={}
prometheusExporter={}
webDash={}
rqWorker={}
redis={}
Expand Down Expand Up @@ -132,6 +133,9 @@ def palm_job():
else:
outp.write(" MQTT_Topic=\""+str(os.getenv("MQTT_TOPIC_"+str(inv),"")+"\"\n"))

outp.write(" Prometheus_Exporter="+str(os.getenv("PROMETHEUS_EXPORTER"))+"\n")
outp.write(" Prometheus_Port="+str(os.getenv("PROMETHEUS_PORT"))+"\n")

outp.write(" Log_Level=\""+str(os.getenv("LOG_LEVEL","")+"\"\n"))
#setup debug filename for each inv
outp.write(" Influx_Output="+str(os.getenv("INFLUX_OUTPUT",""))+"\n")
Expand Down Expand Up @@ -209,6 +213,11 @@ def palm_job():
if os.getenv('MQTT_OUTPUT')=="True" or isAddon:
logger.critical ("Subscribing MQTT Broker for control")
mqttClient[inv]=subprocess.Popen(["/usr/local/bin/python3",PATH+"/mqtt_client.py"])
if (os.getenv('PROMETHEUS_EXPORTER')=="True" and os.getenv('PROMETHEUS_PORT') is not None) or isAddon:
PROMETHEUS_PORT = str(int(os.getenv("PROMETHEUS_PORT"))+inv-1)
logger.critical("Creating prometheus metric exporter on port "+PROMETHEUS_PORT)
command=shlex.split("/usr/local/bin/python3 "+PATH+"/prometheus_exporter.py -p " + PROMETHEUS_PORT)
prometheusExporter[inv]=subprocess.Popen(command)

GUPORT=6344+inv
logger.critical ("Starting Gunicorn on port "+str(GUPORT))
Expand Down Expand Up @@ -263,6 +272,12 @@ def palm_job():
logger.critical ("Starting Gunicorn on port "+str(GUPORT))
command=shlex.split("/usr/local/bin/gunicorn -w 3 -b :"+str(GUPORT)+" REST:giv_api")
gunicorn[inv]=subprocess.Popen(command)
elif (os.getenv('PROMETHEUS_EXPORTER')=="True" and os.getenv('PROMETHEUS_PORT') is not None) and prometheusExporter[inv].poll() is not None:
PROMETHEUS_PORT = str(int(os.getenv("PROMETHEUS_PORT"))+inv-1)
os.chdir(PATH)
logger.critical("Prometheus died, creating prometheus metric exporter on port "+PROMETHEUS_PORT)
command=shlex.split("/usr/local/bin/python3 "+PATH+"/prometheus_exporter.py -p " + PROMETHEUS_PORT)
prometheusExporter[inv]=subprocess.Popen(command)
if os.getenv('MQTT_ADDRESS')=="127.0.0.1" and not mqttBroker.poll()==None:
logger.error("MQTT Broker process died. Restarting...")
os.chdir(PATH)
Expand Down