text: #!/usr/bin/env python3
  
  # References:
  # https://stoborblog.wordpress.com/2017/03/11/sunspec-solar-viewer-fronius-modbus/
  
  from pyModbusTCP.client import ModbusClient
  from pyModbusTCP import utils
  
  import math
  
  from devices import *
  
  class Symo:
      def __init__(self, ipaddr, model="autodetect"):
          self.ipaddr = ipaddr
          self.modbus = ModbusClient(host=ipaddr, port=502, auto_open=True, auto_close=True)
          self.model = model
          # self.modbus.debug(True)
  
          # Format:
          # "name : [register address, data type, unit 1]
          registers_gen24 = {
          # Common Block Register   
              "Sunspec_SID" : [40001, "uint32", 1],
              "Sunspec_Common_ID" : [40003, "uint16", 1],
              "Sunspec_Common_L" : [40004, "uint16", 1],
              "Sunspec_Devicename" : [40021, "string16", 1],
              "Sunspec_Software Version" : [40045, "string8", 1],
              "Sunspec_Inverter_ID" : [40070, "uint16", 1],
              "Sunspec_Inverter_L" : [40071, "uint16", 1],
              "AC_Phase-A_Current" : [40074, "float", 1],
              "AC_Phase-B_Current" : [40076, "float", 1],
              "AC_Phase-C_Current" : [40078, "float", 1],
              "AC_Voltage_Phase-AB" : [40080, "float", 1],
              "AC_Voltage_Phase-BC" : [40082, "float", 1],
              "AC_Voltage_Phase-CA" : [40084, "float", 1],
              "AC_Voltage_Phase-A-N" : [40086, "float", 1],
              "AC_Voltage_Phase-B-N" : [40088, "float", 1],
              "AC_Voltage_Phase-C-N" : [40090, "float", 1],
              "AC_Output_Power" : [40092, "float", 1],
              "AC_Frequency" : [40094, "float", 1],
              "AC_Energy" : [40102, "float", 1],
              "DC_Power" : [40108, "float", 1],
              
              "Cabinet_Temperature" : [40110, "float", 1],
              "Operating_State" : [40118, "uint16", 1],
          # Nameplate model
              "Nameplate_Continous_AC_Power" : [[40135,40136], "uint16_sunssf", 1],
              "Nameplate_Battery_Capacity" : [[40151,40152], "uint16_sunssf", 1],
              "Nameplate_Battery_Charge_Power" : [[40155,40156], "uint16_sunssf", 1],
              "Nameplate_Battery_Discharge_Power" : [[40157,40158], "uint16_sunssf", 1],
          # Status model
              "Isolation_resistance" : [[40236,40237], "uint16_sunssf", 1],
          # Storage device (Battery)
              "Sunspec_Battery_ID" : [40354, "uint16", 1],
              "Sunspec_Battery_L" : [40355, "uint16", 1],
              "Battery_Max_Charge_Power" : [[40356,40372], "uint16_sunssf", 1],
              "Battery_WChaGra" : [40357, "uint16", 1],
              "Battery_WDisChaGra" : [40358, "uint16", 1],
              "Battery_StorCtl_Mod" : [40359, "uint16", 1],
              "Battery_Min_Reserve" : [[40361,40375], "uint16_sunssf", 1],
              "Battery_SoC" : [[40362,40376], "uint16_sunssf", 1],
              "Battery_Status" : [40365, "uint16", 1],
              "Battery_OutWRte" : [[40366,40379], "uint16_sunssf", 1],
              "Battery_InWRte" : [[40367,40379], "uint16_sunssf", 1],
              "Battery_InOutWRte_RvrtTm" : [40369, "uint16", 1],
              "Battery_ChaGriSet" : [40371, "uint16", 1],
          # Multiple MPPT
              "Sunspec_MPPT_ID" : [40264, "uint16", 1],
              "MPPT_1_DC_Current" : [[40283,40266], "uint16_sunssf", 1],
              "MPPT_1_DC_Voltage" : [[40284,40267], "uint16_sunssf", 1],
              "MPPT_1_DC_Power" : [[40285,40268], "uint16_sunssf", 1],
              "MPPT_1_DC_Energy" : [[40286,40269], "acc32_sunssf", 1],
              "MPPT_2_DC_Current" : [[40303,40266], "uint16_sunssf", 1],
              "MPPT_2_DC_Voltage" : [[40304,40267], "uint16_sunssf", 1],
              "MPPT_2_DC_Power" : [[40305,40268], "uint16_sunssf", 1],
              "MPPT_2_DC_Energy" : [[40306,40269], "acc32_sunssf", 1],
              "MPPT_3_DC_Current" : [[40323,40266], "uint16_sunssf", 1],
              "MPPT_3_DC_Voltage" : [[40324,40267], "uint16_sunssf", 1],
              "MPPT_3_DC_Power" : [[40325,40268], "uint16_sunssf", 1],
              "MPPT_3_DC_Energy" : [[40326,40269], "acc32_sunssf", 1],
              "MPPT_4_DC_Current" : [[40343,40266], "uint16_sunssf", 1],
              "MPPT_4_DC_Voltage" : [[40344,40267], "uint16_sunssf", 1],
              "MPPT_4_DC_Power" : [[40345,40268], "uint16_sunssf", 1],
              "MPPT_4_DC_Energy" : [[40346,40269], "acc32_sunssf", 1],
          # Power Meter
              "Sunspec_Meter_ID" : [40070, "uint16", 200],
              "Meter_Frequency" : [40096, "float", 200],
              "Meter_Power_Total" : [40098, "float", 200],
              "Meter_Power_L1" : [40100, "float", 200],
              "Meter_Power_L2" : [40102, "float", 200],
              "Meter_Power_L3" : [40104, "float", 200],
              "Meter_Real_Energy_Exported" : [40130, "float", 200],
              "Meter_Real_Energy_Exported_L1" : [40132, "float", 200],
              "Meter_Real_Energy_Exported_L2" : [40134, "float", 200],
              "Meter_Real_Energy_Exported_L3" : [40136, "float", 200],
              "Meter_Real_Energy_Imported" : [40138, "float", 200],
              "Meter_Real_Energy_Imported_L1" : [40140, "float", 200],
              "Meter_Real_Energy_Imported_L2" : [40142, "float", 200],
              "Meter_Real_Energy_Imported_L3" : [40144, "float", 200],
          }
  
          registers_symo = {
          # Common Block Register   
              "Sunspec_SID" : [40001, "uint32", 1],
              "Sunspec_Common_ID" : [40003, "uint16", 1],
              "Sunspec_Common_L" : [40004, "uint16", 1],
              "Sunspec_Devicename" : [40021, "string16", 1],
              "Sunspec_Software Version" : [40045, "string8", 1],
              "Sunspec_Inverter_ID" : [40070, "uint16", 1],
              "Sunspec_Inverter_L" : [40071, "uint16", 1],
              "AC_Phase-A_Current" : [40074, "float", 1],
              "AC_Phase-B_Current" : [40076, "float", 1],
              "AC_Phase-C_Current" : [40078, "float", 1],
              "AC_Voltage_Phase-AB" : [40080, "float", 1],
              "AC_Voltage_Phase-BC" : [40082, "float", 1],
              "AC_Voltage_Phase-CA" : [40084, "float", 1],
              "AC_Voltage_Phase-A-N" : [40086, "float", 1],
              "AC_Voltage_Phase-B-N" : [40088, "float", 1],
              "AC_Voltage_Phase-C-N" : [40090, "float", 1],
              "AC_Output_Power" : [40092, "float", 1],
              "AC_Frequency" : [40094, "float", 1],
              "AC_Energy" : [40102, "float", 1],
              "DC_Power" : [40108, "float", 1],
              "Operating_State" : [40118, "uint16", 1],
          # Nameplate model
              "Nameplate_Continous_AC_Power" : [[40135,40136], "uint16_sunssf", 1],
          # Multiple MPPT
              "Sunspec_MPPT_ID" : [40264, "uint16", 1],
              "MPPT_1_DC_Current" : [[40283,40266], "uint16_sunssf", 1],
              "MPPT_1_DC_Voltage" : [[40284,40267], "uint16_sunssf", 1],
              "MPPT_1_DC_Power" : [[40285,40268], "uint16_sunssf", 1],
              "MPPT_2_DC_Current" : [[40303,40266], "uint16_sunssf", 1],
              "MPPT_2_DC_Voltage" : [[40304,40267], "uint16_sunssf", 1],
              "MPPT_2_DC_Power" : [[40305,40268], "uint16_sunssf", 1],
          }
          
          calculated_parameters_gen24 = {
              "Consumption_Sum" : ['AC_Output_Power', 'Meter_Power_Total', '+'],
              "Battery_Power" : ['MPPT_4_DC_Power', 'MPPT_3_DC_Power', '-'],
              "PV_Power" : ['MPPT_1_DC_Power', 'MPPT_2_DC_Power', '+'],
              "AC_Output_L1" : ['AC_Voltage_Phase-A-N', 'AC_Phase-A_Current', '*'],
              "AC_Output_L2" : ['AC_Voltage_Phase-B-N', 'AC_Phase-B_Current', '*'],
              "AC_Output_L3" : ['AC_Voltage_Phase-C-N', 'AC_Phase-C_Current', '*'],
              }
  
          calculated_parameters_symo = {
              "PV_Power" : ['MPPT_1_DC_Power', 'MPPT_2_DC_Power', '+'],
              "AC_Output_L1" : ['AC_Voltage_Phase-A-N', 'AC_Phase-A_Current', '*'],
              "AC_Output_L2" : ['AC_Voltage_Phase-B-N', 'AC_Phase-B_Current', '*'],
              "AC_Output_L3" : ['AC_Voltage_Phase-C-N', 'AC_Phase-C_Current', '*'],
              }
  
          self.modbus.unit_id(1)
          sunspecid = self.read_uint16(40070)
          if sunspecid != 113:
              print("Warning: Invalid SunspecID, wrong device ?")
  
          if self.model == "autodetect":
              name = self.read_string(40021, 16)
              if "GEN24" in name:
                  self.registers = registers_gen24
                  self.calculated_parameters = calculated_parameters_gen24
              else:
                  self.registers = registers_symo
                  self.calculated_parameters = calculated_parameters_symo
          elif self.model == "symo_gen24":
              self.registers = registers_gen24
              self.calculated_parameters = calculated_parameters_gen24
          elif self.model == "symo":
              self.registers = registers_symo
              self.calculated_parameters = calculated_parameters_symo
          else:
              print("Error: Unkown symo model")
                                        
      def read_uint16(self, addr):
          regs = self.modbus.read_holding_registers(addr-1, 2)
          if regs:
              if regs[0] == 0xffff:
                  print("read_uint16() - invalid value/no data")
                  return None
              else:
                  return int(regs[0])
          else:
              print("read_uint16() - error")
              return False
        
      def read_uint32(self, addr):
          regs = self.modbus.read_holding_registers(addr-1, 2)
          if regs:
              if regs[0] == 0xffff and regs[1] == 0xffff:
                  print("read_uint32() - invalid value/no data")
                  return None
              else:
                  return int(utils.word_list_to_long(regs, big_endian=True)[0])
          else:
              print("read_uint32() - error")
              return False
          
      def read_float(self, addr):
          regs = self.modbus.read_holding_registers(addr-1, 2)
          if not regs:
              print("read_float() - error")
              return False
  
          list_32_bits = utils.word_list_to_long(regs, big_endian=True)
          value = float(utils.decode_ieee(list_32_bits[0]))
          if math.isnan(value):
              return None
          else:
              return value
  
      def read_uint16_sunssf(self, addrs):
          value = self.modbus.read_holding_registers(addrs[0]-1, 1)
          scalereg = self.modbus.read_holding_registers(addrs[1]-1, 1)
          # print(value[0],scalereg[0])
          if value and scalereg:
              if scalereg[0] == 0x8000:
                  print("read_uint16_sunssf() - invalid value/no data")
                  return None
              elif value[0] == 0xffff:
                  print("read_uint16_sunssf() - invalid value/no data")
                  return None
              elif scalereg[0] > 32768:   # Bad hack for int16 conversion
                  scalef = 10**(scalereg[0]-65536)
              else:
                  scalef = 10**(scalereg[0])
              return float(value[0] * scalef)
          else:
              print("read_uint16_sunssf() - error")
              return False
  
      def read_acc32_sunssf(self, addrs):
          regs = self.modbus.read_holding_registers(addrs[0]-1, 2)
          value = int(utils.word_list_to_long(regs, big_endian=True)[0])
          scalereg = self.modbus.read_holding_registers(addrs[1]-1, 1)
          # print(regs,scalereg[0])
          if regs and scalereg:
              if scalereg[0] == 0x8000:
                  print("read_acc32_sunssf() - invalid value/no data")
                  return None
              elif value == 0xffffffff:
                  print("read_acc32_sunssf() - invalid value/no data")
                  return None
              elif scalereg[0] > 32768:   # Bad hack for int16 conversion
                  scalef = 10**(scalereg[0]-65536)
              else:
                  scalef = 10**(scalereg[0])
              return float(value * scalef)
          else:
              print("read_acc32_sunssf() - error")
              return False
  
      def read_string(self, addr, size):
          regs = self.modbus.read_holding_registers(addr-1, size)
          if regs:
              result = ''
              for i in range(size):
                  if regs[i] == 0:
                      break
                  result += (chr((regs[i]>>8) & 0xff))
                  result += (chr(regs[i] & 0xff))
              if result == '':
                  return False
              else:
                  return result
  
      def read_data(self, parameter):
          [register, datatype, unit_id] = self.registers[parameter]
          
          self.modbus.unit_id(unit_id)
          if datatype == "float":
              return self.read_float(register)
          elif datatype == "uint32":
              return self.read_uint32(register)
          elif datatype == "uint16":
              return self.read_uint16(register)
          elif datatype == 'uint16_sunssf':
              return self.read_uint16_sunssf(register)
          elif datatype == 'acc32_sunssf':
              return self.read_acc32_sunssf(register)
          elif datatype == 'string8':
              return self.read_string(register, 8)
          elif datatype == 'string16':
              return self.read_string(register, 16)
          else:
              return False
          
      def read_calculated_value(self, parameter):
          [param1, param2, operant] = self.calculated_parameters[parameter]
  
          try:
              if operant == '+':
                  value = self.read_data(param1) + self.read_data(param2)
              elif operant == '-':
                  value = self.read_data(param1) - self.read_data(param2)
              elif operant == '*':
                  value = self.read_data(param1) * self.read_data(param2)
              elif operant == '/':
                  value = self.read_data(param1) / self.read_data(param2)
              else:
                  return False
          except TypeError:
              return False
  
          return value
  
      def write_float(self, addr, value):
          floats_list = [value]
          b32_l = [utils.encode_ieee(f) for f in floats_list] 
          b16_l = utils.long_list_to_word(b32_l, big_endian=False)
          return self.modbus.write_multiple_registers(addr-1, b16_l)
  
      def write_uint16(self, addr, value):
          return self.modbus.write_single_register(addr-1, value)
  
      def write_uint16_sunssf(self, addrs, value):
          scalereg = self.modbus.read_holding_registers(addrs[1]-1, 1)
          if scalereg[0] > 32768:   # Bad hack for int16 conversion
              scalef = 10**(scalereg[0]-65536)
          else:
              scalef = 10**(scalereg[0])
          value = int(value / scalef)
          return self.modbus.write_single_register(addrs[0]-1, value)
     
      def write_data(self, parameter, value):
          [register, datatype, unit_id] = self.registers[parameter]
          
          self.modbus.unit_id(unit_id)
          if datatype == "float":
              return self.write_float(register, value)
          elif datatype == "uint16":
              return self.write_uint16(register, value)
          elif datatype == 'uint16_sunssf':
              return self.write_uint16_sunssf(register, value)
          else:
              return False
  
      def print_all(self):
          print("Show all registers:")
          for name, params in self.registers.items():
              value = self.read_data(name)
              if value is not None:
                  if type(params[0]) is list:
                      print("{0:d}: {1:s} - {2:2.1f}".format(params[0][0], name, value))
                  elif type(value) is str:
                      print("{0:d}: {1:s} - {2:s} ".format(params[0], name, value))
                  else:
                      print("{0:d}: {1:s} - {2:2.1f}".format(params[0], name, value))
                  
      def print_all_calculated(self):
          print("Show all calculated values:")
          for name, params in self.calculated_parameters.items():
              value = self.read_calculated_value(name)
              print("{0:s} - {1:2.1f}".format(name, value))
          
      def get_all_parameters(self):
          return list(self.registers.keys())
  
      def get_all_calculated(self):
          return list(self.calculated_parameters.keys())
      
      # To search for undocument registers.... 
      def print_raw(self):
          print("Raw read 1000-2000:")
          for i in range(40354,40354+24,1):
              value = self.read_uint16(i)
              if value:
  #                print("{0:d}: {1:2.1f}".format(i, value))
                  print("{0:d}: {1:d}".format(i, value))
              #else:
              #    print("{0:d}: error".format(i))
          
      def set_battery_charge_rate(self, power):
          if power == None:
              print("Disable Battery Charge Limit")
              reg = self.read_data('Battery_StorCtl_Mod')
              reg = reg & 0xfffe # clear bit 0
              self.write_data('Battery_StorCtl_Mod',reg)
          else:
              print("Setting Charge limit to {0}".format(power))
              reg = self.read_data('Battery_StorCtl_Mod')
              reg = reg | 0x1 # set bit 0
              self.write_data('Battery_StorCtl_Mod',reg)
  #             self.write_uint16(40359,1)
              self.write_data('Battery_InWRte', power)
              
      def set_battery_discharge_rate(self, power):
          if power == None:
              print("Disable Battery discharge Limit")
              reg = self.read_data('Battery_StorCtl_Mod')
              reg = reg & 0xfffd # clear bit 0
              self.write_data('Battery_StorCtl_Mod',reg)
          else:
              print("Setting discharge limit to {0}".format(power))
              reg = self.read_data('Battery_StorCtl_Mod')
              reg = reg | 0x2 # set bit 1
              self.write_data('Battery_StorCtl_Mod',reg)
  #             self.write_uint16(40359,1)
              self.write_data('Battery_OutWRte', power)
      
  # Test program
  if __name__ == "__main__":
      
      symo = Symo(ipaddr=gen24_ip)
      
      # Current AC Output Power
      # print(symo.read_float(40092))
      
      print(symo.read_data("Sunspec_Common_ID"))
  
      print(symo.read_uint32(40286))
   
  #    symo.set_battery_charge_rate(10)
  #    symo.set_battery_discharge_rate(None)
  #    symo.write_uint16(40366,400)
  #    symo.write_uint16(40358,3)
  #    symo.write_uint16(40358,0)
  #    print(symo.read_uint16(40359))
      
      print(symo.get_all_parameters())
          
      symo.print_all()
      
      # print(symo.read_calculated_value('Consumption_Sum'))
      
      symo.print_all_calculated()
      # symo.print_raw()
  
      # print(symo.read_uint16(40069))
  
      # print(symo.modbus.unit_id())
      
      # symo.modbus.unit_id(200)
  
      # print(symo.read_uint16(40070))
      # print(symo.read_float(40098))
      
      # print(symo.get_mppt_power())
      
      # symo.modbus.unit_id(1)
  
  
  
  