From ef6e17b19d4c1353383349f793846d184028f2d7 Mon Sep 17 00:00:00 2001 From: angoosh Date: Sun, 7 Jul 2024 10:08:19 +0200 Subject: [PATCH] added shit --- README.md | 2 +- .../goodwe-prometheus-exporter/Dockerfile | 7 + .../goodwe-prometheus-exporter/README.md | 165 ++++++++++++ .../goodwe-prometheus-exporter/build.sh | 1 + .../docker-compose.yml | 24 ++ .../goodwe-prometheus-exporter/exporter.log | 1 + .../goodwe-prometheus-exporter/push.sh | 1 + .../scripts/get-inverter-data.py | 17 ++ .../scripts/inverter_scan.py | 66 +++++ .../src/exporter.py | 250 ++++++++++++++++++ grafana/docker-compose.yml | 79 ++++++ grafana/prometheus.yml | 31 +++ 12 files changed, 643 insertions(+), 1 deletion(-) create mode 100644 docker-builds/goodwe-prometheus-exporter/Dockerfile create mode 100644 docker-builds/goodwe-prometheus-exporter/README.md create mode 100755 docker-builds/goodwe-prometheus-exporter/build.sh create mode 100644 docker-builds/goodwe-prometheus-exporter/docker-compose.yml create mode 100644 docker-builds/goodwe-prometheus-exporter/exporter.log create mode 100755 docker-builds/goodwe-prometheus-exporter/push.sh create mode 100644 docker-builds/goodwe-prometheus-exporter/scripts/get-inverter-data.py create mode 100644 docker-builds/goodwe-prometheus-exporter/scripts/inverter_scan.py create mode 100644 docker-builds/goodwe-prometheus-exporter/src/exporter.py create mode 100644 grafana/docker-compose.yml create mode 100644 grafana/prometheus.yml diff --git a/README.md b/README.md index 5484dde..5fee8eb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Metrics_sobkovice -Docker stuff and other stuff concerning metrics in Sobkovice \ No newline at end of file +Metrics stuff for Sobkovice server \ No newline at end of file diff --git a/docker-builds/goodwe-prometheus-exporter/Dockerfile b/docker-builds/goodwe-prometheus-exporter/Dockerfile new file mode 100644 index 0000000..c064bd1 --- /dev/null +++ b/docker-builds/goodwe-prometheus-exporter/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.8-alpine +ENV PYTHONUNBUFFERED=1 +WORKDIR /app +COPY /src/exporter.py . +RUN python -m pip install --upgrade pip +RUN pip install --root-user-action=ignore --upgrade goodwe asyncio aiohttp prometheus_client +ENTRYPOINT ["python", "exporter.py"] diff --git a/docker-builds/goodwe-prometheus-exporter/README.md b/docker-builds/goodwe-prometheus-exporter/README.md new file mode 100644 index 0000000..c95ceae --- /dev/null +++ b/docker-builds/goodwe-prometheus-exporter/README.md @@ -0,0 +1,165 @@ +# goodwe-prometheus-exporter +Exporter for prometheus to export metrics from GoodWe Inverter + +
+ +This exporter should be working on GoodWe ET, EH, BT, BH, ES, EM, BP, DT, MS, and D-NS families of inverters. It may work on other inverters as well, as long as they listen on UDP port 8899 and respond to one of supported communication protocols. +The inverters communicate via UDP protocol, by default running on port 8899. They use a native 'AA55' protocol and (some models) ModBus protocol. ET inverters support both protocols, some inverters may not support both of them. + +(If you can't communicate with the inverter despite your model is listed above, it is possible you have old ARM firmware version. You should ask manufacturer support to upgrade your ARM firmware (not just inverter firmware) to be able to communicate with the inveter via UDP.) + +more info about the python goodwe library: https://github.com/marcelblijleven/goodwe + +
+ +### Pre-requisites +1. Configured inverter: +Inverter must be connected to a 2.4GHz wifi network (if you have a wifi module) +If not, you can configure it following:
+ 1.1 Connect to a wifi network called `Solar-Wifi`. The default login is `admin` and password `admin`
+ note: default password is sometimes `12345678`
+ 1.2 Open your browser and go to http://10.10.100.253 or http://10.10.100.254
+ 1.3 enter `admin` as username, and `admin` as password
+ 1.4 Click `Start setup`, and select router's SSID (must be a 2.4GHz network) and it's password
+ 1.5 click `complete` to finish the setup process
+
+ +2. Installed python (tested with python 3.8, 3.9, 3.10): + +for Ubuntu: +``` +sudo apt update +sudo apt install software-properties-common +sudo add-apt-repository ppa:deadsnakes/ppa +sudo apt-get install python3.8 python3.8-dev python3.8-distutils python3.8-venv +``` + +for RHEL/CentOS: +``` +yum install python3.8 +``` + +check: +``` +python3.8 --version +``` + +
+ + +3. installed required modules for python: +``` +python -m pip install asyncio prometheus_client goodwe +``` +
+ +### How to get the IP Address of the inverter +*note: the inverter must be on the same network* + +To get the IP adress of the inverter, run: +``` +python scripts/inverter_scan.py +``` + +you will see something like:
+`Located inverter at IP: 192.168.2.35, mac: 289C6E05xxxx, name: Solar-WiFi222W0782` +
+ +### How to get test data + +Edit the file `scripts/get-inverter-data.py` and on the line #7 add the IP address of the inverter +then run it with: +``` +python scripts/get-inverter-data.py +``` + +and you should get all the data your inverter is exposing + +

+ +## For standalone installation + +check that you have: +- installed python +- installed goodwe modules +(see [Pre-requisites](https://github.com/gustonator/goodwe-prometheus-exporter#Pre-requisites) ) +
+ +### Run/test + +To test, start the exporter with minimal configuration: +``` +python src/exporter.py --port --interval --inverter + +ie. +python src/exporter.py --port 8787 --interval 30 --inverter 192.168.2.35 +``` +(for more settings, see [Supported parameters](https://github.com/gustonator/goodwe-prometheus-exporter#supported-parameters)) + +
+ +now you can call it via curl (from another terminal) to see, if it exports some metrics: +(run in a new tab) +``` +curl http://127.0.0.1:8787 +``` + +
+ +to show help, just run the script with a `--help` parameter: +``` +python src/exporter.py --help +``` + +
+ +if everything is OK, you can set up the script as a service: +For Ubuntu: + +
+ + +## For Docker Installation + +check that you have: +- installed python (need for the script to get the IP adress)(see [Pre-requisites](https://github.com/gustonator/goodwe-prometheus-exporter#Pre-requisites)) +- Installed docker compose ([Docker compose installation](https://docs.docker.com.zh.xy2401.com/v17.12/compose/install/)) +
+ + +### Install/Run +1. edit the docker-compose.yml file and put there the correct inverter IP. (other values are optional) + - To get the IP address, see section "How to get the IP Address of the inverter" + +2. from command line run: +``` +docker compose up -d +``` +
+ + +### check + +3. get IP address of the container +``` +docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' goodwe-exporter +``` +
+ +4. check via curl to see,if exporter works metrics. use the IP address from step 3. +``` +curl http://:8787 +``` +
+ +### Supported parameters + +`--inverter ` - [required] IP address of the inverter. To get the IP Address, see section [How to get the inverter IP address](https://github.com/gustonator/goodwe-prometheus-exporter#how-to-get-the-ip-address-of-the-inverter).
+`--port ` - [optional][default: 8787] port, on which the exporter should expose the metrics
+`--interval ` - [optional][default: 30] interval between scrapings (in seconds).
+`--energy-price ` - [optional][default: 0] energy price per kwh (in eur). If '--scrape-spot-price' is set to true, '--energy-price' value is ignored
+`--PVpower ` - [optional][default: 5670] maximum power in Watts of your PV you can generate (ie. 5670 = 5.67 KW)
+`--scrape-spot-price ` - [optional][default: False] True/False, if the exporter should scrape spot price from https://www.ote-cr.cz. If it's set to 'True', exporter will set the scraped spot price as the energy price (--energy-price is ignored)
+`--spot-scrape-interval ` - [optional][default: 30] (in minutes) scrape interval of spot prices. If you set it too low, ote-cr.cz will block your requests

+ + diff --git a/docker-builds/goodwe-prometheus-exporter/build.sh b/docker-builds/goodwe-prometheus-exporter/build.sh new file mode 100755 index 0000000..0c73531 --- /dev/null +++ b/docker-builds/goodwe-prometheus-exporter/build.sh @@ -0,0 +1 @@ +docker build -t gitea.angoosh.com/angoosh/goodwe-prometheus-exporter:latest . diff --git a/docker-builds/goodwe-prometheus-exporter/docker-compose.yml b/docker-builds/goodwe-prometheus-exporter/docker-compose.yml new file mode 100644 index 0000000..bce6312 --- /dev/null +++ b/docker-builds/goodwe-prometheus-exporter/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.8' + +services: + goodwe-exporter: + container_name: goodwe-exporter + build: . + ports: + - 8787:8787 + command: + - "--port=8787" + - "--interval=30" + - "--inverter=192.168.88.26" + - "--energy-price=4.5" + - "--PVpower=10000" + - "--scrape-spot-price=False" + networks: + - internal + restart: unless-stopped + +networks: + internal: + external: false + driver: bridge + diff --git a/docker-builds/goodwe-prometheus-exporter/exporter.log b/docker-builds/goodwe-prometheus-exporter/exporter.log new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docker-builds/goodwe-prometheus-exporter/exporter.log @@ -0,0 +1 @@ + diff --git a/docker-builds/goodwe-prometheus-exporter/push.sh b/docker-builds/goodwe-prometheus-exporter/push.sh new file mode 100755 index 0000000..44fcf8d --- /dev/null +++ b/docker-builds/goodwe-prometheus-exporter/push.sh @@ -0,0 +1 @@ +docker push gitea.angoosh.com/angoosh/goodwe-prometheus-exporter:latest diff --git a/docker-builds/goodwe-prometheus-exporter/scripts/get-inverter-data.py b/docker-builds/goodwe-prometheus-exporter/scripts/get-inverter-data.py new file mode 100644 index 0000000..7208460 --- /dev/null +++ b/docker-builds/goodwe-prometheus-exporter/scripts/get-inverter-data.py @@ -0,0 +1,17 @@ +import asyncio +import goodwe + + +async def get_runtime_data(): + #ip_address = '2a00:1028:c000:fdb:730b:1770:8783:6eac' + ip_address = '192.168.2.35' + + inverter = await goodwe.connect(ip_address) + runtime_data = await inverter.read_runtime_data() + + for sensor in inverter.sensors(): + if sensor.id_ in runtime_data: + print(f"{sensor.id_}: \t\t {sensor.name} = {runtime_data[sensor.id_]} {sensor.unit}") + + +asyncio.run(get_runtime_data()) diff --git a/docker-builds/goodwe-prometheus-exporter/scripts/inverter_scan.py b/docker-builds/goodwe-prometheus-exporter/scripts/inverter_scan.py new file mode 100644 index 0000000..6a6219e --- /dev/null +++ b/docker-builds/goodwe-prometheus-exporter/scripts/inverter_scan.py @@ -0,0 +1,66 @@ +"""Simple test script to scan inverter present on local network""" +import asyncio +import binascii +import logging +import sys + +import goodwe +from goodwe.exceptions import InverterError +from goodwe.protocol import ProtocolCommand + +logging.basicConfig( + format="%(asctime)-15s %(funcName)s(%(lineno)d) - %(levelname)s: %(message)s", + stream=sys.stderr, + level=getattr(logging, "INFO", None), +) + + +def try_command(command, ip): + print(f"Trying command: {command}") + try: + response = asyncio.run(ProtocolCommand(bytes.fromhex(command), lambda x: True).execute(result[0], timeout=2, retries=0)) + print(f"Response to {command} command: {response.hex()}") + except InverterError as err: + print(f"No response to {command} command") + + +def omnik_command(logger_sn): + # frame = (headCode) + (dataFieldLength) + (contrlCode) + (sn) + (sn) + (command) + (checksum) + (endCode) + frame_hdr = binascii.unhexlify('680241b1') # from SolarMan / new Omnik app + command = binascii.unhexlify('0100') + defchk = binascii.unhexlify('87') + endCode = binascii.unhexlify('16') + + # tar = bytearray.fromhex(hex(logger_sn)[8:10] + hex(logger_sn)[6:8] + hex(logger_sn)[4:6] + hex(logger_sn)[2:4]) + # frame = bytearray(frame_hdr + tar + tar + command + defchk + endCode) + frame = bytearray(frame_hdr + binascii.unhexlify(logger_sn) + command + defchk + endCode) + + checksum = 0 + frame_bytes = bytearray(frame) + for i in range(1, len(frame_bytes) - 2, 1): + checksum += frame_bytes[i] & 255 + frame_bytes[len(frame_bytes) - 2] = int((checksum & 255)) + return frame_bytes.hex() + + +result = asyncio.run(goodwe.search_inverters()).decode("utf-8").split(",") +print(f"Located inverter at IP: {result[0]}, mac: {result[1]}, name: {result[2]}") + +# EM/ES +try_command("AA55C07F0102000241", result[0]) +# DT (SolarGo) +try_command("7F03753100280409", result[0]) +# Omnik v5 ? +try_command("197d0001000dff045e50303036564657f6e60d", result[0]) +# Omnik 4 ? +sn = bytes(result[2][10:], 'utf-8').hex() +try_command(omnik_command(sn), result[0]) +# Omnik 4 reversed ? +sn = "".join(reversed([sn[i:i + 2] for i in range(0, len(sn), 2)])) +try_command(omnik_command(sn), result[0]) + +print(f"\n\nIdentifying inverter at IP: {result[0]}, name: {result[2]} mac: {result[1]}") +inverter = asyncio.run(goodwe.discover(result[0])) +print( + f"Identified inverter model: {inverter.model_name}, serialNr: {inverter.serial_number}" +) diff --git a/docker-builds/goodwe-prometheus-exporter/src/exporter.py b/docker-builds/goodwe-prometheus-exporter/src/exporter.py new file mode 100644 index 0000000..a9ff045 --- /dev/null +++ b/docker-builds/goodwe-prometheus-exporter/src/exporter.py @@ -0,0 +1,250 @@ +from prometheus_client import CollectorRegistry, Gauge, Counter, Info +from datetime import date, datetime, timedelta +from decimal import Decimal +import prometheus_client as prometheus +import xml.etree.ElementTree as ET +import traceback +import logging +import sys +import getopt +import time +import asyncio +import aiohttp +import goodwe + +#logger = logging.getLogger(__name__) + +print("\nGOODWE DATA EXPORTER v1.4.3\n") + +QUERY = ''' + + + + + {start} + {end} + {in_eur} + + + +''' +class OTEFault(Exception): + pass + +class InvalidFormat(OTEFault): + pass + + +def checkArgs(argv): + global EXPORTER_PORT + global POLLING_INTERVAL + global INVERTER_IP + global ENERGY_PRICE + global PV_POWER + global SCRAPE_SPOT_PRICE + global SPOT_SCRAPE_INTERVAL + global LAST_SPOT_UPDATE + + # set default values + EXPORTER_PORT = 8787 + POLLING_INTERVAL = 30 + ENERGY_PRICE = 0.20 + PV_POWER = 5670 + INVERTER_IP = "" + SCRAPE_SPOT_PRICE = False + SPOT_SCRAPE_INTERVAL = timedelta(minutes=int(30)) + LAST_SPOT_UPDATE = datetime.now() - SPOT_SCRAPE_INTERVAL + + # help + arg_help = "\nREQUIRED PARAMETERS::\n\t-i, --inverter\n\t\tIP adress of the inverter\n\nOPTIONAL PARAMETERS:\n\t-h, --help \n\t\tShows this menu\n\t-p, --port \n\t\texporter port - on which port should the exporter expose data [default:8787]\n\t-t, --interval\n\t\tscrape interval (in seconds) [default:30] \n\t-e. --energy-price \n\t\tprice per KWh in eur [default: 0.20] \n\t-w, --PVpower \n\t\tmaximum KW your PV can produce [default:5670] \n\t-s, --scrape-spot-price \n\t\t[True/False] Set to True, for scraping spot prices from www.ote-cr.cz [default: False] \n\t-x, --spot-scrape-interval \n\t\tscrape interval of spot prices. If you set it too low, ote-cr.cz will block your requests (in minutes) [default:30] ".format(argv[0]) + + try: + opts, args = getopt.getopt(argv[1:], "hp:t:i:s:", ["help", "port=", "interval=", "inverter=", "energy-price=", "PVpower=", "scrape-spot-price=", "spot-scrape-interval="]) + except: + print(arg_help) + sys.exit(2) + + for opt, arg in opts: + if opt in ("-h", "--help"): + print(arg_help) + sys.exit(2) + elif opt in ("-p", "--port"): + EXPORTER_PORT= arg + elif opt in ("-t", "--interval"): + POLLING_INTERVAL = arg + elif opt in ("-i", "--inverter"): + INVERTER_IP = arg + elif opt in ("-e", "--energy-price"): + ENERGY_PRICE = arg + elif opt in ("-w", "--PVpower"): + PV_POWER = arg + elif opt in ("-s", "--scrape-spot-price"): + SCRAPE_SPOT_PRICE = True + elif opt in ("-x", "--spot-scrape-interval"): + # define time for spot price scrape interval + SPOT_SCRAPE_INTERVAL = timedelta(minutes=int(arg)) + # take defined interval so it scrapes it always the 1st time + LAST_SPOT_UPDATE = datetime.now() - SPOT_SCRAPE_INTERVAL + + # check if Inverter IP is set + if not INVERTER_IP: + print("ERROR: missing IP Address of inverter!\n") + print(arg_help) + sys.exit(2) + +class InverterMetrics: + ELECTRICITY_PRICE_URL = 'https://www.ote-cr.cz/services/PublicDataService' #deleted "e" + + # build the query - fill the variables + def get_query(self, start: date, end: date, in_eur: bool) -> str: + return QUERY.format(start=start.isoformat(), end=end.isoformat(), in_eur='true' if in_eur else 'false') + + # download data from web + async def _download(self, query: str) -> str: + try: + async with aiohttp.ClientSession() as session: + async with session.get('https://www.ote-cr.cz') as response: + async with session.post(self.ELECTRICITY_PRICE_URL, data=query) as response: + return await response.text() + except aiohttp.ClientConnectorError as e: + print(f"SSL error occurred: {e}") + + def parse_spot_data(self, xmlResponse): + root = ET.fromstring(xmlResponse) + for item in root.findall('.//{http://www.ote-cr.cz/schema/service/public}Item'): + hour_el = item.find('{http://www.ote-cr.cz/schema/service/public}Hour') + price_el = item.find('{http://www.ote-cr.cz/schema/service/public}Price') + current_hour = datetime.now().hour + + if (int(hour_el.text) - 1) == current_hour: + price_el = Decimal(price_el.text) + price_el /= Decimal(1000) #convert MWh -> KWh + return price_el + + def __init__(self, POLLING_INTERVAL,ENERGY_PRICE,PV_POWER,SCRAPE_SPOT_PRICE,SPOT_SCRAPE_INTERVAL,LAST_SPOT_UPDATE): + self.POLLING_INTERVAL = POLLING_INTERVAL + self.ENERGY_PRICE = ENERGY_PRICE + self.PV_POWER = PV_POWER + self.SCRAPE_SPOT_PRICE = SCRAPE_SPOT_PRICE + self.SPOT_SCRAPE_INTERVAL = SPOT_SCRAPE_INTERVAL + self.LAST_SPOT_UPDATE = LAST_SPOT_UPDATE + self.metricsCount = 0 + self.g = [] + self.i = [] + + # create placeholder for metrics in the register + def collector_register(self): + async def create_collector_registers(): + inverter = await goodwe.connect(INVERTER_IP) + runtime_data = await inverter.read_runtime_data() + + for sensor in inverter.sensors(): + if sensor.id_ in runtime_data and type(runtime_data[sensor.id_]) == int or type(runtime_data[sensor.id_]) == float: + self.g.append(Gauge(sensor.id_, sensor.name)) + + elif sensor.id_ in runtime_data and sensor.id_ != "timestamp" and type(runtime_data[sensor.id_]) != int: + self.i.append(Info(sensor.id_, sensor.name)) + + # add additional energy-price + self.g.append(Gauge("energy_price", "Energy price per KW")) + + # add additional PV Power + self.g.append(Gauge("pv_total_power", "Total power in WATTS, that can be produced by PV")) + + asyncio.run(create_collector_registers()) + + # scrape loop + def run_metrics_loop(self): + self.collector_register() + while True: + self.fetch_data() + time.sleep(self.POLLING_INTERVAL) + + # scrape metrics in a loop and write to the prepared metrics register + def fetch_data(self): + self.metricsCount = 0 + + # get spot prices + if self.SCRAPE_SPOT_PRICE: + now = datetime.now() + # if the last spot price update was more that 30min ago, scrape it again + if now - self.LAST_SPOT_UPDATE > self.SPOT_SCRAPE_INTERVAL: + query = self.get_query(date.today(), date.today(), in_eur=True) + xmlResponse = asyncio.run(self._download(query)) + self.ENERGY_PRICE = self.parse_spot_data(xmlResponse) + self.LAST_SPOT_UPDATE = now + + async def fetch_inverter(): + inverter = await goodwe.connect(INVERTER_IP) + runtime_data = await inverter.read_runtime_data() + countID = 0 + + for sensor in inverter.sensors(): + if sensor.id_ in runtime_data and type(runtime_data[sensor.id_]) == int or type(runtime_data[sensor.id_]) == float: + self.g[countID].set(str(runtime_data[sensor.id_])) + countID+=1 + + # set value for additional energy-price + self.g[countID].set(float(self.ENERGY_PRICE)) + self.g[countID+1].set(float(PV_POWER)) + self.metricsCount=len(self.g) + + asyncio.run(fetch_inverter()) + + # print number of metrics and date and rewrites it every time + print('-------------------------------------------------------') + if self.SCRAPE_SPOT_PRICE: + print("energy price(spot):\t\t"+str(self.ENERGY_PRICE)+" eur/KW") + print("last spot price scrape:\t\t"+str(self.LAST_SPOT_UPDATE)) + else: + print("energy price (fixed):\t\t"+str(self.ENERGY_PRICE)+" eur/KW") + print("number of metrics:\t\t"+str(self.metricsCount)) + print("last scrape:\t\t\t"+ str(datetime.now().strftime("%d.%m.%Y %H:%M:%S"))) + + +def main(): + try: + # Set up logging + logging.basicConfig(filename='exporter.log', level=logging.WARNING, format='%(asctime)s %(name)-14s %(levelname)-10s %(message)s', filemode='a') + + checkArgs(sys.argv) + + print("polling interval:\t\t"+str(POLLING_INTERVAL)+"s") + print("inverter scrape IP:\t\t"+str(INVERTER_IP)) + print("total PV power: \t\t"+str(PV_POWER)+"W") + if SCRAPE_SPOT_PRICE: + print("spot price scrape: \t\tEnabled") + print("spot price scrape interval: \t"+str(SPOT_SCRAPE_INTERVAL)+" min") + else: + print("spot price scrape: \t\tDisabled") + print("fixed energy price: \t\t"+str(ENERGY_PRICE)+" eur/KW") + + inverter_metrics = InverterMetrics( + POLLING_INTERVAL=int(POLLING_INTERVAL), + ENERGY_PRICE=ENERGY_PRICE, + PV_POWER=PV_POWER, + SCRAPE_SPOT_PRICE=SCRAPE_SPOT_PRICE, + SPOT_SCRAPE_INTERVAL=SPOT_SCRAPE_INTERVAL, + LAST_SPOT_UPDATE=LAST_SPOT_UPDATE + ) + + # Start the server to expose metrics. + prometheus.start_http_server(int(EXPORTER_PORT)) + print("exporter started on port:\t"+str(EXPORTER_PORT)+"\n") + + inverter_metrics.run_metrics_loop() + + except KeyboardInterrupt: + key_message='Manually interrupted by keyboard' + print("\n"+key_message+"\n") + logging.warning(key_message) + + except Exception as e: + logging.error("An error occurred: %s", e) + traceback.print_exc() + + +if __name__ == "__main__": + main() + + diff --git a/grafana/docker-compose.yml b/grafana/docker-compose.yml new file mode 100644 index 0000000..2474b8d --- /dev/null +++ b/grafana/docker-compose.yml @@ -0,0 +1,79 @@ +services: + goodwe-exporter-garaz: + image: "gitea.angoosh.com/angoosh/goodwe-prometheus-exporter:latest" + restart: always + ports: + - 8787:8787 + command: + - "--port=8787" + - "--interval=30" + - "--inverter=192.168.88.26" + - "--energy-price=4.5" + - "--PVpower=10000" + - "--scrape-spot-price=False" + networks: + - grafana + + goodwe-exporter-bouda: + image: "gitea.angoosh.com/angoosh/goodwe-prometheus-exporter:latest" + restart: always + ports: + - 8788:8787 + command: + - "--port=8787" + - "--interval=30" + - "--inverter=192.168.88.14" + - "--energy-price=4.5" + - "--PVpower=10000" + - "--scrape-spot-price=False" + networks: + - grafana + + prometheus: + image: prom/prometheus:v2.33.5 + restart: always + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus-data:/prometheus + ports: + - 9090:9090 + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - '--storage.tsdb.retention.time=1y' + - "--web.console.libraries=/usr/share/prometheus/console_libraries" + - "--web.console.templates=/usr/share/prometheus/consoles" + - "--query.lookback-delta=40m" + networks: + - grafana + + grafana: + image: grafana/grafana-oss:9.5.20 + restart: always + volumes: + - grafana-cfg:/etc/grafana + - grafana-data:/var/lib/grafana + environment: + GF_FEATURE_TOGGLES_PUBLICDASHBOARDS: "true" + ports: + - 3001:3000 + labels: + - "traefik.enable=true" + - "traefik.http.routers.grafana.rule=Host(`grafana.angoosh.com`)" + - "traefik.http.routers.grafana.entrypoints=websecure" + - "traefik.http.routers.grafana.tls.certresolver=letsencrypt" + - "traefik.http.services.grafana.loadbalancer.server.port=3000" + networks: + - grafana + - default + +volumes: + prometheus-data: + grafana-cfg: + grafana-data: + +networks: + default: + name: gateway + external: true + grafana: diff --git a/grafana/prometheus.yml b/grafana/prometheus.yml new file mode 100644 index 0000000..5e9e453 --- /dev/null +++ b/grafana/prometheus.yml @@ -0,0 +1,31 @@ +global: + scrape_interval: 15s # By default, scrape targets every 15 seconds. + +# scrape_timeout: 15s +# evaluation_interval: 1m + + # Attach these labels to any time series or alerts when communicating with + # external systems (federation, remote storage, Alertmanager). +# external_labels: +# monitor: 'PV' + +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'prometheus' +# scrape_interval: 5s + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'PV-garaz' + scrape_interval: 5s + static_configs: + - targets: ['goodwe-exporter-garaz:8787'] + labels: + group: 'PV' + + - job_name: 'PV-bouda' + scrape_interval: 5s + static_configs: + - targets: ['goodwe-exporter-bouda:8788'] + labels: + group: 'PV'