#!/usr/bin/python
# Dick Streefland <dick@streefland.net>

port	= "/dev/solaredge"
speed	= 38400
devid	= 1

usage	= """solaredge - query SolarEdge inverter
Usage: solaredge [options]
Options:
  -a         show all relevant registers
  -d         dump all registers
  -i<sec>    show power & energy every <sec> seconds
  -r         round AC Power to watts"""
optdef	= "ai:dr"
states	= {
	'1': 'off',
	'2': 'sleeping',
	'3': 'starting',
	'4': 'mppt',
	'5': 'throttled',
	'6': 'shutting_down',
	'7': 'fault',
	'8': 'standby'
}

import serial, sys, struct, getopt, time

def error(err):
	sys.stderr.write("%s\n" % err)
	sys.exit(1)

def dumpreg(addr, data):
	line = ''
	for r in [data[i:i+2] for i in range(0, len(data), 2)]:
		if not line:
			line = '%5d:' % addr
		line += ' %04x' % struct.unpack(">H", r)
		addr += 1
		if (addr % 10) == 0:
			print line
			line = ''
	if line:
		print line

def crc_update(crc, byte):
	crc ^= byte
	for bit in range(8):
		xor = crc & 1
		crc >>= 1
		if xor:
			crc ^= 0xA001
	return crc

def crc(data):
	crc = 0xffff
	for byte in data:
		crc = crc_update(crc, ord(byte))
	return crc

def query(function, addr, size):
	request = struct.pack(">BBHH", devid, function, addr - 1, size)
	request += struct.pack("<H", crc(request))
	dev.write(request)
	expect = 3 + 2 * size + 2
	reply = dev.read(expect)
	if len(reply) != expect:
		error("short read")
	length = struct.unpack("B", reply[2])[0]
	if length != 2 * size:
		error("incorrect length")
	check = struct.unpack("<H", reply[-2:])[0]
	if check != crc(reply[:-2]):
		error("incorrect CRC")
	return reply[3:-2]

def reg(addr, addr_scale, size):
	if addr_scale == 0:
		regs = size
	else:
		# avoid race condition by reading register + scale together
		regs = addr_scale - addr + 1
	data = query(3, addr, regs)
	value = struct.unpack('>' + "HI"[size-1], data[:2*size])[0]
	decimals = 0
	if addr_scale != 0:
		scale = struct.unpack('>h', data[-2:])[0]
		if scale != 0:
			value *= 10 ** scale
		if scale < 0:
			decimals = - scale
	return "%.*f" % (decimals, value)

def summary():
	s = reg(40108,     0, 1)
	p = reg(40084, 40085, 1)
	e = reg(40094, 40096, 2)
	if opt['r']:
		p = "%.0f" % round(float(p))
	return "Power: %6s W  Energy: %8s Wh  (%s)" % (p, e, states.get(s, s))

# --- parse options
try:
	options, queries = getopt.getopt(sys.argv[1:], optdef)
except:
	error(usage)
opt = {}
for c in optdef:
	opt[c] = 0
for (option, arg) in options:
	if arg:
		opt[option[1]] = int(arg)
	else:
		opt[option[1]] = 1

# --- open serial port
try:
	# Communication does not work when the "clocal" flag is set on
	# the serial port. Opening with baudrate=0 is a workaround.
	dev = serial.Serial(port, 0, timeout=0.2)
	dev.baudrate = speed
except Exception, e:
	error(e)

# --- perform requested action
if opt['d']:
	addr = 40001
	magic = query(3, addr, 2)
	dumpreg(addr, magic)
	addr += 2
	while True:
		header = query(3, addr, 2)
		(id, length) = struct.unpack('>HH', header)
		if length == 0:
			break
		print "%d: device id %d" % (addr, id)
		addr += 1
		print "%d: %d registers" % (addr, length)
		addr += 1
		data = query(3, addr, length)
		dumpreg(addr, data)
		addr += length
	sys.exit(0)
if opt['a']:
	print "Status       : %10s"    % states[reg(40108, 0, 1)]
	print "Temperature  : %10s C"  % reg(40104, 40107, 1)
	print "DC Voltage   : %10s V"  % reg(40099, 40100, 1)
	print "DC Current   : %10s A"  % reg(40097, 40098, 1)
	print "DC Power     : %10s W"  % reg(40101, 40102, 1)
	print "AC Frequency : %10s Hz" % reg(40086, 40087, 1)
	print "AC Voltage   : %10s V"  % reg(40077, 40083, 1)
	print "AC Current   : %10s A"  % reg(40073, 40076, 1)
	print "AC Power     : %10s W"  % reg(40084, 40085, 1)
	print "AC Energy    : %10s Wh" % reg(40094, 40096, 2)
	sys.exit(0)
if opt['i']:
	try:
		while True:
			stamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
			print stamp, summary()
			time.sleep(opt['i'])
	except KeyboardInterrupt:
		print "Interrupted"
	sys.exit(0)
stamp = time.strftime("%Y-%m-%d %H:%M", time.localtime())
print stamp, summary()
