Short or garbled gateway responses are rejected on register count instead of half-decoding (previously: 'unpack requires a buffer', OverflowError on garbage scale factors). Scale factors outside +/-10 and non-finite floats are dropped before they reach line protocol. offgrid is written as integer offgrid_state: the existing DB cannot be migrated (mixed inmem/tsi1 shards block all deletes) and the old boolean field conflicts with integer writes, which poisoned the spool - HTTP 400 now drops the batch and logs the InfluxDB error body instead of re-spooling it forever. grafana_dashboard.json: fully German dashboard (InfluxQL, InfluxDB 1.x) with off-grid state, SoC (SoE*SoH/100) and consumption-source panels; status tiles use integers plus value mappings because Grafana drops string/boolean fields from InfluxQL time series.
96 lines
3.4 KiB
Python
Executable File
96 lines
3.4 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']
|
|
)
|
|
|
|
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'])
|