commit b76b7e2b66aa0cbc2391c92b5fcdfe4f63c32fcb Author: Max Date: Fri Mar 22 15:20:53 2024 +0100 Initial version diff --git a/README.md b/README.md new file mode 100644 index 0000000..938c1b2 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# SolarEdge Cloud API Exporter + +## Configuration + +Edit the script and put your API key, site id and inverter serial number +Edit the listening host and port if needed + +## Usage + +Run the script as a service, add the URL to prometheus. diff --git a/solaredge.py b/solaredge.py new file mode 100755 index 0000000..b6a2b8d --- /dev/null +++ b/solaredge.py @@ -0,0 +1,145 @@ +#!/usr/bin/python3 + +import bottle +import datetime +import requests +from collections import defaultdict + +api_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" +site_id = 4143190 + +inverter = '7B0E5700-E0' + +endpoint = "https://monitoringapi.solaredge.com/" + +delta = datetime.timedelta(minutes=20) + +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": ["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"] +} + +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() + +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) + +def meters(**kw): + return get(f"site/{site_id}/meters.json", kw) + +def latest(method, **kw): + end = datetime.datetime.now() + start = end - delta + return method(startTime=time(start), endTime=time(end), **kw) + +def collect(): + tech = latest(tech_data)['data']['telemetries'] + ms = latest(meters)['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', {'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')) + + for m in ms: + point = m['values'][-1] + date = ptime(point['date']) + yield (date, 'meter_energy_watthours', {'meter': m['meterType'].lower()}, point['value']) + +def format_metrics(entries): + 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 = time.timestamp() + attr_s = "" + if attrs: + attr_s = "{" + ','.join('{}="{}"'.format(k, attrs[k]) for k in attrs ) + "}" + yield f"{metric} {attr_s} {value} {time_s}\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()