SBFspot aanpassingen voor SMA TriPower 5.0

Door Pixelmagic op zondag 23 februari 2020 14:00 - Reacties (1)
Categorie: -, Views: 1.662

Vanwege de nieuwe zonnepanelen heb ik ook een nieuwe inverter, namelijk en 5Kw SMA TriPower 3 fase inverter. Hiervoor had ik een AEG welke zelfstandig naar PVOutput logde dmv een insteekkaartje wat een SMA even wennen.

Ik heb zelf een Linux server draaien dus na wat zoeken ben ik SBFspot (https://github.com/SBFspot/SBFspot tegengekomen en geinstalleerd op de server waar ook Domoticz draait.

Wat ik hier gedaan heb is vanuit mijn eigen comfort zone en niet noodzakelijk de beste manier. Ik heb geen opleiding als programmeur gevolg maar ben een home-mad(e) knutselaar. Dus als het niet doet wat het moet doen bij jou aub niet schelden op mij. Opbouwend commentaar is uiteraard welkom. De combinatie van Python en PHP vinden sommige mensen misschien vreemd, ik vind het prettig omdat ik wat meer ervaring heb met en dus comfortabel ben met PHP.

Na wat uitzoeken uiteindelijk aan de praat, ik wilde namelijk niet naar SQLite loggen maar naar een bestaande MySQL DB welke ik gebruik, ik heb namelijk wat meer met PHP en dan is de bekende weg naar MySQL voor mij simpeler. Het duurde even voordat ik doorhad dat de standaard make voor SBFspot aangepast moet worden (make mysql en make install_mysql) en dan werkt het prima.

Enige nadeel blijkt te zijn dat SBFspot voor bepaalde inverters niet de interne temperatuur logt. Aangezien ik voor mijn AEG inverter exact bijhield wat de temp was en daar ook een fan op aanstuurde indien hij boven de 40-45 graden kwam wil ik dat hier ook weer kunnen doen.

Na wat zoeken ben ik een Python script tegengekomen wat de registers van de SMA uit kan lezen dmv de Modbus. Ik heb het script wat aangepast zodat het alleen doet wat ik wil. Het bestaat uit twee delen waarvan de eerste een JSON string uitspoegt die ik dmv PHP opvraag en vervolgens in de SpotData DB plaats op de rij die door SBFspot is geplaatst. Tegelijk neem ik ook de Aardlekstroom mee en plaats ik deze in de tabel.

Om te beginnen moet SBFspot aangepast worden om toe te staan dat er een extra kolom in de DB aanwezig is. Het gaat om het bestand db_MySQL_Export.cpp regel 251:
code:
1
(float)inv[i]->Temperature/100 << ")" ;

aanpassen naar
code:
1
2
(float)inv[i]->Temperature/100 << ',' <<
(float)inv[i]->Temperature/100 << ")" ;

Dan een make mysql en make install_mysql

Vervolgens de SpotData DB uitbreiden met een extra kolom:
code:
1
ALTER TABLE `SpotData` ADD `Aardlekstroom` FLOAT NOT NULL AFTER `Temperature`;


Nu word er netjes 0 in de kolom Aardlekstroom neergezet (SFBspot even draaien en kijken of er geen fouten zijn.

Dan de twee Python bestanden, als eerste is smalogger.py, deze moet uitvoerbaar zijn en heeft wat modules nodig die je met pip3 moet installeren.

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/python3

import smamodbus
import re
import time
import config
import json

interval=10
# vul hier je eigen IP nummer van de SMA omvormer in 
MODBUS_URL = 'xxx.xxx.xxx.xxx'
MODBUS_UNIT_ID = 3

sma = smamodbus.SMAModbus(MODBUS_URL,MODBUS_UNIT_ID)

lastPVOutput = None

dataActual = sma.readModbus()

for tag,v in dataActual.items():
    dataActual[tag] = float(v)
    #print(tag,dataActual[tag])

data = json.dumps(dataActual)
print(data)

quit()


Dan de tweede file wat daarwerkelijk de waardes ophaalt bij de inverter, hier heb ik wat aanpassingen gedaan in namen en adressen. Het bestand heeft smamodbus.py en moet in dezelfde directory staan:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from pymodbus.client.sync import ModbusTcpClient
import logging
import struct
modbus_fields = {
    'ETotal': {'address': 30529, 'datatype': 'U32', 'scale': 1, 'unit': 'kWh','min': 0,'max': 4000.*100},
    'TempHeatsink': {'address': 30953, 'datatype': 'S32', 'scale': 10, 'unit': 'C','min': 0,'max': 200.},
    'Status': {'address': 40029, 'datatype': 'U32', 'scale': 1, 'unit': '','min': 0,'max':16777213},

    'DCPower': {'address': 30773, 'datatype': 'S32', 'scale': 1, 'unit': 'kWh','min': 0,'max': 4e3},
    'DCVoltage': {'address': 30771, 'datatype': 'S32', 'scale': 100, 'unit': 'V','min': 0,'max': 1e3},
    'DCCurrent': {'address': 30769, 'datatype': 'S32', 'scale': 1000, 'unit': 'A','min': 0,'max': 100.},

    'AC1Power': {'address': 30777, 'datatype': 'U32', 'scale': 1, 'unit': 'kW','min': 0,'max': 1e4},
    'AC1Voltage': {'address': 30783, 'datatype': 'U32', 'scale': 100, 'unit': 'V','min': 0,'max': 500.},
    'AC1Current': {'address': 30977, 'datatype': 'S32', 'scale': 1000, 'unit': 'A','min': 0,'max': 100.},

    'AC2Power': {'address': 30779, 'datatype': 'U32', 'scale': 1, 'unit': 'kW','min': 0,'max': 1e4},
    'AC2Voltage': {'address': 30785, 'datatype': 'U32', 'scale': 100, 'unit': 'V','min': 0,'max': 500.},
    'AC2Current': {'address': 30979, 'datatype': 'S32', 'scale': 1000, 'unit': 'A','min': 0,'max': 100.},

    'AC3Power': {'address': 30981, 'datatype': 'S32', 'scale': 1000, 'unit': 'A','min': 0,'max': 100.},
    'AC3Voltage': {'address': 30787, 'datatype': 'U32', 'scale': 100, 'unit': 'V','min': 0,'max': 500.},
    'AC3Current': {'address': 30981, 'datatype': 'S32', 'scale': 1000, 'unit': 'A','min': 0,'max': 100.},

    'Aardlekstroom': {'address': 31247, 'datatype': 'S32', 'scale': 100, 'unit': 'A','min': 0,'max': 1000.},


}

class SMAModbus:
    def __init__(self,url,unit_id):
        logging.info('Connecting to Modbus server: {}'.format(url))
        # hier nogmaals je eigen IP nummer van SMA omvormer invullen
        self._client = ModbusTcpClient('xxx.xxx.xxx.xxx')
        self._client.connect()
        logging.info('Connected')
        self._unit_id = unit_id

    def readModbus(self):
        if not self._client.is_socket_open():
            self._client.connect()
        data = {}
        for tag,cfg in modbus_fields.items():
            result = self._client.read_holding_registers(cfg['address'], 2, unit=self._unit_id)
            if result.isError():
                logging.error('Reading registers for {} gave an error'.format(tag))
                continue
            w1 = struct.pack('H', result.registers[0]) # Assuming register values are unsigned short's
            w2 = struct.pack('H', result.registers[1]) # Assuming register values are unsigned short's
            if cfg['datatype'] == 'S32':
                v = struct.unpack('i', w2 + w1)
            elif cfg['datatype'] == 'U32':
                v = struct.unpack('I', w2 + w1)
            else:
                raise NotImplementedError('Datatype {} is not implemented'.format(cfg['datatype']))
            v = v[0]/cfg['scale']
            if v<cfg['min'] or v>cfg['max']:
                logging.error('Value out of range for {}: {} < {} < {} [{}]'.format(tag,cfg['min'],v,cfg['max'],cfg['unit']))
                logging.error('Raw data: {}.{}'.format(result.registers[0],result.registers[1]))
                #skip output
                continue
            v =  (result.registers[0]*2**16+result.registers[1])/cfg['scale']
            # check for negative or larger than 100y of output
            if v<0 or v>100*3000*1000:
                v=0.
            data[tag] = v
        return data


Let op, je moet in beide files het IP nummer van je SMA omvormer invullen.

Wanneer je nu smalogger.py start zou je een json string als antwoord moeten krijgen.

Dan nu de php code, in totaal 2 bestanden, 1 voor de mysqli connectie en 1 die de data ophaalt vanuit het Python bestand en wegschrijft in de DB.

mysql.inc.php
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$servername = "dbserver";
$username = "username";
$password = "password";

// Create connection
$db = new mysqli($servername, $username, $password);

// Check connection
if ($db->connect_error) {
    die("DB Connection failed: " . $db->connect_error);
}
?>


en het daadwerkelijke script (b.v. set_smadata.php)

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?php
require_once('/pad_naar/mysqli.inc.php');
/*
40029 status waardes:
    295: MPP (Mpp)
    381: Stop (Stop)
    443: Constante spanning (VolDCConst)
    455: Waarschuwing (Wrn)
    1392: Fout (Flt)
    1393: Wacht op PV-spanning (WaitPV)
    1467: Start (Str)
    1469: Uitschakelen (Shtdwn)
    1480: Wacht op energieleverancier (WaitUtil)
    2119: Afregeling (Drt)
    16777213: (NaNStt)

*/

echo '<pre>';
echo date("d-m-Y H:i:s").chr(10);
# 10 seconde wachten zodat cron job op SFBspot gedraaid heeft
if (!ISSET($_SERVER['HTTP_USER_AGENT'])) sleep(10);
# laatste insert van SpotData ophalen
$sql = "SELECT * FROM `SBFspot`.`SpotData` ORDER BY `SpotData`.`TimeStamp` DESC  LIMIT 1";
$result = mysqli_query($db,$sql) or die("Error: " . mysqli_error($db));
$row = mysqli_fetch_array($result);
$cur_datetime = date("d-m-Y H:i");
$last_datetime = date("d-m-Y H:i",$row['TimeStamp']);
$last_timestamp = $row['TimeStamp'];
$last_dc_power = $row['Pdc1'];
$last_etotal = $row['ETotal'];
# maximum verschil in etotal
$diff_etotal = 50;

# locatie van het python script
$command = escapeshellcmd('/var/www/192.168.1.13/modbus/smalogger.py');
# data ophalen van python modbus script
$output = shell_exec($command);

# json omzetten naar array
$data = json_decode($output, true);

# array sorteren op naam
ksort($data);

if ($cur_datetime == $last_datetime) {
    echo 'Minuut identiek'.chr(10);
    #if ($last_etotal == $data['ETotal']) {
    if (abs($last_etotal - $data['ETotal']) < $diff_etotal) {
        echo 'DC Power identiek binnen '.$diff_etotal.' ('.$last_etotal.' '.$data['ETotal'].')'.chr(10);
        $temp = $data['TempHeatsink'];
        $aardlek = $data['Aardlekstroom'];
        # update van temp doen
        $sql = "UPDATE `SBFspot`.`SpotData` SET 
                `Temperature` = '$temp',
                `Aardlekstroom` = '$aardlek'
                WHERE `SpotData`.`TimeStamp` = '$last_timestamp';";
        $result = mysqli_query($db,$sql) or die("Error: " . mysqli_error($db));
    } else {
        echo $last_etotal.' '.$data['ETotal'].chr(10);
        echo abs($last_etotal - $data['ETotal']);
    }   
}
?>


Als je deze draait vanaf de commandline met php zou er wat tekst moeten komen (na 10 seconde!!) en met wat geluk zijn er waardes in de DB aangepast.
Ik heb de php elke minuut dmc cron draaien, de 10 seconde wachten is voor mijzelf bedoeld omdat de SBFspot cronjob op een andere virtuele server draait dan het PHP script, hierdoor weet ik redelijk zeker dat het PHPO script draait nadat de insert door SFBspot is gedaan.