#!/usr/bin/env python3
#
# Log power parameters from the solar charger.
#
# Note: this requires Modbus and dependencies to be available in python3.

from pymodbus.client import ModbusSerialClient as ModbusClient
import serial.rs485
import os
import sys
import datetime
import json
import glob

# supported serial devices
# devices will be located by checking "/dev/serial/by-id/<pattern>"
# e.g. /dev/serial/by-id/usb-1a86_USB_Single_Serial_589A041798-if00
SERIAL_DEVICES = [
  {
    'name': 'XR21B1411',
    'pattern': '*XR21B1411*',
    'invert_rts': True
  },
  {
    'name': 'CH343',
    'pattern': '*1a86_USB*',
    'invert_rts': False
  }
]

# default options
log_to_blockgraph = True
invert_rts = False
port = None

HELP_MSG = f'''
Usage: log_power_status [OPTIONS]

Options:
  -h, --help           Print usage and exit
  -c, --stdout         Write to stdout only, not blockgraph
  -i, --invert-rts     Invert RTS logic level
  port=PORT            Set modbus port (e.g. /dev/ttyUSB0)
'''

# collect user arguments
for arg in sys.argv[1:]:

  if arg in ('-h', '--help', 'help'):
    print(HELP_MSG)
    sys.exit(0)

  elif arg in ('-i', '--invert-rts'):
    invert_rts = True

  elif arg in ('-c', '--stdout'):
    ts = datetime.datetime.now()
    print("time: {\"ts\": \"" + ts.strftime("%Y-%m-%d %H:%M:%S.%f") + "\"}")
    log_to_blockgraph = False

  elif 'port=' in arg:
    port = arg.split('=', maxsplit=1)[1]

  else:
    print(f'Invalid argument "{arg}"')
    sys.exit(1)

# if not specified, try to find a suitable serial device from known devices
if port is None:
  for dev in SERIAL_DEVICES:
    name = dev['name']
    pattern = dev['pattern']
    portlink = glob.glob(f'/dev/serial/by-id/{pattern}')
    if portlink:
      if len(portlink) > 1:
        print(f'WARNING: Multiple serial devices found matching "{pattern}"')
      port = os.path.realpath(portlink[0])
      invert_rts = dev.get('invert_rts', False)
      break

# check serial device present
if not port or not os.path.exists(port):
  print(f'Serial device not found: {port}')
  sys.exit(1)


# Create 32bit int from two 16bit ones
def combine_LH(lo, hi):
  L = lo.to_bytes(2, byteorder='big', signed=False)
  H = hi.to_bytes(2, byteorder='big', signed=False)
  return int.from_bytes(H+L, byteorder='big', signed=True)

# Log to stdout or blockgraph
def writelog(level, system, msg):
  if log_to_blockgraph:
    os.system("bgsystemlog -l " + level + " -s " + system + " -m '" + msg + "'")
  else:
    print(level + " " + system + " " + msg)

# Log data to stdout or blockgraph
def writedatalog(location, msg):
  if log_to_blockgraph:
    os.system("bgsystemlog -j " + location + " -m '" + msg + "'")
  else:
    print(location + ": " + msg)

# initialize client
client = ModbusClient(port=port, baudrate=115200, timeout=1, retries=3)
if not client.connect():
  writelog('ERROR', 'POWER', 'ModbusClient failed to connect')
  sys.exit(1)
if invert_rts:
  client.socket.rs485_mode = serial.rs485.RS485Settings(
    rts_level_for_tx=False,
    rts_level_for_rx=True,
  )

# Solar power
result = client.read_input_registers(0x3100, 4, 1)
solar = {}
solar['V_mV'] = int(result.registers[0] * 10)
solar['I_mA'] = int(result.registers[1] * 10)
solar['P_mW'] = int(combine_LH(result.registers[2], result.registers[3]) * 10)
result = client.read_input_registers(0x330C, 4, 1)
solar['Eday_Wh']   = int(combine_LH(result.registers[0], result.registers[1]) * 10)
solar['Emonth_Wh'] = int(combine_LH(result.registers[2], result.registers[3]) * 10)

# Battery
result = client.read_input_registers(0x331A, 3, 1)
battery = {}
battery['V_mV'] = int(result.registers[0] * 10)
battery['I_mA'] = int(combine_LH(result.registers[1], result.registers[2]) * 10)
result = client.read_input_registers(0x311A, 1, 1)
battery['SoC_pct'] = int(result.registers[0])

# Load
result = client.read_input_registers(0x310C, 4, 1)
load = {}
load['V_mV'] = int(result.registers[0] * 10)
load['I_mA'] = int(result.registers[1] * 10)
load['P_mW'] = int(combine_LH(result.registers[2], result.registers[3]) * 10)
result = client.read_input_registers(0x3304, 4, 1)
load['Eday_Wh']   = int(combine_LH(result.registers[0], result.registers[1]) * 10)
load['Emonth_Wh'] = int(combine_LH(result.registers[2], result.registers[3]) * 10)

# Temp
result = client.read_input_registers(0x3110, 2, 1)
temp = {}
temp['Tbat_mC']  = int(result.registers[0] * 10)
temp['Tdevice_mC'] = int(result.registers[1] * 10)

result = client.read_input_registers(0x3200, 3, 1)
# Battery Status
stat_tmp = int(result.registers[0])
if stat_tmp:
  print( 'ERROR CHARGER reg. A15 battery status abnormal:', hex(stat_tmp) )
# Charging Status
stat_tmp = int(result.registers[1])
if stat_tmp & int('0xFFF0', 16):
  writelog( 'ERROR', 'POWER', 'reg. A16 charging equipment status abnormal: '+hex(stat_tmp) )
if not (stat_tmp & int('0x0001', 16)):    # D0
  writelog( 'ERROR', 'POWER', 'charging equipment status STANDBY' )
# Discharging status
stat_tmp = int(result.registers[2])
if stat_tmp & int('0x0FF0', 16):
  writelog( 'ERROR', 'POWER', 'reg. A17 discharging equipment status abnormal: '+hex(stat_tmp) )
if not (stat_tmp & int('0x0001', 16)):    # D0
  writelog( 'ERROR', 'POWER', 'discharging equipment status STANDBY' )
if stat_tmp & int('0x0002', 16):          # D1
  writelog( 'ERROR', 'POWER', 'discharging equipment status FAULT' )
stat_power = (stat_tmp >> 12) & int('0x0003', 16)   #D13-D12
if stat_power == 3:
  writelog( 'ERROR', 'POWER', 'discharging equipment status output power overload' )
stat_voltage = (stat_tmp >> 14) & int('0x0003', 16)   #D15-D14
message=''
if stat_voltage == 1:
  message = 'input voltage low'
elif stat_voltage == 2:
  message = 'input voltage high'
elif stat_voltage == 3:
  message = 'input voltage no access'
if message:
  writelog( 'ERROR', 'POWER', 'discharging equipment status: '+message )

writedatalog( 'solar',    json.dumps(solar) )
writedatalog( 'battery',  json.dumps(battery) )
writedatalog( 'poweruse', json.dumps(load) )
writedatalog( 'temperature', json.dumps(temp) )

client.close()

