Opbrengst zonnepanelen uitlezen

Dit project heeft tot doel om de opbrengst, en nog wat aanvullende gegevens, van mijn zonnepanelen uit te lezen en te presenteren in een mooi dashboard. Ik heb 20 zonnepanelen op mijn dak welke per 10 aangesloten zijn op 2 Delta Solivia Omvormers (Inverters).

Deze 2 omvormers hangen in de berging van mijn woning. Ik heb deze omvormers middels een standaard RJ-45 kabel op elkaar aangesloten, en vervolgens middels een standaard RJ-45 kabel waar ik één zijde van afgeknipt heb een middels een FT232RL + 75176 FTDI USB naar RS485 seriële adapter aangesloten op de USB-aansluiting van een Linksys NSLU2 (De NSLU2 is een netwerkapparaat dat wordt gemaakt door Linksys. Het wordt gebruikt om USB-opslagapparaten zoals USB-sticks en harde schijven met USB-interface toegankelijk te maken over een netwerk.). Deze NSLU2 had ik al jaren werkeloos is de doos liggen, maar is een ideaal apparaat om deze toepassing mee te realiseren. Natuurlijk kun je ook prima een Raspberry PI of ander linux doosje gebruiken. Dus deze afgestoft en van een vers Debian 7 linux operating voorzien en Python opgezet. Vervolgens twee python scripts en een shell-script gemaakt die de twee omvormers uitlezen en de output van de omvormers in JSON-formaat naar de MQTT-broker doorzet. Node-red pakt vervolgens deze output op en zet deze in een MariaDB-tabel. Tot slot met Google Data studio een flashy Dashboard gemaakt. Hieronder een overzicht van mijn twee Delta Solivia omvormers:

 

Solivia 2.5 EU G3 Solivia 3.0 TR EU G4
Variant 15 (paragraaf 8.6) Variant 105 (paragraaf 8.7)
Serienr. 220268151317000697 Serienr. 220287161303000538

Benodigdheden:

  • 3 RJ-45 kabels
  • FT232RL + 75176 FTDI USB naar RS485 seriële adapter
  • USB 2.0 kabel A mannelijk – B mannelijk
  • NSLU2 (of andere linux doos)
  • Minimaal 4Gb USB-stick
  • Vrije poort op je switch of router
Reeds geïnstalleerd en up-and-running:
  • Node-red
  • MQTT
  • MariaDB (MySQL)
  • Google account

Pinout:

Solivia 3.0

Solivia 2.5

RJ-45 pin – draad FT232RL RJ-45 pin – draad FT232RL
8 – Bruin RX_B 7 – Bruin/wit TX_A
7 – Bruin/wit TX_A 6 – Groen RX_B
4 – Blauw Gnd 4 – Blauw Gnd

Verbindingskabel tussen de twee omvormers is een gewone standaard recht-toe-recht-aan RJ45 kabel, blijkbaar wordt intern in de omvormers de juiste verbinding gemaakt. Volgens onderstaand plaatje zou bij meer dan één inverter een 120R terminator geplaatst moeten worden. Echter toen ik deze terminator plaatste kreeg ik geen data binnen, en zonder doet hij het ook prima, dus geen terminator toegepast. Connectietest: Voor een eerste test of er een connectie te maken valt met de omvormers heb ik de tool DeltaSolviaProtocolTest geïnstalleerd op mijn Windows laptop. Je kunt gewoon de zip uitpakken in een map en de executable opstarten. De tool detecteert zelf automatisch de COM-poort waarop de FT232RL adapter aangesloten zit, je moet hem alleen nog even selecteren. Baudrate, Parity en stopbits instellen op 19200,n,8,1, Timeout kun je standaard houden. Het is handig om eerst één omvormer te testen, dus haal de verbindingskabel nog even los. Standaard staat namelijk het RS485 adres van de omvormer op de waarde 1 ingesteld, met de verbindingskabel aangesloten weet het tool niet welke omvormer reageert. Later stellen we de juiste RS485 adressen op de omvormers in. Klik vervolgens willekeurig één van de blauwe linkjes aan, bijv. DC Cur 1, en klik op Send. Als het goed is zie je een Send: bericht, en darna een Port event received: bericht. Het werkt ! Probeer dit uit op beide omvormers, eerst zonder verbindingskabel. Krijg je een timeout, check dan nog even goed je kabel en pinout ! Mijn timeouts werden veroorzaakt doordat het klipje van de RJ45 connector niet goed in de RJ45-aansluiting van de Delta Solivia 3.0 klikte en los zat. Tools: Windows: http://www.bettersoftware.com.au/DeltaSoliviaProtocolTest.zip Linux:

  • unset_serial_ctrlchars.sh /dev/ttyUSB1
  • sudo apt-get install jpnevulator
  • jpnevulator –ascii –timing-print –tty /dev/ttyUSB1 –read

Stel vervolgens een ander RS485 adres in op één van de omvormers, dit adres kan een waarde hebben tussen de 1 en 254. Volg de instructies in de gebruikershandleiding van je omvormer om het adres aan te passen. Ik heb mijn Solivia 3.0 op de default waarde 1 laten staan, en de Solivia 2.5 op de waarde 2. Zet de baudrate op 19200:

NSLU2:

NSLU2 van “vers” operating systeem voorzien, nou ja vers, Debian 7 is de laatste versie waarin de NSLU2 ondersteund wordt. Aangezien dit systeem uitsluitend intern wordt gebruikt, en geen connecties van buitenaf nodig heeft, voor mij niet een ramp.

  • NSLU2 Debian 7 firmware + OS geïnstalleerd conform beschrijving op https://www.cyrius.com/debian/nslu2/unpack/  
  • Verder qua inrichting het minimale gedaan, alleen de hostname gezet, het root wachtwoord aangepast en de NTP client geïnstalleerd en geconfigureerd naar de time servers van google
  • De APT bibliotheek nog gerefreshed met: sudo apt-get update
  • NLSU2 met ethernet kabel aangesloten op een switch, en de USB kabel van de FT232RL adapter op de nog vrije USB poort van de NLSU2 aangesloten.

Python:

Python3 geïnstalleerd:

        • PIP geïnstalleerd met
          sudo apt-get install python3-pip
          Note: Bij een herinstallatie werkten pip en pip3 niet, dit opgelost door:
          cd /tmp
          curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
          sudo python3 get-pip.py
        • pyserial met PIP geïnstalleerd met 
          sudo pip3 install pyserial
        • Git geïnstalleerd met 
      sudo apt-get install git
      • map /root/python aangemaakt mkdir /root/python
      • cd /root/python

      Voorbeeld python scripts aangepast naar Solivia 3.0 en 2.5:

      git clone https://github.com/bbinet/delta-rpi
        • cd delta-rpi
        • python script delta-rpi.py gekopieerd naar delta-solivia3.0.py respectievelijk delta-solivia2.5.py met
      cp delta-rpi.py delta-solivia3.0.py

      cp delta-rpi.py delta-solivia2.5.py
        • Conform Public RS485 Protocol 1V2.pdf “Public Solar Inverter  Communication Protocol (Version 1.2)” van Delta Energy Systems, paragraaf 8.7 (Solivia 3.0 EU G4 TR = Variant 105 ) de DELTA_RPI_STRUCT in python script delta-solivia3.0.py aangepast, er voor gezorgd dat er JSON uit komt samen met wat andere noodzakelijke aanpassingen (geel gekleurd)
      #!/usr/bin/python3
      # -*- coding: utf-8 -*-
      
      import time
      import binascii
      import struct
      import serial
      import sys
      import os
      import signal
      from datetime import datetime
      from argparse import ArgumentParser
      from pprint import pprint
      
      import crc16
      
      
      def ma_mi_bf(data):
          ma, mi, bf = struct.unpack('>BBB', data)
          return '{:02d}.{:02d}.{:02d}'.format(ma, mi, bf)
      
      
      DEBUG=False
      READ_BYTES = 1024
      STX=0x02
      ETX=0x03
      ENQ=0x05
      ACK=0x06
      NAK=0x15
      # Variables in the data-block of a Delta RPI M-series inverter,
      # as far as I've been able to establish their meaning.
      # The fields for each variable are as follows:
      # name, struct, size in bytes, decoder, multiplier-exponent (10^x), unit, SunSpec equivalent
      DELTA_RPI = (
          ("SAP part number", "11s", str),
          ("SAP serial number", "18s", str),
          ("SAP date code", "4s", binascii.hexlify),
          ("SAP revision", "2s", binascii.hexlify),
          ("Software Revision System controller", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
          ("Software Revision Power controller", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
          ("Software Revision ENS controller", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
          ("Software Revision Watch dog controller", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
          ("Software Revision DC controller", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
          ("Software Revision DC 1", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
          ("Software Revision DC 2", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
          ("Software Revision DC 3", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
          ("Software Revision AC", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
          ("Software Revision AC 1", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
          ("Software Revision AC 2", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
          ("Software Revision AC 3", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
          ("Software Revision reserved", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
          ("Solar Power at Input 1", "H", int, 0, "W"),
          ("Solar Voltage at Input 1", "H", int, 0, "V"),
          ("Solar Current at Input 1", "H", int, 0, "A"),
          ("Solar Power at Input 2", "H", int, 0, "W"),
          ("Solar Voltage at Input 2", "H", int, 0, "V"),
          ("Solar Current at Input 2", "H", int, 0, "A"),
          ("Solar Power at Input 3", "H", int, 0, "W"),
          ("Solar Voltage at Input 3", "H", int, 0, "V"),
          ("Solar Current at Input 3", "H", int, 0, "A"),
          ("AC Current(Phase1)", "H", int, 0, "A", "AphA"),
          ("AC Voltage(Phase1)", "H", int, 0, "V"),
          ("AC Frequency(Phase1)", "H", int, 0, "Hz"),
          ("AC Active Power(Phase1)", "H", int, 0, "W"),
          ("AC Reactive Power(Phase1)", "h", int, 0, "VAR"),
          ("AC Current(Phase2)", "H", int, 0, "A", "AphA"),
          ("AC Voltage(Phase2)", "H", int, 0, "V"),
          ("AC Frequency(Phase2)", "H", int, 0, "Hz"),
          ("AC Active Power(Phase2)", "H", int, 0, "W"),
          ("AC Reactive Power(Phase2)", "h", int, 0, "VAR"),
          ("AC Current(Phase3)", "H", int, 0, "A", "AphA"),
          ("AC Voltage(Phase3)", "H", int, 0, "V"),
          ("AC Frequency(Phase3)", "H", int, 0, "Hz"),
          ("AC Active Power(Phase3)", "H", int, 0, "W"),
          ("AC Reactive Power(Phase3)", "h", int, 0, "VAR"),
          ("Solar Isolation plus", "H", int, 0, "kOhm"),
          ("Solar Isolation minus", "H", int, 0, "kOhm"),
          ("Temperature amb", "h", int, 0, "Celsius"),
          ("Temperature heatsink", "h", int, 0, "Celsius"),
          ("Supplied ac energy (total)", "Q", int, 0, "Wh"),
          ("Inverter runtime (total)", "L", int, 0, "Minutes"),
          ("Status AC Output 1", "L", int),
          ("Status AC Output 2", "L", int),
          ("Status AC Output 3", "L", int),
          ("Status AC Output 4", "L", int),
          ("Internal Status 1", "L", int),
          ("Internal Status 2", "L", int),
          ("Internal Status 3", "L", int),
          ("Internal Status 4", "L", int),
          ("Supplied ac energy (day)", "Q", int, 0, "Wh"),
          ("Inverter runtime (day)", "I", int, 0, "Minute"),
          ("reserved", "67s", binascii.hexlify),
      )
      DELTA_RPI_STRUCT = '>' + ''.join([item[1] for item in DELTA_RPI])
      DUMMY_DATA = (
          b'802FA0E1000',  # SAP part number
          b'O1S16300040WH',  # SAP serial number
          b'0901',  # SAP date code
          b'0\x00',  # SAP revision
          b'\x01#',  # DSP FW Rev
          b'\x0f0',  # DSP FW Date
          b'\x01\r',  # Redundant MCU FW Rev
          b'\x0f\x0e',  # Redundant MCU FW Date
          b'\x01\x10',  # Display MCU FW Rev
          b'\x0f0',  # Display MCU FW Date
          b'\x00\x00',  # Display WebPage Ctrl FW Rev
          b'\x00\x00',  # Display WebPage Ctrl FW Date
          b'\x00\x00',  # Display WiFi Ctrl FW Rev
          b'\x00\x00',  # Display WiFi Ctrl FW Date
          0,  # AC Voltage(Phase1)
          0,  # AC Current(Phase1)
          0,  # AC Power(Phase1)
          0,  # AC Frequency(Phase1)
          0,  # AC Voltage(Phase1) [Redundant]
          0,  # AC Frequency(Phase1) [Redundant]
          0,  # AC Voltage(Phase2)
          0,  # AC Current(Phase2)
          0,  # AC Power(Phase2)
          0,  # AC Frequency(Phase2)
          0,  # AC Voltage(Phase2) [Redundant]
          0,  # AC Frequency(Phase2) [Redundant]
          0,  # AC Voltage(Phase3)
          0,  # AC Current(Phase3)
          0,  # AC Power(Phase3)
          0,  # AC Frequency(Phase3)
          0,  # AC Voltage(Phase3) [Redundant]
          0,  # AC Frequency(Phase3) [Redundant]
          0,  # Solar Voltage at Input 1
          0,  # Solar Current at Input 1
          0,  # Solar Power at Input 1
          0,  # Solar Voltage at Input 2
          0,  # Solar Current at Input 2
          0,  # Solar Power at Input 2
          0,  # ACPower
          0,  # (+) Bus Voltage
          0,  # (-) Bus Voltage
          0,  # Supplied ac energy today
          0,  # Inverter runtime today
          0,  # Supplied ac energy (total)
          0,  # Inverter runtime (total)
          0,  # Calculated temperature inside rack
          0,  # Status AC Output 1
          0,  # Status AC Output 2
          0,  # Status AC Output 3
          0,  # Status AC Output 4
          0,  # Status DC Input 1
          0,  # Status DC Input 2
          0,  # Error Status
          0,  # Error Status AC 1
          0,  # Global Error 1
          0,  # CPU Error
          0,  # Global Error 2
          0,  # Limits AC output 1
          0,  # Limits AC output 2
          0,  # Global Error 3
          0,  # Limits DC 1
          0,  # Limits DC 2
          b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',  # History status messages
      )
      
      
      def signal_handler(signal, frame):
          ''' Catch SIGINT/SIGTERM/SIGKILL and exit gracefully '''
          print("Stop requested...")
          sys.exit(0)
      signal.signal(signal.SIGINT, signal_handler)
      signal.signal(signal.SIGTERM, signal_handler)
      
      
      def send(conn, req, cmd, subcmd, data=b'', addr=1):
          """
          Send cmd/subcmd (e.g. 0x60/0x01) and optional data to the RS485 bus
          """
          assert req in (ENQ, ACK, NAK)  # req should be one of ENQ, ACK, NAK
          msg = struct.pack('BBBBB', req, addr, 2 + len(data), cmd, subcmd)
          if len(data) > 0:
              msg = struct.pack('5s%ds' % len(data), msg, data)
          crcval = crc16.calcData(msg)
          lsb = crcval & (0xff)
          msb = (crcval >> 8) & 0xff
          data = struct.pack('B%dsBBB' % len(msg), STX, msg, lsb, msb, ETX)
          if DEBUG: print(">>> SEND:", binascii.hexlify(msg), "=>", binascii.hexlify(data))
          conn.write(data)
          conn.flush()
      
      
      def receive(conn):
          """
          Attempt to read messages from a serial connection
          """
          data = bytearray()
          while True:
              buf = conn.read(READ_BYTES)
              if buf:
                  if DEBUG: print(">>> RAW RECEIVE:", buf)
                  data.extend(buf)
              if (not buf) or len(buf) < READ_BYTES:
                  break
      
          idx = 0
          while idx + 9 <= len(data):
              if data[idx] != STX:
                  idx += 1
                  continue
              stx, req, addr, size = struct.unpack('>BBBB', data[idx:idx+4])
              if req not in (ENQ, ACK, NAK):
                  print("Bad req value: {:02x} (should be one of ENQ/ACK/NAK)".format(req))
                  idx += 1
                  continue
              if idx + 4 + size >= len(data):
                  print("Can't read %d bytes from buffer" % size)
                  idx += 1
                  continue
              msg, lsb, msb, etx = struct.unpack('>%dsBBB' % size, data[idx+4:idx+7+size])
              if etx != ETX:
                  print("Bad ETX value: {:02x}".format(etx))
                  idx += 1
                  continue
              crc_calc = crc16.calcData(data[idx+1:idx+4+size])
              crc_msg = msb << 8 | lsb
              if crc_calc != crc_msg:
                  print("Bad CRC check: %s <> %s" % (binascii.hexlify(crc_calc), binascii.hexlify(crc_msg)))
                  idx += 1
                  continue
      
              if DEBUG: print(">>> RECV:", binascii.hexlify(data), "=>", binascii.hexlify(msg))
              yield {
                  "stx": stx,
                  "req": req,
                  "addr": addr,
                  "size": size,
                  "msg": msg,
                  "lsb": lsb,
                  "msb": msb,
                  "etx": etx,
              }
              idx += 4 + size
      
      
      def decode_msg(data):
          req = data['req']
          cmd, cmdsub = struct.unpack('>BB', data['msg'][0:2])
          data['cmd'] = cmd
          data['cmdsub'] = cmdsub
          data['raw'] = data['msg'][2:]
          if req == NAK:
              print("NAK value received: cmd/subcmd request was invalid".format(req))
          elif req == ENQ:
              if DEBUG: print("ENQ value received: request from master (datalogger)")
          elif req == ACK:
              if DEBUG: print("ACK value received: response from slave (inverter)")
              data['values'] = struct.unpack(DELTA_RPI_STRUCT, data['raw'])
          if DEBUG: pprint(data)
          return data
      
      
      def main():
          global DEBUG, MODE
          parser = ArgumentParser(description='Delta inverter simulator (slave mode) or datalogger (master mode) for RPI M8A')
          parser.add_argument('-a', metavar='ADDRESS', type=int,
                            default=1,
                            help='slave address [default: 1]')
          parser.add_argument('-d', metavar='DEVICE',
                            default='/dev/ttyUSB0',
                            help='serial device port [default: /dev/ttyUSB0]')
          parser.add_argument('-b', metavar='BAUDRATE',
                            default=9600,
                            help='baud rate [default: 9600]')
          parser.add_argument('-t', metavar='TIMEOUT', type=float,
                            default=2.0,
                            help='timeout, in seconds (can be fractional, such as 1.5) [default: 2.0]')
          parser.add_argument('--debug', action="store_true",
                            help='show debug information')
          parser.add_argument('mode', metavar='MODE', choices=['master', 'slave'],
                            help='mode can either be "master" or "slave"')
      
          args = parser.parse_args()
          DEBUG = args.debug
          MODE = args.mode
      
          conn = serial.Serial(args.d, args.b, timeout=args.t);
          conn.flushOutput()
          conn.flushInput()
          if MODE == 'master':
              send(conn, ENQ, 0x60, 0x01, addr=args.a)
              time.sleep(0.1)
          for data in receive(conn):
              if MODE == 'master' and data['addr'] == args.a and data['req'] in (ACK, NAK,):
                  d = decode_msg(data)
                  if d['req'] == ACK:
                      if not (d['cmd'] == 0x60 and d['cmdsub'] == 0x01):
                          print("Can't decode request cmd=0x%02X, cmdsub=0x%02X" % (d['cmd'], d['cmdsub']))
                          print("The only supported request is cmd=0x60, cmdsub=0x01")
                          continue
                  #    print(61 * '=')
                  print('{')
                  print('"inverter": 1,')
                  print('"Datetime": "', datetime.now(), '",')
                  for i, item in enumerate(DELTA_RPI):
                      label = item[0]
                      decoder = item[2]
                      scale = item[3] if len(item) > 3 else 0
                      units = item[4] if len(item) > 4 else ''
                      value = decoder(data['values'][i])
                      if decoder == float:
                          value = value * pow(10, scale)
                      # print('%-40s %20s %-10s' % (label, value, units))
                      print(' "' + label + '": [')
                      print(' {')
                      print(' "Value": ', end='')
                      if isinstance(value, str) :
                          print('"', end='')
                          print(value, end='')
                          print('",')
                      elif isinstance(value, int) :
                          print(value, end='')
                          print(',')
                      else:
                          print('"', end='')
                          print(value.decode('ascii'), end='')
                          print('",')
                      print('"Units": "' + units + '"')
                      print('}')
                      print('],')
                  print(' "end": "TRUE"')
                  print('}')
              if MODE == 'slave' and data['addr'] == args.a and data['req'] in (ENQ,):
                  d = decode_msg(data)
                  if d['cmd'] == 0x60 and d['cmdsub'] == 0x01:
                      raw = struct.pack(DELTA_RPI_STRUCT, *DUMMY_DATA)
                      send(conn, ACK, 0x60, 0x01, data=raw, addr=args.a)
                  else:
                      print("This simulator only replies to cmd=0x60 cmdsub=0x01 requests...")
      
      
      if __name__ == "__main__":
          main()
      
          • Conform Public RS485 Protocol 1V2.pdf “Public Solar Inverter  Communication Protocol (Version 1.2)” van Delta Energy Systems, paragraaf 8.6 (Solivia 2.5 EU G3 = Variant 15  de DELTA_RPI_STRUCT in python script delta-solivia2.5.py aangepast
        #!/usr/bin/python3
        # -*- coding: utf-8 -*-
        
        import time
        import binascii
        import struct
        import serial
        import sys
        import os
        import signal
        from datetime import datetime
        from argparse import ArgumentParser
        from pprint import pprint
        
        import crc16
        
        
        def ma_mi_bf(data):
            ma, mi, bf = struct.unpack('>BBB', data)
            return '{:02d}.{:02d}.{:02d}'.format(ma, mi, bf)
        
        
        DEBUG=False
        READ_BYTES = 1024
        STX=0x02
        ETX=0x03
        ENQ=0x05
        ACK=0x06
        NAK=0x15
        # Variables in the data-block of a Delta RPI M-series inverter,
        # as far as I've been able to establish their meaning.
        # The fields for each variable are as follows:
        # name, struct, size in bytes, decoder, multiplier-exponent (10^x), unit, SunSpec equivalent
        DELTA_RPI = (
            ("SAP part number", "11s", str),
            ("SAP serial number", "18s", str),
            ("SAP date code", "4s", binascii.hexlify),
            ("SAP revision", "2s", binascii.hexlify),
            ("Software Revision AC control", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
            ("Software Revision DC control", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
            ("Software Revision display", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
            ("Software Revision SC control", "3s", ma_mi_bf, 0, "Major,Minor,Bug fixing"),
            ("Solar Voltage at Input 1", "H", int, 0, "V"),
            ("Solar Current at Input 1", "H", int, 0, "A"),
            ("Solar Isolation resistance at Input 1", "H", int, 0, "kOhm"),
            ("Calculated temperature at ntc (DC side)", "h", int, 0, "C"),
            ("Solar Input MOV resistance", "H", int, 0, "kOhm"),
            ("AC Current", "H", int, 0, "A"),
            ("AC Voltage", "H", int, 0, "V"),
            ("AC Power", "H", int, 0, "W"),
            ("AC Frequency", "H", int, 0, "Hz"),
            ("Calculated temperature at ntc (AC side)", "h", int, 0, "C"),
            ("SC Grid Voltage", "H", int, 0, "V"),
            ("SC Grid ens Frequency", "H", int, 0, "Hz"),
            ("SC Grid DC injection current", "H", int, 0, "A"),
            ("AC Grid Voltage", "H", int, 0, "V"),
            ("AC Grid Frequency", "H", int, 0, "Hz"),
            ("AC Grid DC injection current", "H", int, 0, "A"),
            ("Day Supplied ac energy", "H", int, 0, "Wh"),
            ("Inverter runtime", "H", int, 0, "Minutes"),
            ("Maximum ac current of today", "H", int, 0, "A"),
            ("Minimum ac voltage of today", "H", int, 0, "V"),
            ("Maximum ac voltage of today", "H", int, 0, "V"),
            ("Maximum ac power of today", "H", int, 0, "W"),
            ("Minimum ac frequency of today", "H", int, 0, "Hz"),
            ("Maximum ac frequency of today", "H", int, 0, "Hz"),
            ("Supplied ac energy", "L", int, 0, "kWh"),
            ("Inverter runtime", "L", int, 0, "Hours"),
            ("Maximum solar 1 input current", "H", int, 0, "A"),
            ("Maximum solar 1 input voltage", "H", int, 0, "V"),
            ("Maximum solar 1 input power", "H", int, 0, "W"),
            ("Minimum Isolation resistance solar 1", "H", int, 0, "kOhm"),
            ("Maximum Isolation resistance solar 1", "H", int, 0, "kOhm"),
            ("Alarm status", "1s", binascii.hexlify),
            ("Status DC input", "1s", binascii.hexlify),
            ("Limits DC input", "1s", binascii.hexlify),
            ("Status AC output", "1s", binascii.hexlify),
            ("Limits AC output", "1s", binascii.hexlify),
            ("Warning status", "1s", binascii.hexlify),
            ("DC hardware failure", "1s", binascii.hexlify),
            ("AC hardware failure", "1s", binascii.hexlify),
            ("SC hardware failure", "1s", binascii.hexlify),
            ("Internal Bulk failure", "1s", binascii.hexlify),
            ("Internal communications failure", "1s", binascii.hexlify),
            ("AC hardware disturbance", "1s", binascii.hexlify),
            ("DC HW stage error", "1s", binascii.hexlify),
            ("Calibration Status", "1s", binascii.hexlify),
            ("Neutral error", "1s", binascii.hexlify),
            ("History status messages", "20s", binascii.hexlify),
        )
        DELTA_RPI_STRUCT = '>' + ''.join([item[1] for item in DELTA_RPI])
        DUMMY_DATA = (
            b'802FA0E1000',  # SAP part number
            b'O1S16300040WH',  # SAP serial number
            b'0901',  # SAP date code
            b'0\x00',  # SAP revision
            b'\x01#',  # DSP FW Rev
            b'\x0f0',  # DSP FW Date
            b'\x01\r',  # Redundant MCU FW Rev
            b'\x0f\x0e',  # Redundant MCU FW Date
            b'\x01\x10',  # Display MCU FW Rev
            b'\x0f0',  # Display MCU FW Date
            b'\x00\x00',  # Display WebPage Ctrl FW Rev
            b'\x00\x00',  # Display WebPage Ctrl FW Date
            b'\x00\x00',  # Display WiFi Ctrl FW Rev
            b'\x00\x00',  # Display WiFi Ctrl FW Date
            0,  # AC Voltage(Phase1)
            0,  # AC Current(Phase1)
            0,  # AC Power(Phase1)
            0,  # AC Frequency(Phase1)
            0,  # AC Voltage(Phase1) [Redundant]
            0,  # AC Frequency(Phase1) [Redundant]
            0,  # AC Voltage(Phase2)
            0,  # AC Current(Phase2)
            0,  # AC Power(Phase2)
            0,  # AC Frequency(Phase2)
            0,  # AC Voltage(Phase2) [Redundant]
            0,  # AC Frequency(Phase2) [Redundant]
            0,  # AC Voltage(Phase3)
            0,  # AC Current(Phase3)
            0,  # AC Power(Phase3)
            0,  # AC Frequency(Phase3)
            0,  # AC Voltage(Phase3) [Redundant]
            0,  # AC Frequency(Phase3) [Redundant]
            0,  # Solar Voltage at Input 1
            0,  # Solar Current at Input 1
            0,  # Solar Power at Input 1
            0,  # Solar Voltage at Input 2
            0,  # Solar Current at Input 2
            0,  # Solar Power at Input 2
            0,  # ACPower
            0,  # (+) Bus Voltage
            0,  # (-) Bus Voltage
            0,  # Supplied ac energy today
            0,  # Inverter runtime today
            0,  # Supplied ac energy (total)
            0,  # Inverter runtime (total)
            0,  # Calculated temperature inside rack
            0,  # Status AC Output 1
            0,  # Status AC Output 2
            0,  # Status AC Output 3
            0,  # Status AC Output 4
            0,  # Status DC Input 1
            0,  # Status DC Input 2
            0,  # Error Status
            0,  # Error Status AC 1
            0,  # Global Error 1
            0,  # CPU Error
            0,  # Global Error 2
            0,  # Limits AC output 1
            0,  # Limits AC output 2
            0,  # Global Error 3
            0,  # Limits DC 1
            0,  # Limits DC 2
            b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',  # History status messages
        )
        
        
        def signal_handler(signal, frame):
            ''' Catch SIGINT/SIGTERM/SIGKILL and exit gracefully '''
            print("Stop requested...")
            sys.exit(0)
        signal.signal(signal.SIGINT, signal_handler)
        signal.signal(signal.SIGTERM, signal_handler)
        
        
        def send(conn, req, cmd, subcmd, data=b'', addr=1):
            """
            Send cmd/subcmd (e.g. 0x60/0x01) and optional data to the RS485 bus
            """
            assert req in (ENQ, ACK, NAK)  # req should be one of ENQ, ACK, NAK
            msg = struct.pack('BBBBB', req, addr, 2 + len(data), cmd, subcmd)
            if len(data) > 0:
                msg = struct.pack('5s%ds' % len(data), msg, data)
            crcval = crc16.calcData(msg)
            lsb = crcval & (0xff)
            msb = (crcval >> 8) & 0xff
            data = struct.pack('B%dsBBB' % len(msg), STX, msg, lsb, msb, ETX)
            if DEBUG: print(">>> SEND:", binascii.hexlify(msg), "=>", binascii.hexlify(data))
            conn.write(data)
            conn.flush()
        
        
        def receive(conn):
            """
            Attempt to read messages from a serial connection
            """
            data = bytearray()
            while True:
                buf = conn.read(READ_BYTES)
                if buf:
                    if DEBUG: print(">>> RAW RECEIVE:", buf)
                    data.extend(buf)
                if (not buf) or len(buf) < READ_BYTES:
                    break
        
            idx = 0
            while idx + 9 <= len(data):
                if data[idx] != STX:
                    idx += 1
                    continue
                stx, req, addr, size = struct.unpack('>BBBB', data[idx:idx+4])
                if req not in (ENQ, ACK, NAK):
                    print("Bad req value: {:02x} (should be one of ENQ/ACK/NAK)".format(req))
                    idx += 1
                    continue
                if idx + 4 + size >= len(data):
                    print("Can't read %d bytes from buffer" % size)
                    idx += 1
                    continue
                msg, lsb, msb, etx = struct.unpack('>%dsBBB' % size, data[idx+4:idx+7+size])
                if etx != ETX:
                    print("Bad ETX value: {:02x}".format(etx))
                    idx += 1
                    continue
                crc_calc = crc16.calcData(data[idx+1:idx+4+size])
                crc_msg = msb << 8 | lsb
                if crc_calc != crc_msg:
                    print("Bad CRC check: %s <> %s" % (binascii.hexlify(crc_calc), binascii.hexlify(crc_msg)))
                    idx += 1
                    continue
        
                if DEBUG: print(">>> RECV:", binascii.hexlify(data), "=>", binascii.hexlify(msg))
                yield {
                    "stx": stx,
                    "req": req,
                    "addr": addr,
                    "size": size,
                    "msg": msg,
                    "lsb": lsb,
                    "msb": msb,
                    "etx": etx,
                }
                idx += 4 + size
        
        
        def decode_msg(data):
            req = data['req']
            cmd, cmdsub = struct.unpack('>BB', data['msg'][0:2])
            data['cmd'] = cmd
            data['cmdsub'] = cmdsub
            data['raw'] = data['msg'][2:]
            if req == NAK:
                print("NAK value received: cmd/subcmd request was invalid".format(req))
            elif req == ENQ:
                if DEBUG: print("ENQ value received: request from master (datalogger)")
            elif req == ACK:
                if DEBUG: print("ACK value received: response from slave (inverter)")
                data['values'] = struct.unpack(DELTA_RPI_STRUCT, data['raw'])
            if DEBUG: pprint(data)
            return data
        
        
        def main():
            global DEBUG, MODE
            parser = ArgumentParser(description='Delta inverter simulator (slave mode) or datalogger (master mode) for RPI M8A')
            parser.add_argument('-a', metavar='ADDRESS', type=int,
                              default=1,
                              help='slave address [default: 1]')
            parser.add_argument('-d', metavar='DEVICE',
                              default='/dev/ttyUSB0',
                              help='serial device port [default: /dev/ttyUSB0]')
            parser.add_argument('-b', metavar='BAUDRATE',
                              default=9600,
                              help='baud rate [default: 9600]')
            parser.add_argument('-t', metavar='TIMEOUT', type=float,
                              default=2.0,
                              help='timeout, in seconds (can be fractional, such as 1.5) [default: 2.0]')
            parser.add_argument('--debug', action="store_true",
                              help='show debug information')
            parser.add_argument('mode', metavar='MODE', choices=['master', 'slave'],
                              help='mode can either be "master" or "slave"')
        
            args = parser.parse_args()
            DEBUG = args.debug
            MODE = args.mode
        
            conn = serial.Serial(args.d, args.b, timeout=args.t);
            conn.flushOutput()
            conn.flushInput()
            if MODE == 'master':
                send(conn, ENQ, 0x60, 0x01, addr=args.a)
                time.sleep(0.1)
            for data in receive(conn):
                if MODE == 'master' and data['addr'] == args.a and data['req'] in (ACK, NAK,):
                    d = decode_msg(data)
                    if d['req'] == ACK:
                        if not (d['cmd'] == 0x60 and d['cmdsub'] == 0x01):
                            print("Can't decode request cmd=0x%02X, cmdsub=0x%02X" % (d['cmd'], d['cmdsub']))
                            print("The only supported request is cmd=0x60, cmdsub=0x01")
                            continue
                    #    print(61 * '=')
                    print('{')
                    print('"inverter": 2,')
                    print('"Datetime": "', datetime.now(), '",')
                    for i, item in enumerate(DELTA_RPI):
                        label = item[0]
                        decoder = item[2]
                        scale = item[3] if len(item) > 3 else 0
                        units = item[4] if len(item) > 4 else ''
                        value = decoder(data['values'][i])
                        if decoder == float:
                            value = value * pow(10, scale)
                        # print('%-40s %20s %-10s' % (label, value, units))
                        print(' "' + label + '": [')
                        print(' {')
                        print(' "Value": ', end='')
                        if isinstance(value, str) :
                            print('"', end='')
                            print(value, end='')
                            print('",')
                        elif isinstance(value, int) :
                            print(value, end='')
                            print(',')
                        else:
                            print('"', end='')
                            print(value.decode('ascii'), end='')
                            print('",')
                        print('"Units": "' + units + '"')
                        print('}')
                        print('],')
                    print(' "end": "TRUE"')
                    print('}')
                if MODE == 'slave' and data['addr'] == args.a and data['req'] in (ENQ,):
                    d = decode_msg(data)
                    if d['cmd'] == 0x60 and d['cmdsub'] == 0x01:
                        raw = struct.pack(DELTA_RPI_STRUCT, *DUMMY_DATA)
                        send(conn, ACK, 0x60, 0x01, data=raw, addr=args.a)
                    else:
                        print("This simulator only replies to cmd=0x60 cmdsub=0x01 requests...")
        
        
        if __name__ == "__main__":
            main()
        
      •  

      JSON output gegenereerd en gevalideerd:

          • met dmesg gechecked op welke USB poort de FT232RL aangesloten zit
        dmesg

        [155013.243404] usb 3-1: new full-speed USB device number 5 using ohci_hcd
        [155013.485049] usb 3-1: New USB device found, idVendor=0403, idProduct=6001
        [155013.491972] usb 3-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
        [155013.499369] usb 3-1: Product: FT232R USB UART
        [155013.503916] usb 3-1: Manufacturer: FTDI
        [155013.507892] usb 3-1: SerialNumber: A50285BI
        [155013.527773] ftdi_sio 3-1:1.0: FTDI USB Serial Device converter detected
        [155013.549271] usb 3-1: Detected FT232RL
        [155013.553223] usb 3-1: Number of endpoints 2
        [155013.557467] usb 3-1: Endpoint 1 MaxPacketSize 64
        [155013.562236] usb 3-1: Endpoint 2 MaxPacketSize 64
        [155013.567029] usb 3-1: Setting MaxPacketSize 64
        [155013.585881] usb 3-1: FTDI USB Serial Device converter now attached to ttyUSB0
          • M.b.v. het in de zip-file bijgesloten script unset_serial_ctrlchars.sh de USB poort vrijgemaakt van ctrl karakters
        foobar:~/python/delta-rpi# ls -lia
        total 112
        57353 drwxr-xr-x 4 root root 4096 Mar 23 10:05 .
        57352 drwxr-xr-x 4 root root 4096 Mar 23 09:59 ..
        57354 drwxr-xr-x 8 root root 4096 Mar 17 15:12 .git
        57428 -rw-r--r-- 1 root root 1067 Mar 17 15:12 LICENSE
        57429 -rw-r--r-- 1 root root 2721 Mar 17 15:12 README.md
        57434 drwxr-xr-x 2 root root 4096 Mar 17 15:32 __pycache__
        57430 -rw-r--r-- 1 root root 3816 Mar 17 15:12 crc16.py
        57431 -rw-r--r-- 1 root root 11970 Mar 17 15:12 delta-rpi.py
        57620 -rwxrwxrwx 1 root root 411 Mar 19 12:48 delta-solivia.sh
        57621 -rw-r--r-- 1 root root 12662 Mar 19 10:57 delta-solivia2.5.py
        57619 -rw-r--r-- 1 root root 12041 Mar 18 10:20 delta-solivia3.0.backup
        57618 -rw-r--r-- 1 root root 12913 Mar 18 16:09 delta-solivia3.0.py
        57622 -rw-r--r-- 1 root root 12385 Mar 18 11:52 hhh
        57432 -rw-r--r-- 1 root root 74 Mar 17 15:12 tty.settings
        57433 -rwxr-xr-x 1 root root 649 Mar 17 15:12 unset_serial_ctrlchars.sh

        foobar:~/python/delta-rpi# ./unset_serial_ctrlchars.sh /dev/ttyUSB0
        • JSON output gegenereerd met:
      python3 /root/python/delta-rpi/delta-solivia3.0.py -d /dev/ttyUSB0 -b 19200 -a 1 master

      -d /dev/ttyUSB0, de USB poort waarop de FT232RL adapter op aangesloten zit (uit vorige stap)

      -b 19200, baudrate conform instellingen Omvormer

      -a 1, RS485 adres van Omvormer

      master, python script gedraagt zich als master (Let op! Slave mode niet geïmplementeerd in script voor Delta Solivia 3.0 of 2.5)

        • JSON output geknipt in Terminal venster
      {
      "inverter": 1,
      "Datetime": "2021-03-23 19:41:10.373687",
      "SAP part number": [
      {
      "Value": "b'EOE46010287'",
      "Units": ""
      }
      ],
      "SAP serial number": [
      {
      "Value": "b'220287161303000538'",
      "Units": ""
      }
      ],
      "SAP date code": [
      {
      "Value": "31333033",
      "Units": ""
      }
      ],
      "SAP revision": [
      {
      "Value": "3136",
      "Units": ""
      }
      ],
      "Software Revision System controller": [
      {
      "Value": "01.02.37",
      "Units": "Major,Minor,Bug fixing"
      }
      ],
      "Software Revision Power controller": [
      {
      "Value": "00.00.00",
      "Units": "Major,Minor,Bug fixing"
      }
      ],
      "Software Revision ENS controller": [
      {
      "Value": "00.00.00",
      "Units": "Major,Minor,Bug fixing"
      }
      ],
      "Software Revision Watch dog controller": [
      {
      "Value": "00.00.00",
      "Units": "Major,Minor,Bug fixing"
      }
      ],
      "Software Revision DC controller": [
      {
      "Value": "00.00.00",
      "Units": "Major,Minor,Bug fixing"
      }
      ],
      "Software Revision DC 1": [
      {
      "Value": "00.00.00",
      "Units": "Major,Minor,Bug fixing"
      }
      ],
      "Software Revision DC 2": [
      {
      "Value": "00.00.00",
      "Units": "Major,Minor,Bug fixing"
      }
      ],
      "Software Revision DC 3": [
      {
      "Value": "00.00.00",
      "Units": "Major,Minor,Bug fixing"
      }
      ],
      "Software Revision AC": [
      {
      "Value": "00.00.00",
      "Units": "Major,Minor,Bug fixing"
      }
      ],
      "Software Revision AC 1": [
      {
      "Value": "00.00.00",
      "Units": "Major,Minor,Bug fixing"
      }
      ],
      "Software Revision AC 2": [
      {
      "Value": "00.00.00",
      "Units": "Major,Minor,Bug fixing"
      }
      ],
      "Software Revision AC 3": [
      {
      "Value": "00.00.00",
      "Units": "Major,Minor,Bug fixing"
      }
      ],
      "Software Revision reserved": [
      {
      "Value": "00.01.00",
      "Units": "Major,Minor,Bug fixing"
      }
      ],
      "Solar Power at Input 1": [
      {
      "Value": 0,
      "Units": "W"
      }
      ],
      "Solar Voltage at Input 1": [
      {
      "Value": 0,
      "Units": "V"
      }
      ],
      "Solar Current at Input 1": [
      {
      "Value": 0,
      "Units": "A"
      }
      ],
      "Solar Power at Input 2": [
      {
      "Value": 0,
      "Units": "W"
      }
      ],
      "Solar Voltage at Input 2": [
      {
      "Value": 0,
      "Units": "V"
      }
      ],
      "Solar Current at Input 2": [
      {
      "Value": 0,
      "Units": "A"
      }
      ],
      "Solar Power at Input 3": [
      {
      "Value": 0,
      "Units": "W"
      }
      ],
      "Solar Voltage at Input 3": [
      {
      "Value": 0,
      "Units": "V"
      }
      ],
      "Solar Current at Input 3": [
      {
      "Value": 0,
      "Units": "A"
      }
      ],
      "AC Current(Phase1)": [
      {
      "Value": 0,
      "Units": "A"
      }
      ],
      "AC Voltage(Phase1)": [
      {
      "Value": 0,
      "Units": "V"
      }
      ],
      "AC Frequency(Phase1)": [
      {
      "Value": 0,
      "Units": "Hz"
      }
      ],
      "AC Active Power(Phase1)": [
      {
      "Value": 0,
      "Units": "W"
      }
      ],
      "AC Reactive Power(Phase1)": [
      {
      "Value": 0,
      "Units": "VAR"
      }
      ],
      "AC Current(Phase2)": [
      {
      "Value": 0,
      "Units": "A"
      }
      ],
      "AC Voltage(Phase2)": [
      {
      "Value": 0,
      "Units": "V"
      }
      ],
      "AC Frequency(Phase2)": [
      {
      "Value": 0,
      "Units": "Hz"
      }
      ],
      "AC Active Power(Phase2)": [
      {
      "Value": 0,
      "Units": "W"
      }
      ],
      "AC Reactive Power(Phase2)": [
      {
      "Value": 0,
      "Units": "VAR"
      }
      ],
      "AC Current(Phase3)": [
      {
      "Value": 0,
      "Units": "A"
      }
      ],
      "AC Voltage(Phase3)": [
      {
      "Value": 0,
      "Units": "V"
      }
      ],
      "AC Frequency(Phase3)": [
      {
      "Value": 0,
      "Units": "Hz"
      }
      ],
      "AC Active Power(Phase3)": [
      {
      "Value": 0,
      "Units": "W"
      }
      ],
      "AC Reactive Power(Phase3)": [
      {
      "Value": 0,
      "Units": "VAR"
      }
      ],
      "Solar Isolation plus": [
      {
      "Value": 0,
      "Units": "kOhm"
      }
      ],
      "Solar Isolation minus": [
      {
      "Value": 0,
      "Units": "kOhm"
      }
      ],
      "Temperature amb": [
      {
      "Value": 0,
      "Units": "Celsius"
      }
      ],
      "Temperature heatsink": [
      {
      "Value": 0,
      "Units": "Celsius"
      }
      ],
      "Supplied ac energy (total)": [
      {
      "Value": 5835591,
      "Units": "Wh"
      }
      ],
      "Inverter runtime (total)": [
      {
      "Value": 655794,
      "Units": "Minutes"
      }
      ],
      "Status AC Output 1": [
      {
      "Value": 4,
      "Units": ""
      }
      ],
      "Status AC Output 2": [
      {
      "Value": 0,
      "Units": ""
      }
      ],
      "Status AC Output 3": [
      {
      "Value": 0,
      "Units": ""
      }
      ],
      "Status AC Output 4": [
      {
      "Value": 0,
      "Units": ""
      }
      ],
      "Internal Status 1": [
      {
      "Value": 0,
      "Units": ""
      }
      ],
      "Internal Status 2": [
      {
      "Value": 0,
      "Units": ""
      }
      ],
      "Internal Status 3": [
      {
      "Value": 0,
      "Units": ""
      }
      ],
      "Internal Status 4": [
      {
      "Value": 0,
      "Units": ""
      }
      ],
      "Supplied ac energy (day)": [
      {
      "Value": 9233,
      "Units": "Wh"
      }
      ],
      "Inverter runtime (day)": [
      {
      "Value": 745,
      "Units": "Minute"
      }
      ],
      "reserved": [
      {
      "Value": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
      "Units": ""
      }
      ],
      "end": "TRUE"
      }
        • en gevalideerd met https://jsonlint.com/

      Mosquitto MQTT cliënt geïnstalleerd

      • Mosquitto MQTT cliënt geïnstalleerd met:
      wget http://repo.mosquitto.org/debian/mosquitto-repo.gpg.key
      apt-key add mosquitto-repo.gpg.key
      apt-get update
      apt-get install mosquitto-clients

      Shell script gemaakt en crontab job aangemaakt

      foobar:~/python/delta-rpi# cat delta-solivia.sh
      #!/bin/bash
      export PYTHONIOENCODING=utf8 # Solivia 3.0
      message=`python3 /root/python/delta-rpi/delta-solivia3.0.py -d /dev/ttyUSB0 -b 19200 -a 1 master`
      mosquitto_pub -h 192.168.2.29 -p 1883 -m "$message" -t delta/inverter/1 # Solivia 2.5
      message=`python3 /root/python/delta-rpi/delta-solivia2.5.py -d /dev/ttyUSB0 -b 19200 -a 2 master`
      mosquitto_pub -h 192.168.2.29 -p 1883 -m "$message" -t delta/inverter/2
        • Zet execute rechten op het script met: chmod 744 delta-solivia.sh
      •  
        • crontab van root aangepast met
      crontab -e

      # Edit this file to introduce tasks to be run by cron.
      #
      # Each task to run has to be defined through a single line
      # indicating with different fields when the task will be run
      # and what command to run for the task
      #
      # To define the time you can provide concrete values for
      # minute (m), hour (h), day of month (dom), month (mon),
      # and day of week (dow) or use '*' in these fields (for 'any').#
      # Notice that tasks will be started based on the cron's system
      # daemon's notion of time and timezones.
      #
      # Output of the crontab jobs (including errors) is sent through
      # email to the user the crontab file belongs to (unless redirected).
      #
      # For example, you can run a backup of all your user accounts
      # at 5 a.m every week with:
      # 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
      #
      # For more information see the manual pages of crontab(5) and cron(8)
      #
      # m h dom mon dow command
      */5 * * * * /root/python/delta-rpi/delta-solivia.sh

      en de regel */5 * * * * /root/python/delta-rpi/delta-solivia.sh toegevoegd zodat iedere 5 minuten de omvormers uitgelezen worden en de JSON output middels MQTT op de topic delta/inverter/* wordt geplaatst

      Node-red flow aangemaakt:

      • Deze flow zorg ervoor dat de JSON berichten van de omvormers resulteren in een record in een tabel in de MariaDb database

      Node

      Doel

      MQTT in

      Leest topic delta/inverter/* uit

      Inverter

      Op basis van inverternummer (delta/inverter/1 of /delta/inverter/2) wordt de juiste functie aangeroepen om de JSON payload om te zetten in een SQL-statement

      Inverter 1

      Zet JSON payload om naar een SQL-statement

      Inverter 2

      Zet JSON payload om naar een SQL-statement

      Database

      Database connectie naar MariaDB

      Msg.payload

      Voor debugging doeleinden

        • Export van flow
      [{"id":"6a438d02.3617b4","type":"tab","label":"PVOutput","disabled":false,"info":""},{"id":"a5f5f935.15d6e8","type":"mqtt in","z":"6a438d02.3617b4","name":"MQTT in","topic":"delta/inverter/#","qos":"2","datatype":"json","broker":"ede154bd.f75f38","x":120,"y":200,"wires":[["b30a4fd9.11196","ef3d4c35.3b56","fe49f977.93b998"]]},{"id":"f9f1fbc3.9ae3d8","type":"function","z":"6a438d02.3617b4","name":"Inverter 1","func":"// https://pvoutput.org/service/r2/addoutput.jsp?key=Your-API-Key&sid=Your-System-Id&d=20100830&g=12000\n//var apiKey=\"753e8f6c6b5c5be53ea92a1c3d1e48f46d302ae7\";\n//var systemId=\"82717\";\n//var url = \"https://pvoutput.org/service/r2/addoutput.jsp?key=\" + apiKey + \"&sid=\" + systemId;\nvar sql = \"insert into pv (`inverter`, `sample datetime`, `Solar Power`, `Solar Voltage`, `Solar Current`, `AC Power`, `AC Voltage`, `AC Current`, `AC Frequency`, `Temperature`, `Status AC Output`, `Supplied AC Energy`, `Inverter runtime`)\"\nsql = sql + \"values (:inverter, :sample_datetime, :Solar_Power, :Solar_Voltage, :Solar_Current, :AC_Power, :AC_Voltage, :AC_Current, :AC_Frequency, :Temperature, :Status_AC_Output, :Supplied_AC_Energy, :Inverter_runtime);\";\n\n//var datetime=msg.payload.Datetime.substr(0,4) + msg.payload.Datetime.substr(5,2) + msg.payload.Datetime.substr(8,2);\n//var generated=msg.payload[\"Supplied ac energy (day)\"][0].Value;\n\n// url=url + \"&d=\" + datetime + \"&g=\" + generated + \"&tm=\" + flow.get(\"minTemp\") + \"&tx=\" + flow.get(\"maxTemp\") + \"&cd=\" + flow.get(\"situatie\");\n\nmsg.payload.inverter=1;\nmsg.payload.sample_datetime=msg.payload.Datetime;\nmsg.payload.Solar_Power=msg.payload[\"Solar Power at Input 1\"][0].Value;\nmsg.payload.Solar_Voltage=msg.payload[\"Solar Voltage at Input 1\"][0].Value;\nmsg.payload.Solar_Current=msg.payload[\"Solar Current at Input 1\"][0].Value;\nmsg.payload.AC_Power=msg.payload[\"AC Active Power(Phase1)\"][0].Value;\nmsg.payload.AC_Voltage=msg.payload[\"AC Voltage(Phase1)\"][0].Value;\nmsg.payload.AC_Current=msg.payload[\"AC Current(Phase1)\"][0].Value;\nmsg.payload.AC_Frequency=msg.payload[\"AC Frequency(Phase1)\"][0].Value;\nmsg.payload.Temperature=msg.payload[\"Temperature amb\"][0].Value;\nmsg.payload.Status_AC_Output=msg.payload[\"Status AC Output 1\"][0].Value;\nmsg.payload.Supplied_AC_Energy=msg.payload[\"Supplied ac energy (day)\"][0].Value;\nmsg.payload.Inverter_runtime=msg.payload[\"Inverter runtime (day)\"][0].Value;\n\nmsg.topic=sql;\nnode.war\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1020,"y":160,"wires":[["ea031847.3a63e8"]]},{"id":"b30a4fd9.11196","type":"http request","z":"6a438d02.3617b4","d":true,"name":"Buitentemperatuur","method":"GET","ret":"obj","paytoqs":"ignore","url":"http://192.168.2.29:8080/json.htm?type=graph&sensor=temp&idx=87&range=day","tls":"","persist":false,"proxy":"","authType":"","x":510,"y":320,"wires":[["edae57f5.bf3318"]]},{"id":"edae57f5.bf3318","type":"function","z":"6a438d02.3617b4","d":true,"name":"","func":"var arrayDayTemp = msg.payload.result;\nvar minTemp=99;\nvar maxTemp=0;\n\narrayDayTemp.forEach((element) => {\n if (element.te <= minTemp){\n minTemp=element.te;\n }\n \n if (element.te >= maxTemp){\n maxTemp=element.te;\n }\n\n})\n\nflow.set(\"minTemp\",minTemp);\nflow.set(\"maxTemp\",maxTemp);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":760,"y":320,"wires":[[]]},{"id":"ef3d4c35.3b56","type":"http request","z":"6a438d02.3617b4","d":true,"name":"Barometer","method":"GET","ret":"obj","paytoqs":"ignore","url":"http://192.168.2.29:8080/json.htm?type=graph&sensor=temp&idx=88&range=day","tls":"","persist":false,"proxy":"","authType":"","x":490,"y":440,"wires":[["872ab880.c03fc8"]]},{"id":"872ab880.c03fc8","type":"function","z":"6a438d02.3617b4","d":true,"name":"","func":"var arrayDayBaro = msg.payload.result;\nvar maxBaro=0;\nvar situatie=\"\";\n\narrayDayBaro.forEach((element) => {\n if (element.ba >= maxBaro){\n maxBaro=element.ba;\n }\n\n})\n\nif (maxBaro >= 1050) {\n situatie=\"Fine\";\n}\nelse if (maxBaro >= 1035) {\n situatie=\"Fine\";\n}\nelse if (maxBaro >= 1025) {\n situatie=\"Fine\";\n}\nelse if (maxBaro >= 1010) {\n situatie=\"Not Sure\";\n}\nelse if (maxBaro >= 995) {\n situatie=\"Showers\";\n}\nelse if (maxBaro >= 980) {\n situatie=\"Showers\";\n}\nelse if (maxBaro &gt;= 970) {\n situatie=\"Cloudy\";\n}\nelse if (maxBaro >= 955) {\n situatie=\"Cloudy\";\n}\nelse {\n situatie=\"Cloudy\";\n}\n\nflow.set(\"situatie\",situatie);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":760,"y":440,"wires":[[]]},{"id":"9be65ffe.8eaf2","type":"debug","z":"6a438d02.3617b4","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1650,"y":200,"wires":[]},{"id":"fe49f977.93b998","type":"switch","z":"6a438d02.3617b4","name":"Inverter","property":"payload.inverter","propertyType":"msg","rules":[{"t":"eq","v":"1","vt":"str"},{"t":"eq","v":"2","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":780,"y":200,"wires":[["f9f1fbc3.9ae3d8"],["948e290b.2481a8"]]},{"id":"948e290b.2481a8","type":"function","z":"6a438d02.3617b4","name":"Inverter 2","func":"var sql = \"insert into pv (`inverter`, `sample datetime`, `Solar Voltage`, `Solar Current`, `AC Power`, `AC Voltage`, `AC Current`, `AC Frequency`, `Temperature`, `Status AC Output`, `Supplied AC Energy`, `Inverter runtime`)\"\nsql = sql + \"values (:inverter, :sample_datetime, :Solar_Voltage, :Solar_Current, :AC_Power, :AC_Voltage, :AC_Current, :AC_Frequency, :Temperature, :Status_AC_Output, :Supplied_AC_Energy, :Inverter_runtime);\";\n\nmsg.payload.inverter=2;\nmsg.payload.sample_datetime=msg.payload.Datetime;\nmsg.payload.Solar_Voltage=msg.payload[\"Solar Voltage at Input 1\"][0].Value;\nmsg.payload.Solar_Current=msg.payload[\"Solar Current at Input 1\"][0].Value;\nmsg.payload.AC_Power=msg.payload[\"AC Power\"][0].Value;\nmsg.payload.AC_Voltage=msg.payload[\"AC Voltage\"][0].Value;\nmsg.payload.AC_Current=msg.payload[\"AC Current\"][0].Value;\nmsg.payload.AC_Frequency=msg.payload[\"AC Frequency\"][0].Value;\nmsg.payload.Temperature=msg.payload[\"Calculated temperature at ntc (AC side)\"][0].Value;\nmsg.payload.Status_AC_Output=msg.payload[\"Status AC output\"][0].Value;\nmsg.payload.Supplied_AC_Energy=msg.payload[\"Supplied ac energy\"][0].Value;\nmsg.payload.Inverter_runtime=msg.payload[\"Inverter runtime\"][0].Value * 60;\n\nmsg.topic=sql;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1020,"y":240,"wires":[["ea031847.3a63e8"]]},{"id":"ea031847.3a63e8","type":"mysql","z":"6a438d02.3617b4","mydb":"8056d2ce.5f8c8","name":"Database","x":1320,"y":200,"wires":[["9be65ffe.8eaf2"]]},{"id":"ede154bd.f75f38","type":"mqtt-broker","z":"","name":"pi","broker":"192.168.2.29","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"8056d2ce.5f8c8","type":"MySQLdatabase","z":"","name":"mysql","host":"192.168.2.29","port":"3306","db":"energy","tz":"","charset":"UTF8"}]

      MariaDB geconfigureerd

      • Mysql tool gestart met mysql -u root -p
      • Database “energy” aangemaakt met
      create database energy;
        • Tabel “pv” aangemaakt met
      use energy;
      
      CREATE TABLE `pv` (
        `inverter` int(1) NOT NULL,
        `sample datetime` datetime NOT NULL,
        `Solar Power` int(32) DEFAULT NULL,
        `Solar Power unit` char(6) DEFAULT 'W',
        `Solar Voltage` int(32) DEFAULT NULL,
        `Solar Voltage unit` char(6) DEFAULT 'V',
        `Solar Current` int(32) DEFAULT NULL,
        `Solar Current unit` char(6) DEFAULT 'A',
        `AC Power` int(32) DEFAULT NULL,
        `AC Power unit` char(6) DEFAULT 'W',
        `AC Voltage` int(32) DEFAULT NULL,
        `AC Voltage unit` char(6) DEFAULT 'V',
        `AC Current` int(32) DEFAULT NULL,
        `AC Current unit` char(6) DEFAULT 'A',
        `AC Frequency` int(32) DEFAULT NULL,
        `AC Frequency unit` char(6) DEFAULT 'Hz',
        `Temperature` int(32) DEFAULT NULL,
        `Temperature unit` char(6) DEFAULT 'C',
        `Status AC Output` int(32) DEFAULT NULL,
        `Supplied AC Energy` int(32) DEFAULT NULL,
        `Supplied AC Energy unit` char(6) DEFAULT 'Wh',
        `Inverter runtime` int(32) DEFAULT NULL,
        `Inverter runtime unit` char(6) DEFAULT 'Minute',
        PRIMARY KEY (`inverter`,`sample datetime`)
      );
        • gebruiker “googledata” aangemaakt met select rechten op tabel pv
      create user googledata@% identified by 'password';
      grant select on energy.* to googledata@%;

      Firewall aangepast en poort naar MariaDB database opengezet:

      Dashboard aangemaakt met Google Data Studio:

        • Login in met je Google account op https://datastudio.google.com/
        • Voor de 3 scoreboards heb ik de volgende MySQL verbinding gedefinieerd
        • Op dezelfde wijze heb ik de volgende MySQL verbindingen gemaakt voor elementen op het dashboard

      Dashboard element

      SQL-query

      Inverter: 1,2

      Vandaag,

      Deze maand,

      Totaal

      select inverter,

             date(`sample datetime`) as datum,

             max(time(`sample datetime`)) as tijd,

             round((max(`Supplied AC Energy`)-min(`Supplied AC Energy`))/10) as energy,

             `Supplied AC Energy unit` as unit

      from pv where inverter = 2

      group by date(`sample datetime`)

      union

      select inverter,

             date(`sample datetime`) as datum,

             max(time(`sample datetime`)) as tijd,

             round(max(`Supplied AC Energy`)/1000) as energy,

             `Supplied AC Energy unit` as unit

      from pv where inverter = 1

      group by date(`sample datetime`);

      Laatste update

      select max(time(`sample datetime`)) as tijd

      from pv

      where date(`sample datetime`) = curdate()

      Inverter temperature, Inverter voltage,

      Inverter solarpower

      select inverter,

             temperature,

             `AC Voltage` as voltage,

             `Solar Power` as solarpower

      from pv

      where `sample datetime` =

        (select max(`sample datetime`)

         from pv

         where inverter = 1)

      union

      select inverter,

             temperature,

             `AC Voltage` as voltage,

             `Solar Power` as solarpower

      from pv

      where `sample datetime` =

        (select max(`sample datetime`)

         from pv

         where inverter = 2)

      Opgewekte energie Inverter 1,

      Opgewekte energie Inverter 2

      select inverter,

             `sample datetime` as T,

             `Supplied AC Energy`/1000 as A,

             (select min(`Supplied AC Energy`/1000)

              from pv

              where inverter = 1

              and date(`sample datetime`) = curdate()) as B,

              (`Supplied AC Energy` – (select min(`Supplied AC Energy`)

                                      from pv

                                      where inverter = 1

                                      and date(`sample datetime`) = curdate()))/1000 as C

      from pv

      where inverter = 1

      and date(`sample datetime`) = curdate();

      • Hieronder screenshots van de configuratie van de verschillende dashboard onderdelen:

      En dit is het resultaat van al die noeste arbeid

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *