SolarEdge firmware is known to silently drop the Modbus TCP listener (port refuses connections while still shown as enabled). Write a connection_state=1 point on first failed poll and 0 on recovery so the outage is visible in Grafana instead of just a data gap. ConnectionException is re-raised from read() - it is fatal for the whole cycle, not a flaky single block. Marker points carry no tags (registers unreadable during outage), so the tile query skips the serial filter. Also includes threshold tweaks on the power stat tiles.
105 lines
3.8 KiB
Python
Executable File
105 lines
3.8 KiB
Python
Executable File
#!/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']
|
|
)
|
|
|
|
modbus_ok = True
|
|
measurement = config['influx'].get('measurement', 'solaredge')
|
|
|
|
while True:
|
|
try:
|
|
raw = inverter.read()
|
|
if not modbus_ok:
|
|
publish(to_line_protocol({'connection_state': 0}, measurement), config['influx'])
|
|
modbus_ok = True
|
|
solar_data = SolarData(raw)
|
|
line = to_line_protocol(solar_data.data, measurement)
|
|
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))
|
|
if modbus_ok:
|
|
publish(to_line_protocol({'connection_state': 1}, measurement), config['influx'])
|
|
modbus_ok = False
|
|
|
|
time.sleep(config['poll_interval'])
|