solaredge_exporter/solaredge.py

180 lines
5.9 KiB
Python
Executable File

#!/usr/bin/python3
import bottle
import datetime
import logging
import requests
from collections import defaultdict
from sys import stderr
api_key = "ZYR991B0ZO9S530WW21B0RRNYBJGO482"
site_id = 4143190
inverter = '7B0E5700-E0'
endpoint = "https://monitoringapi.solaredge.com/"
query_delta = datetime.timedelta(minutes=20)
freshness_delta = datetime.timedelta(minutes=20)
cache = {}
numeric_modes = {
'OFF': 0,
'SLEEPING': 1,
'STARTING': 2,
'MPPT': 3,
'SHUTTING_DOWN': 4,
'FAULT': 5,
'STANDBY': 6,
'LOCKED': 7,
}
metric_data = {
"dc_voltage_volts": ["gauge", "Input bus voltage"],
"meter_energy_watthours_total": ["counter", "Power meter"],
"temperature_celsius": ["gauge", "Temperature"],
"inverter_mode": ["gauge", "Inverter state (0=off, 3=producing, 5=fault)"],
"ac_phase_volts": ["gauge", "Output bus phase voltage"],
"ac_current_amps": ["gauge", "Output bus current"],
"ac_voltage_volts": ["gauge", "Output bus voltage"],
'ac_frequency_hertz': ["gauge", "Output bus frequency"],
'ac_apparent_power_watts': ["gauge", "Apparent AC power"],
'ac_active_power_watts': ["gauge", "Active AC power"],
'ac_reactive_power_watts': ["gauge", "Reactive AC power"],
'ac_cos_phi': ["gauge", "AC Phase factor"]
}
last_ts = {}
def is_fresh(date):
return (datetime.datetime.now()) - date < freshness_delta
def numeric_mode(s):
if s in numeric_modes:
return numeric_modes[s]
if s.startswith('LOCKED'):
return 7
def time(t):
return t.strftime("%F %T")
def ptime(s):
return datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
def get(path, params={}, **kw):
r = requests.get(endpoint + path, params | { "api_key": api_key }, **kw)
r.raise_for_status()
return r.json() | { 'fetch_time': datetime.datetime.now().timestamp() }
def details():
return get(f"site/{site_id}/details.json")
def inventory():
return get(f"site/{site_id}/inventory.json")
def get_inverters():
return [ i['SN'] for i in inventory()['Inventory']['inverters'] ]
def energy(**kw):
return get(f"site/{site_id}/energyDetails.json",
kw | {"timeUnit": "QUARTER_OF_AN_HOUR"})
def tech_data(**kw):
return get(f"equipment/{site_id}/{inverter}/data.json", kw | {"timeUnit": "QUARTER_OF_AN_HOUR"})
def meters(**kw):
return get(f"site/{site_id}/meters.json", kw | {"timeUnit": "QUARTER_OF_AN_HOUR"})
def flow(**kw):
return get(f"site/{site_id}/currentPowerFlow.json", kw)
def latest(method, timefn, **kw):
name = method.__name__
if name in cache and is_fresh(timefn(cache[name])):
logging.info("Cache for {} is fresh", method.__name__)
return cache[name]
logging.info("Cache for {} is stale, requesting", method.__name__)
end = datetime.datetime.now()
start = end - query_delta
data = method(startTime=time(start), endTime=time(end), **kw)
cache[name] = data
return data
def tech_check(c):
if c['data']['telemetries']:
return ptime(c['data']['telemetries'][-1]['date'])
else:
return datetime.datetime.fromtimestamp(c['fetch_time'])
def meters_check(c):
return ptime(c['meterEnergyDetails']['meters'][0]['values'][-1]['date'])
def collect():
tech = latest(tech_data, tech_check)['data']['telemetries']
ms = latest(meters, meters_check)['meterEnergyDetails']['meters']
if len(tech):
point = tech[-1]
date = ptime(point['date'])
yield (date, 'dc_voltage_volts', {}, point.get('dcVoltage'))
yield (date, 'meter_energy_watthours_total', {'meter': 'production'}, point.get('totalEnergy'))
yield (date, 'temperature_celsius', {}, point.get('temperature'))
yield (date, 'inverter_mode', {}, numeric_mode(point.get('inverterMode')))
yield (date, 'ac_phase_volts', {'phase':'1-2'}, point.get('vL1To2'))
yield (date, 'ac_phase_volts', {'phase':'2-3'}, point.get('vL2To3'))
yield (date, 'ac_phase_volts', {'phase':'3-1'}, point.get('vL3To1'))
for p in ('L1Data','L2Data','L3Data'):
phase = point[p]
yield (date, 'ac_current_amps', {'phase': p[1:2] }, phase.get('acCurrent'))
yield (date, 'ac_voltage_volts', {'phase': p[1:2] }, phase.get('acVoltage'))
yield (date, 'ac_frequency_hertz', {'phase': p[1:2] }, phase.get('acFrequency'))
yield (date, 'ac_apparent_power_watts', {'phase': p[1:2] }, phase.get('apparentPower'))
yield (date, 'ac_active_power_watts', {'phase': p[1:2] }, phase.get('activePower'))
yield (date, 'ac_reactive_power_watts', {'phase': p[1:2] }, phase.get('reactivePower'))
yield (date, 'ac_cos_phi', {'phase': p[1:2] }, phase.get('cosPhi'))
else:
yield (datetime.datetime.now(), 'inverter_mode', {}, 1)
for m in ms:
point = m['values'][-1]
date = ptime(point['date'])
yield (date, 'meter_energy_watthours_total', {'meter': m['meterType'].lower()}, point['value'])
def format_metrics(entries, timestamps=False):
collected = defaultdict(list)
for entry in entries:
collected[entry[1]].append(entry)
for metric in collected:
md = metric_data.get(metric)
if md is not None:
yield "# TYPE {} {}\n".format(metric, md[0])
yield "# HELP {} {}\n".format(metric, md[1])
for (time, metric, attrs, value) in collected[metric]:
time_s = int(time.timestamp() * 1000)
attr_s = ""
if attrs:
attr_s = "{" + ','.join('{}="{}"'.format(k, attrs[k]) for k in attrs ) + "}"
if timestamps:
yield f"{metric} {attr_s} {value} {time_s}\n"
else:
yield f"{metric} {attr_s} {value}\n"
@bottle.route('/')
def root():
return "SolarEdge prometheus exporter"
@bottle.route('/metrics')
def metrics():
return format_metrics(collect())
def main():
bottle.run(host='localhost', port=9150)
if __name__ == '__main__':
main()