diff --git a/Dockerfile b/Dockerfile index c7d5c22..85c8eb4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ FROM python:3.8-alpine +ENV PYTHONUNBUFFERED=1 WORKDIR /app COPY /src/exporter.py . -RUN pip install --upgrade goodwe asyncio prometheus_client +RUN pip install --upgrade goodwe asyncio aiohttp prometheus_client ENTRYPOINT ["python", "exporter.py"] diff --git a/README.md b/README.md index 3382340..c9fd457 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,14 @@ pip install goodwe==0.2.23 ### Run/test -3. to test, start the exporter: +3. to test, start the exporter with minimal configuration: ``` python exporter.py --port --interval --inverter ie. python exporter.py --port 8787 --interval 30 --inverter 192.168.2.35 ``` +(for more settings, see parameters below)
now you can call it via curl to see,if it exports some metrics: @@ -86,7 +87,9 @@ curl http://:8787 ### Supported parameters -`--inverter ` - [required] IP address of the iverter. To get the IP Address, you can run the `inverter_scan.py` script.
+`--inverter ` - [required] IP address of the iverter. To get the IP Address, you can run the 'inverter_scan.py' script.
`--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.15] energy price per kwh (in eur )
+`--energy-price ` - [optional][default: 0.15] 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 = 5670W)
+`--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 spot_price as the energy price
diff --git a/docker-compose.yml b/docker-compose.yml index f82ce38..d2f0f64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,9 @@ services: - "--port=8787" - "--interval=30" - "--inverter=192.168.1.10" + - "--energy-price=0.15" + - "--PVpower=5.67" + - "--scrape-spot-price=False" networks: - internal restart: unless-stopped diff --git a/src/exporter.py b/src/exporter.py index e365c45..80d4f0a 100644 --- a/src/exporter.py +++ b/src/exporter.py @@ -1,15 +1,37 @@ from prometheus_client import CollectorRegistry, Gauge, Counter, Info -from datetime import datetime +from datetime import date, datetime +from decimal import Decimal import prometheus_client as prometheus +import xml.etree.ElementTree as ET +import logging import sys import getopt import time -import socket -import urllib.request import asyncio +import aiohttp import goodwe -print("\nGOODWE DATA EXPORTER v1.2.0\n") +#logger = logging.getLogger(__name__) + +print("\nGOODWE DATA EXPORTER v1.3.0\n") + +QUERY = ''' + + + + + {start} + {end} + {in_eur} + + + +''' +class OTEFault(Exception): + pass + +class InvalidFormat(OTEFault): + pass def checkArgs(argv): @@ -18,19 +40,21 @@ def checkArgs(argv): global INVERTER_IP global ENERGY_PRICE global PV_POWER + global SCRAPE_SPOT_PRICE # set default values EXPORTER_PORT = 8787 POLLING_INTERVAL = 30 - ENERGY_PRICE = 0.15 + ENERGY_PRICE = 0 PV_POWER = 5670 INVERTER_IP = "" + SCRAPE_SPOT_PRICE = False # help - arg_help = "{0} --port --interval --inverter --energy-price --PVpower 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: + async with aiohttp.ClientSession() as session: + async with session.post(self.ELECTRICITY_PRICE_URL, data=query) as response: + return await response.text() + + 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): self.POLLING_INTERVAL = POLLING_INTERVAL self.ENERGY_PRICE = ENERGY_PRICE self.PV_POWER = PV_POWER + self.SCRAPE_SPOT_PRICE = SCRAPE_SPOT_PRICE self.metricsCount = 0 - self.g = g - self.i = i + self.g = [] + self.i = [] # create placeholder for metrics in the register def collector_register(self): @@ -85,10 +137,9 @@ async def create_collector_registers(): # 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() @@ -96,10 +147,16 @@ def run_metrics_loop(self): 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: + 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) + async def fetch_inverter(): inverter = await goodwe.connect(INVERTER_IP) runtime_data = await inverter.read_runtime_data() @@ -111,17 +168,20 @@ async def fetch_inverter(): countID+=1 # set value for additional energy-price - self.g[countID].set(float(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") + 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")), end='\r') - print('\033[1A', end='\x1b[2K') - + print("last scrape:\t\t\t"+ str(datetime.now().strftime("%d.%m.%Y %H:%M:%S"))) def main(): @@ -129,15 +189,14 @@ def main(): print("polling interval:\t\t"+str(POLLING_INTERVAL)+"s") print("inverter scrape IP:\t\t"+str(INVERTER_IP)) - print("energy price: \t\t\t"+str(ENERGY_PRICE)+"eur") - print("total PV power: \t\t\t"+str(PV_POWER)+"W") + print("fixed energy price: \t\t"+str(ENERGY_PRICE)+" eur/KW") + print("total PV power: \t\t"+str(PV_POWER)+"W") inverter_metrics = InverterMetrics( POLLING_INTERVAL=int(POLLING_INTERVAL), ENERGY_PRICE=ENERGY_PRICE, PV_POWER=PV_POWER, - g=[], - i=[] + SCRAPE_SPOT_PRICE=SCRAPE_SPOT_PRICE ) # Start the server to expose metrics.