#!/usr/bin/env python3 import argparse import base64 import os import sys import time import urllib.error import urllib.parse import urllib.request import yaml from pymodbus.exceptions import ConnectionException from solaredge_modbus_mqtt import MyInverter from solaredge_modbus_mqtt import SolarData from solaredge_modbus_mqtt import to_line_protocol SPOOL_FILE = 'spool.txt' SPOOL_MAX_LINES = 20000 # ~16h of backlog at a 3s poll interval def send_to_influx(lines, influx): url = '{0}/write?db={1}&precision=ns'.format( influx['url'].rstrip('/'), urllib.parse.quote(influx['db'], safe='') ) request = urllib.request.Request(url, data='\n'.join(lines).encode('utf-8'), method='POST') if influx.get('user'): credentials = '{0}:{1}'.format(influx['user'], influx.get('pass') or '') token = base64.b64encode(credentials.encode('utf-8')).decode('utf-8') request.add_header('Authorization', 'Basic ' + token) urllib.request.urlopen(request, timeout=5) def publish(line, influx): '''Write one line to InfluxDB. On failure, spool it to disk and replay the spool together with the next successful write (capped at SPOOL_MAX_LINES).''' try: backlog = [] if os.path.exists(SPOOL_FILE): with open(SPOOL_FILE, 'r') as f: backlog = f.read().splitlines()[-SPOOL_MAX_LINES:] send_to_influx(backlog + [line], influx) if backlog: os.remove(SPOOL_FILE) except urllib.error.HTTPError as err: body = err.read().decode('utf-8', 'replace')[:200] if err.code == 400: # Permanently rejected (malformed line or field type conflict). # Re-spooling would poison every future write, so drop the batch. print('InfluxDB rejected the write, dropping {0} spooled line(s): {1}'.format(len(backlog) + 1, body)) if os.path.exists(SPOOL_FILE): os.remove(SPOOL_FILE) else: print('InfluxDB write failed (HTTP {0}), spooling line: {1}'.format(err.code, body or err)) with open(SPOOL_FILE, 'a') as f: f.write(line + '\n') except Exception as err: print('InfluxDB write failed, spooling line: {0}'.format(err)) with open(SPOOL_FILE, 'a') as f: f.write(line + '\n') if __name__ == '__main__': argparser = argparse.ArgumentParser() argparser.add_argument('--config', type=str, default=os.environ.get('CONFIG_FILE', 'config.yaml'), help='Path to the config.yaml') args = argparser.parse_args() with open(args.config, 'r') as stream: try: config = yaml.safe_load(stream) except yaml.YAMLError as exc: print(exc) sys.exit(1) inverter = MyInverter( host=config['modbus']['host'], port=config['modbus']['port'], timeout=config['modbus']['timeout'], unit=config['modbus']['unit'] ) while True: try: raw = inverter.read() solar_data = SolarData(raw) line = to_line_protocol(solar_data.data, config['influx'].get('measurement', 'solaredge')) if line: publish(line, config['influx']) except ConnectionException as err: print('Cannot connect to SolarEdge Modbus server on {0}:{1}, error: {2}'.format( config['modbus']['host'], config['modbus']['port'], err)) time.sleep(config['poll_interval'])