Kopano to grommunio HowTo und DEYE Wechselrichter: Unterschied zwischen den Seiten

Aus Neobikers Wiki
(Unterschied zwischen Seiten)
Zur Navigation springen Zur Suche springen
 
 
Zeile 1: Zeile 1:
= Installation auf Debian 12 =
= Balkonkraftwerk: DEYE Wechselrichter ohne Cloud auslesen und per MQTT monitoren =
Eine fertige grommunio Appliance ist auf Basis von S.u.S.E. erstellt und steht u.a. als ISO download zur Verfügung. Da ich derzeit ausschliesslich Debian basierte Systeme verwende, bevorzuge ich eine grommunio Installation auf Debian 12 (bookworm).


Im grommunio Forum findet sich ein Diskussionsthread für die [https://community.grommunio.com/d/447-debian-11-clean-install-script grommunio Installation auf Debian 12 durch ein Script]. Diese läuft auf einer neu erstellten VM auf Proxmox Server fehlerfrei durch. In einem LXC Container gab es Probleme, u.a. klemmte der POP3 Service.
Anfang März habe ich endlich ein '''Balkonkraftwerk''' über Amazon bestellt und installiert. Es ist ja inzwischen keine Raketenwissenschaft mehr, und die Akzeptanz und Möglichkeiten zur einfachen Installation per Do-It-Yourself haben mich nun endlich Fakten schaffen lassen.


Im Anschluss an das Installationsskript habe ich noch folgendes Script unter '''Additions''' - ''Fix Grommunio Admin Live Status page'' ausgeführt, welches ebenfalls funktionierte und die entsprechende Seite im Web Interface reparierte.
Ich habe '''zwei Standard PV-Module''' je 410W samt einem passendem '''DEYE Wechselrichter''' (WR) mit Anschlusskabel als Komplettpaket bestellt. Da ich am Balkon schon eine Klimaanlage stehen habe, ist der Stromanschluss auch kein Problem gewesen. Und endlich gilt im Sommer: Wenn es heiß ist und die Klimaanlage läuft, produzieren die PV-Module ordentlich Strom dafür, außerdem muss ich dann keinen Überschuss verschenken.


= Konfiguration =
Zum Start hatten die DEYE Wechselrichter, die meines Wissens identisch zu z.B. ''Bosswerk'', ''Blaupunkt'' und ''Turbo Energy'' sind, allerdings noch ein Security Problem mit der Firmware, darüber hatte u.a. ''Heise'' berichtet. Ein ''Firmware Upgrade'' stand kurz darauf zur Verfügung und wurde automatisch eingespielt.
Kopano Core ist bei mir als Applikation auf meinem UCS File- und Mailserver installiert. Die Benutzerkonten und deren Konfiguration werden von UCS verwaltet und stehen per LDAP zur Verfügung.


<pre># mein UCS-Kopano Server
Die DEYE Wechselrichter haben bereits WLAN eingebaut, d.h. man steckt die wirklich nur in die Steckdose und verbindet anschliessend die PV-Module und es geht los. Die Einbindung ins eigene WLAN erfolgt wie üblich mittels eingebautem WLAN Accesspoint und internem Webserver, worüber man die Konfiguration vornimmt und dann am besten den WLAN-AP deaktiviert. Die Internetverbindung des Wechselrichters habe ich dann über meine Fritzbox gesperrt.
kopano_server=ucsmail
echo "kopano_server=ucsmail" >> ~/.bashrc


# ssh connections without prompts
Der WR ist '''über WLAN nur erreichbar, wenn die PV-Module eine Spannung (DC) erzeugen'''. Der Anschluss an das Stromnetz (AC) ist hierfür irrelevant. Das heisst wenn es dunkel ist, ist er nicht erreichbar bzw. aus.
ssh-keygen
ssh-copy-id $kopano_server


# mount /var/lib/kopano/attachments per sshfs
Der WR bietet verschiedene Möglichkeiten zur Konfiguration und Abfrage der aktuellen Werte:
if [ -e /etc/debian_version ] then
* einen internen Webserver (http://10.10.10.254)
    apt install sshfs screen
* die Cloud Anbindung zu Solarman (Solarman Smart App)
else
* (angepasstes) Modbus Protokoll über Port 8899
    zypper in sshfs          # (grommunio app/iso installation)
* AT+ Befehle über Port 48899
fi
 
mkdir -p /mnt/kopano_attachments
Ich verwende das Modbus Protokoll zum auslesen des WR, das
sshfs $kopano_server:/var/lib/kopano/attachments /mnt/kopano_attachments
# effizient/schnell ist (Performance)
# ohne Internetverbindung auskommt
 
Für DEYE kompatible WR stehen inzwischen einige Lösungsansätze zur Verfügung. Ich verwende die '''[https://github.com/kbialek/deye-inverter-mqtt DEYE Inverter MQTT Bridge]''' von ''Krzysztof Białek'', da ich hier bereits einen '''MQTT Server''' innerhalb der Heimautomatisierungslösung '''FHEM''' zur Einbindung meiner '''Tasmota''' Devices (Shelly; Hichi) eingerichtet habe.
 
Diese Lösung ist als Docker Container konzipiert, was mir aber unnötiger Overhead ist. Da es in Python realisiert ist, starte ich das Skript ganz einfach direkt, ohne Docker.
 
== Wechselrichter Eigenheiten ==
Der Wechselrichter läuft nur, wenn die PV-Module eine Gleichspannung einspeisen - d.h. wenn es draussen hell ist. Ein Auslesen der Werte des WR funktioniert also nicht, wenn es dunkel ist (Timeout). Ausserdem wird der Tageszähler erst durch setzen von Uhrzeit/Datum wieder auf 0 gesetzt, was über eine Internetverbindung erfolgt. Ist die gesperrt, läuft der Tageszähler weiter.
 
Also, jeden Abend geht der WR aus, und sobald er beim ersten Tageslicht wieder startet möchte ich Datum und Uhrzeit setzen und anschliessend die Werte kontinuierlich auslesen und über MQTT ausgeben. Bis dahin ist der WR aber halt nicht erreichbar.
 
== Installation ==
Das Projekt von Github laden und in einem Verzeichnis speichern, '''Python''' ist normalerweise ja schon installiert.
 
<pre>git clone https://github.com/kbialek/deye-inverter-mqtt
pip install paho-mqtt
cd deye-inverter-mqtt</pre>
 
Unter Debian habe ich im Skript '''deye_cli.sh''' python durch python3 ersetzen müssen:
<pre>#!/bin/bash
set -a; source config.env; set +a
python3 deye_cli.py "$@"
</pre>
</pre>


== Zertifikate ==
=== config.env ===
Falls ein Server Zertifikat von UCS für den grommunio server erzeugt wurde kann das verwendet werden.
Die Konfiguration meines DEYE Wechselrichters vom Typ Micro-Inverter:
Damit funktioniert die LDAP Verbindung auch zum einloggen mit starttls.
'''config.env'''
<pre>
<pre>DEYE_LOGGER_IP_ADDRESS=<IP Wechselrichter im WLAN>
cd /etc/grommunio-common/ssl
DEYE_LOGGER_PORT=8899
scp $kopano-server:/etc/univention/ssl/grommunio/cert.pem server-bundle.pem
DEYE_LOGGER_SERIAL_NUMBER=<Seriennummer des WR>
scp $kopano-server:/etc/univention/ssl/grommunio/private.key server.key
chown gromox:gromox server*
chmod 660 server*


# Root CA
MQTT_HOST=<IP von MQTT Server>
if [ -e /etc/debian_version ] then
MQTT_PORT=1883
    scp $kopano_server:/etc/univention/ssl/ucsCA/CAcert.pem /usr/local/share/ca-certificates/my-custom-ca/
MQTT_USERNAME=
else
MQTT_PASSWORD=
    # SuSE appliance location
MQTT_TOPIC_PREFIX=deye
    scp $kopano_server:/etc/univention/ssl/ucsCA/CAcert.pem /etc/pki/trust/anchors/
 
fi
LOG_LEVEL=ERROR
update-ca-certificates
DEYE_DATA_READ_INTERVAL=60
DEYE_METRIC_GROUPS=micro
</pre>
</pre>


== LDAP ==
=== deye_inverter.sh === 
Die LDAP Konfiguration kann mit dem UCS Template im Webinterface vorgenommen werden.
Ausserdem habe ich ein kleines ''Wrapper Skript'' geschrieben, um '''Daten des WR''' einfacher '''lesen und schreiben''' zu können:<br>
Ich übernehme einige Kopano Werte:
 
<pre>
'''deye_inverter.sh''' [--check <pause>] --read <register> | --write <register> <value>
ssh $kopano_server grep -e ^ldap_uri -e ^ldap_bind -e ^ldap_search_base -e ^ldap_user_search -e ^ldap_group_search /etc/kopano/ldap.cfg


ldap_uri = ldap://ucsmail.domain.de:7389/
<pre>#!/bin/bash
ldap_bind_user = cn=ucsmail,cn=dc,cn=computers,dc=domain,dc=de
# Read / Write DEYE Inverters
ldap_bind_passwd = xxxxxxxxxxxxxx
#  via deye_inverter_mqtt python package
ldap_search_base = dc=domain,dc=de
#  https://github.com/kbialek/deye-inverter-mqtt
ldap_user_search_filter = (kopanoAccount=1)
ldap_group_search_filter = (&(kopanoAccount=1)(objectClass=kopano-group))


# configure LDAP accordingly
retries=2      # retry deye_inverter command multiple times
grommunio-admin ldap configure
sleep=5        # sleep time between retries


# test: list users
# locate script in deye_inverter_mqtt directory
grommunio-admin ldap search
cd $(dirname $0)
ID                                    Type  E-Mail                  Name
if [ ! -f ./deye_cli.sh ]; then
2222222222-11-10111-22222-3333333333  user  neobiker@neobiker.de    Neobiker
  echo "Error: ./deye_cli.sh not found."
</pre>
  echo "      Please move $(basename $0) in deye-inverter-mqtt directory."
  exit 1
fi
. ./config.env


== Email Domains und User ==
log_info ()
Debian: das Anlegen einer Domain im Web-Interface schlägt direkt fehl - "Bad Request".<br>
{
Nicht schlimm, aber vielleicht nehme ich doch lieber die fertige Appliance auf Basis von S.u.S.E. ?
    if [ ${LOG_LEVEL} = "INFO" ]; then
        logger -t $(basename $0) $@
    fi
}


Per Kommandozeile funktioniert es jedenfalls.
log_error ()
<pre>
{
domains=$(ssh $kopano_server ucr get mail/hosteddomains)
    logger -t $(basename $0) $@
users=$(grommunio-admin ldap search)
    echo 2>&1 $@
}


# create email domains
# read parameters: mode [rw], register, value and optional -c <check pause>
for domain in $domains; do
pause=0
     u_cnt=$(echo "$users" | grep -c $domain)
while [ $# -gt 0 ]; do
    [ $u_cnt -gt 0 ] && echo grommunio-admin domain create -u $u_cnt $domain
     case $1 in
        -c|--check) shift
            [ $# -ge 3 ] || exit 1
            pause=$1
            ;;
        -w|--write) mode=w
            shift
            [ $# -eq 2 ] || exit 1
            reg=$1
            val=$2
            shift
            ;;
        -r|--read) mode=r
            shift
            [ $# -eq 1 ] || exit 1
            reg=$1
            ;;
    esac
    shift
done
done


# import and sync LDAP users
# handle offline deye_inverter by
grommunio-admin ldap downsync -c
#  ${pause} > 0 -> endless loop
#  ${pause} = 0 -> error after ${retries}
while true; do
 
    # try deye_cli.sh $retries times every $sleep secs
    check=${retries}
    while [ ${check} -gt 0 ]; do
 
        case ${mode} in
            r) result=$(./deye_cli.sh $mode $reg | grep 'int: ')
                if [ -n "$result" ]; then
                    echo "$result"
                    log_info -t $0 deye_cli.sh $mode $reg
                    exit 0
                fi
              ;;
            w) result=$(./deye_cli.sh $mode $reg $val)
                if [ "$result" = "Ok" ]; then
                    log_info -t $0 deye_cli.sh $mode $reg $val
                    exit 0
              fi
              ;;
        esac
 
        # wait ${sleep} or ${pause} secs
        sleep $(( ${pause} ? ${sleep} : ${pause} ))
 
        # try ${retries} times
        (( check-- ))
    done
 
    # error, or retry endless until wakeup of deye_inverter
    if [ ${pause} -eq 0 ]; then
        log_error "Error: deye_cli.sh $mode $reg $val failed."
        exit 1
    fi
 
done
</pre>
</pre>
Der Import der User funktioniert jedenfalls auch über das Webinterface.


=== Export - Import via Outlook .pst ===
Obiges Skript '''deye_inverter.sh -r 22''' liest z.B. das Register 22 aus dem Wechselrichter aus, '''deye_inverter.sh -w 22 5892''' schreibt das Jahr 2023 und den Monat April (23 *256 + 4) in Register 22.
Da ich nur 5 Konten habe, ist diese Methode auch nicht ganz abwegig:
* Emails
* Kalender
* Kontakte
* Notizen
* usw.


<pre>gromox-pff2mt /tmp/neobiker_outlook.pst | gromox-mt2exm -u neobiker@neobiker.de
Das benötigt man, um dem WR jeden Morgen zum Start Datum und Uhrzeit beizubringen. Damit wird auch der Tageszähler täglich auf 0 gesetzt, ansonsten bliebe der alte Wert stehen und der Tageszähler würde einfach stetig addieren, wie der Gesamtertragszähler.
</pre>
 
Also, jede Nacht geht der WR aus, und sobald er beim ersten Tageslicht wieder startet möchte ich Datum und Uhrzeit setzen und anschliessend die Werte kontinuierlich auslesen und über MQTT ausgeben.
 
Folgendes Skript starte ich jeden Morgen um 04:00 Uhr per Cron (und in rc.local fürs booten):
 
=== deye_mqtt_loop.sh ===
Dieses Script liegt bei mir unter ''/root/sbin'' und startet eine Endlosschleife. Dazu löscht es alte (vorher gestartete) Instanzen von sich selbst - dadurch kann es jederzeit (zB. in der crontab) erneut gestartet werden. Es wartet auf den Sonnenaufgang, der mittels der sunrise() von '''FHEM''' ermittelt wird. Es wartet auf den Sonnenuntergang per sunset() von FHEM, um danach die Abfrage zu pausieren bis zum nächsten Sonnenaufgang.
 
Ausserdem setzt es beim Start den Tageszähler des Wechselrichters bedarfsweise zurück, bevor die Werte des DEYE Wechselrichters per Endlosschleife abgefragt werden.
 
<pre>#!/bin/bash
# reset Daily_Power of DEYE Inverters
#  via deye_inverter_mqtt python package
#  https://github.com/kbialek/deye-inverter-mqtt
 
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
 
# config:
deye_inverter_dir=/opt/deye-inverter-mqtt
fhem_dir=/opt/fhem
horizon="-2"            # sunset CIVIL=-6, REAL=0
 
debug=info
 
myPID=$$
myname=$(basename $0)
arg="$1"
 
deye_inverter_sh=$deye_inverter_dir/deye_inverter.sh
deye_inverter_mqtt_cfg=$deye_inverter_dir/config.env
deye_inverter_mqtt_cmd="python3 deye_docker_entrypoint.py"
 
sunrise_cmd="perl $fhem_dir/fhem.pl localhost:7072 {sunrise_abs('HORIZON=$horizon')}"
sunset_cmd="perl $fhem_dir/fhem.pl localhost:7072 {sunset_abs('HORIZON=$horizon')}"
 
# installation:
# locate FHEM, used for sunrise(), sunset()
if [ ! -x ${fhem_dir/fhem.pl} ]; then
  echo "Error: ${fhem_dir} not found."
  echo "      Is FHEM installed in $fhem_dir ?"
  exit 1
fi
 
# locate script in deye_inverter_mqtt directory
if [ ! -f ${deye_inverter_mqtt_cfg} ]; then
  echo "Error: ${deye_inverter_mqtt_cfg} not found."
  echo "      Please update \$deye_inverter_sh path in $0"
  exit 1
fi
 
# read my config
set -a; source ${deye_inverter_mqtt_cfg}; set +a
 
[ "$debug" = "full" ] && set -x
 
# ----- functions () ------------------------------------
log_info ()
{
    [ "$debug" = "yes" -o "$debug" = "true" -o "$debug" = "info" ] &&
        echo $myname: $@
    [ ${LOG_LEVEL} = "INFO" ] && logger -t $myname $@
}
 
log_error ()
{
    logger -t $myname $@
    echo 1>&2 $@
}
 
# store actual date + time into vars
set_date_time_vars ()
{
    year=$(date +%y)
    month=$(date +%m | sed 's/^0//g')
    day=$(date +%d | sed 's/^0//g')
    hour=$(date +%H | sed 's/^0//g')
    minute=$(date +%m | sed 's/^0//g')
    second=$(date +%S | sed 's/^0//g')
}
 
# kill deye_inverter_loop ()
kill_old_instance ()
{
    # kill old instances of me
    my_PS=$(ps ax | grep -v grep | grep "${myname}" | awk '{print $1}')
    for ps in $my_PS; do
        [ $ps -lt $myPID ] && kill $ps
    done
}
 
# get deye_inverter_mqtt PIDs
get_deye_mqtt_pid ()
{
    deye_mqtt_pid=$(ps ax | grep -v grep | grep "${deye_inverter_mqtt_cmd}" | awk '{print $1}')
}
 
# kill deye_inverter_instance ()
kill_deye_inverter_instance ()
{
    get_deye_mqtt_pid
    if [ ! -z "${deye_mqtt_pid}" ]; then
        kill ${deye_mqtt_pid}
        log_info "Info: KILL running deye_mqtt ${deye_mqtt_pid}"
    fi
 
    # kill any running deye requests
    deye_ps=$(ps ax | grep -v grep | grep "$deye_inverter_sh " | awk '{print $1}')
    [ ! -z "$deye_ps" ] && kill $deye_ps
}
 
# stop all runnning instances
deye_inverter_mqtt_stop ()
{
    kill_old_instance
    kill_deye_inverter_instance
}
 
# start deye_inverter_mqtt
deye_inverter_mqtt_start ()
{
    log_info "Deye MQTT start"
    cd ${deye_inverter_dir}
    ${deye_inverter_mqtt_cmd}
}
 
# read year_month register 0x16 = 22
read_year_month ()
{
    ${deye_inverter_sh} -c 60 -r 22 | sed 's/int: //g' | cut -d, -f1
}
 
# read day register 0x17 = 23
read_day ()
{
    ${deye_inverter_sh} -c 10 -r 23 | sed 's/.*h: //g' | cut -d, -f1
}
 
# read Daily power register 0x3c = 60
read_daily_Power ()
{
    ${deye_inverter_sh} -c 10 -r 60 | sed 's/int: //g' | cut -d, -f1
}
 
# read actual power register 0x56 = 86
read_actual_Power ()
{
    # doubleRegisterSensor("AC Active Power", 0x56, 0.1, mqtt_topic_suffix='ac/active_power', groups=['string', 'micro'])
    # (int.from_bytes(high_word, 'big') * 65536 + int.from_bytes(low_word, 'big')) * self.factor
    # val86=${deye_inverter_sh} -c 10 -r 86 | sed 's/int: //g' | cut -d, -f1
    # val87=${deye_inverter_sh} -c 10 -r 87 | sed 's/int: //g' | cut -d, -f1
    # echo $(( (${val87} * 65536 + ${val86}) * 0.1 ))
 
    # simple value is enough for our purpose
    ${deye_inverter_sh} -c 10 -r 86 | sed 's/int: //g' | cut -d, -f1
}
 
# write actual date/time into deye inverter
reset_deye_inverter ()
{
    # daily reset phase
    daily_reset=init
 
    # ensure by loop, that all register values are written
    while [ ${daily_reset} != "done" ]; do
 
        # wait until we have enough power
        while [ "$(read_actual_Power)" -le 10 ]; do
            sleep 600
        done
 
        # read Daily power register
        dailyPower=$(read_daily_Power)


=== Import von Emails aus Kopano ===
        # read (old) year_month and day
Der Import landet (leider) in einem separatem Verzeichnis:
        val22_old=$(read_year_month)
[[Datei:Grommunio Import.jpg|mini]]
        val23_day=$(read_day)
        log_info "date/time update initialized"


Das ist nicht ganz das was ich mir für einen Import vorstelle: Jeder User muss alle seine Inhalte manuell auf dem Server in die Hauptverzeichnisse verschieben ... ?
        # set variables year, month, day, hour, minute, second
        set_date_time_vars


Ansonsten hat der Import prinzipiell für mein Neobiker Postfach funktioniert.
        # calculate register 22-24 with date and time vars
        val22=$(( ${year}  * 256 + ${month} ))
        val23=$(( ${day}    * 256 + ${hour} ))
        val24=$(( ${minute} * 256 + ${second} ))


<pre># gromox-kdb2mt — Utility for analysis/import of Kopano mailboxes
        # reset power/date/time only once a day or exit loop
# gromox-mt2exm — Utility for importing various mail items
        [ "$dailyPower" -eq 0 -a
          "${val22_old}" = "${val22}" -a
          "${val23_day}" = "${day}" ] && break


mkdir -p /mnt/kopano_attachments
        # reset daily_power by
sshfs $kopano_server:/var/lib/kopano/attachments /mnt/kopano_attachments
        # setting register 22-24 with actual date + time
        ${deye_inverter_sh} -w 22 $val22 || continue
        ${deye_inverter_sh} -w 23 $val23 || continue
        ${deye_inverter_sh} -w 24 $val24 || continue


# detach (CTRL-A d) the SSH tunnel to SQL server (who only accepts localhost conections)
        # test success
screen ssh -L 12345:localhost:3306 "root@${kopano_server}"
        # read Daily power register, should be resetted to 0 now
        dailyPower=$(read_daily_Power)


# EXAMPLE for user neobiker = neobiker@neobiker.de
        # exit loop if power is updated, else log that reset failed
# ------------------------------------------------
        if [ "$dailyPower" -eq 0 ]; then
# ENVIRONMENT variable is used for SQL password
            daily_reset=done
# either EXPORT it or write in one line with cmd
export_mbox="gromox-kdb2mt --sql-host 127.0.0.1 --sql-port 12345 --src-attach /mnt/kopano_attachments"
SQLPASS=$(ssh $kopano_server cat /etc/mysql.secret)


SQLPASS=$SQLPASS $export_mbox --mbox-mro neobiker | gromox-mt2exm -u neobiker@neobiker.de
        ### skip loop here, reset doesn't work always :-(
        # elif [ $daily_reset = "init" ]; then
        #    daily_reset=failed
        #    sleep 300


kdb2mt: No ACLs will be extracted.
        else
kdb Server GUID: xxxxxxxxxxxxxxxxxxxxxxxxxxx
            log_info "daily power reset failed"
Database schema is kdb-118
            break
Store GUID for MRO "neobiker": xxxxxxxxxxxxxxxxxxxxxxxxx
        fi
Processing folder "" (7 elements)...
    done
Processing folder "IPM_SUBTREE" (19 elements)...
    log_info "date/time reset done"
Processing folder "Posteingang" (791 elements)...
}
Processing folder "Postausgang" (0 elements)...
Processing folder "Gelöschte Objekte" (1 elements)...
Processing folder "Gesendete Objekte" (1 elements)...
Processing folder "Kontakte" (0 elements)...
Processing folder "Kalender" (1 elements)...
Processing folder "Entwürfe" (0 elements)...
Processing folder "Journal" (0 elements)...
Processing folder "Notizen" (0 elements)...
Processing folder "Aufgaben" (0 elements)...
Processing folder "Junk E-Mail" (44 elements)...
Processing folder "RSS Feeds" (0 elements)...
Processing folder "Konversationseinstellungen" (0 elements)...
Processing folder "Quickstep Einstellungen" (0 elements)...
Processing folder "Vorgeschlagene Kontakte" (0 elements)...
Processing folder "Spambox" (0 elements)...
Processing folder "Junk-E-Mail" (0 elements)...
Processing folder "Gelöschte Elemente" (0 elements)...
Processing folder "Gesendete Elemente" (0 elements)...
Processing folder "IPM_COMMON_VIEWS" (2 elements)...
Processing folder "IPM_VIEWS" (0 elements)...
Processing folder "FINDER_ROOT" (0 elements)...
Processing folder "Verknüpfung" (0 elements)...
Processing folder "Schedule" (0 elements)...
Processing folder "Freebusy Data" (1 elements)...
</pre>


== Import aller Email Konten ==
# wait for (next) daylight
wait_for_sunrise ()
{
    sunrise=$($sunrise_cmd)
    sunset=$($sunset_cmd)


<pre>
    current_time=$(date +%s)
export_mbox="gromox-kdb2mt --sql-host 127.0.0.1 --sql-port 12345 --src-attach /mnt/kopano_attachments"
    sunrise=$(date -d"$sunrise" +%s)
SQLPASS=$(ssh $kopano_server cat /etc/mysql.secret)
    sunset=$(date -d"$sunset" +%s)


# LOOP over all email accounts (except SYSTEM)
    # calulate time in secs until (next) sunrise
# --------------------------------------------
    ((sleep_sunrise=$sunrise - $current_time)) # this morning
users=$(ssh $kopano_server "kopano-admin -l | tail -n +4 | awk '{print $1}' | grep -v SYSTEM")
    [ $sleep_sunrise -lt 0 ] &&
        ((sleep_sunrise=$sunrise + 86400)) # next morning


for user in ${users}; do
    # wait for daylight only
    if [ $current_time -lt $sunrise -o
        $current_time -gt $sunset ]; then
        log_info sleep $sleep_sunrise
        sleep $sleep_sunrise
    fi
}


  details=$(ssh $kopano_server kopano-admin --details $user)
# wait for sunset
  email=$(echo "${details}" $user | awk '/Emailaddress:/ {print $2}')
wait_for_sunset ()
  guid=$(echo "${details}" $user | awk '/Store GUID:/ {print $3}')
{
    sunset=$($sunset_cmd)


  SQLPASS=$SQLPASS $export_mbox --mbox-guid $guid | gromox-mt2exm -u $email
    current_time=$(date +%s)
    sunset=$(date -d"$sunset" +%s)


done
    # calulate time in secs until next sunset
    ((sleep_sunset=$sunset - $current_time))


# stop ssh tunnel -> exit ssh on kopano server
    # wait until sunset only
screen -r
    if [ $sleep_sunset -gt 0 ]; then
        log_info sleep $sleep_sunset
        sleep $sleep_sunset
    fi
}


# unmount kopano attachments
# ------------------------
umount /mnt/kopano_attachments
# MAIN: Script starts here
</pre>
# ------------------------


== Postfix ==
# sunset_stop: wait until (next) sunset
Mail Delivery Agent (MDA) Konfiguration
[ "$arg" = "sunset_stop" ] &&
* zum versenden von Emails
    wait_for_sunset
* Mailaddress rewritings


<pre>domains=$(ssh ucsmail ucr get mail/hosteddomains)
# stop any runnning deye_inverter_mqtt processes
</pre>
deye_inverter_mqtt_stop


== Mail Relay Server ==
# arg stop: don't start again
Postfix can use a mail relay server.
[ "$arg" = "stop" ] && exit 0


<pre>ssh $kopano_server ucr get mail/relayhost</pre>
# wait for (next) daylight
wait_for_sunrise


<pre># authentification on relay host
# setting date/time also resets daily power register
/etc/postfix/smtp_auth
reset_deye_inverter
# format
# FQDN-Relayhost username:password


postmap /etc/postfix/smtp_auth
# start deye_mqtt_loop
postmap /etc/postfix/tls_policy
# check every 15 min for running deye_inverter_mqtt
service postfix reload
while :; do
</pre>


== Fetchmail ==
    # wait for sunrise
Fetchmail Konfiguration zum abholen der Emails von meinem Email Provider.
    wait_for_sunrise
Emails werden entweder von UCS '''oder ''' grommunio abgeholt und and Postfix übergeben!


Auf UCS für jeden Benutzer deaktivieren <br>
    # start deye_inverter_mqtt
-> ''Advanced settings ‣ Remote mail retrieval (single)''<br>
    get_deye_mqtt_pid
oder Postfix auf UCS zur weiterleitung an grommunio konfigurieren.
    if [ -z "$deye_mqtt_pid" ]; then
        deye_inverter_mqtt_start
    fi


<pre>systemctl stop fetchmail
    # wait 15 min
systemctl disable fetchmail
    sleep 900
ucr set fetchmail/autostart=false
done
</pre>


# fetchmail config and passwords
=== Contrab und /etc/rc.local ===
cat /etc/fetchmailrc
Mein Crontab Eintrag:
<pre># stop DEYE monitoring after sunset (FHEM)
0 18 * * * /usr/local/sbin/deye_mqtt.sh sunset_stop 2>/dev/null
</pre>
und /etc/rc.local:
<pre># start monitoring of PV-System: DEYE inverter
$(sleep 10; /root/sbin/deye_mqtt_loop.sh 2>dev/null)&
</pre>
</pre>
== Dashboard ==
So sieht das aus, wenn es den ganzen Tag im April regnet und bedeckt ist:
[[Datei:Solar Bedeckt Regen.jpg|midi]]
Die Eigenverbrauchsquote habe ich mittels Grafana ermittelt:
[[Datei:Solar Eigenverbrauch.jpg|midi|Eigenverbrauchsquote]]

Version vom 3. Februar 2024, 15:49 Uhr

Balkonkraftwerk: DEYE Wechselrichter ohne Cloud auslesen und per MQTT monitoren

Anfang März habe ich endlich ein Balkonkraftwerk über Amazon bestellt und installiert. Es ist ja inzwischen keine Raketenwissenschaft mehr, und die Akzeptanz und Möglichkeiten zur einfachen Installation per Do-It-Yourself haben mich nun endlich Fakten schaffen lassen.

Ich habe zwei Standard PV-Module je 410W samt einem passendem DEYE Wechselrichter (WR) mit Anschlusskabel als Komplettpaket bestellt. Da ich am Balkon schon eine Klimaanlage stehen habe, ist der Stromanschluss auch kein Problem gewesen. Und endlich gilt im Sommer: Wenn es heiß ist und die Klimaanlage läuft, produzieren die PV-Module ordentlich Strom dafür, außerdem muss ich dann keinen Überschuss verschenken.

Zum Start hatten die DEYE Wechselrichter, die meines Wissens identisch zu z.B. Bosswerk, Blaupunkt und Turbo Energy sind, allerdings noch ein Security Problem mit der Firmware, darüber hatte u.a. Heise berichtet. Ein Firmware Upgrade stand kurz darauf zur Verfügung und wurde automatisch eingespielt.

Die DEYE Wechselrichter haben bereits WLAN eingebaut, d.h. man steckt die wirklich nur in die Steckdose und verbindet anschliessend die PV-Module und es geht los. Die Einbindung ins eigene WLAN erfolgt wie üblich mittels eingebautem WLAN Accesspoint und internem Webserver, worüber man die Konfiguration vornimmt und dann am besten den WLAN-AP deaktiviert. Die Internetverbindung des Wechselrichters habe ich dann über meine Fritzbox gesperrt.

Der WR ist über WLAN nur erreichbar, wenn die PV-Module eine Spannung (DC) erzeugen. Der Anschluss an das Stromnetz (AC) ist hierfür irrelevant. Das heisst wenn es dunkel ist, ist er nicht erreichbar bzw. aus.

Der WR bietet verschiedene Möglichkeiten zur Konfiguration und Abfrage der aktuellen Werte:

  • einen internen Webserver (http://10.10.10.254)
  • die Cloud Anbindung zu Solarman (Solarman Smart App)
  • (angepasstes) Modbus Protokoll über Port 8899
  • AT+ Befehle über Port 48899

Ich verwende das Modbus Protokoll zum auslesen des WR, das

  1. effizient/schnell ist (Performance)
  2. ohne Internetverbindung auskommt

Für DEYE kompatible WR stehen inzwischen einige Lösungsansätze zur Verfügung. Ich verwende die DEYE Inverter MQTT Bridge von Krzysztof Białek, da ich hier bereits einen MQTT Server innerhalb der Heimautomatisierungslösung FHEM zur Einbindung meiner Tasmota Devices (Shelly; Hichi) eingerichtet habe.

Diese Lösung ist als Docker Container konzipiert, was mir aber unnötiger Overhead ist. Da es in Python realisiert ist, starte ich das Skript ganz einfach direkt, ohne Docker.

Wechselrichter Eigenheiten

Der Wechselrichter läuft nur, wenn die PV-Module eine Gleichspannung einspeisen - d.h. wenn es draussen hell ist. Ein Auslesen der Werte des WR funktioniert also nicht, wenn es dunkel ist (Timeout). Ausserdem wird der Tageszähler erst durch setzen von Uhrzeit/Datum wieder auf 0 gesetzt, was über eine Internetverbindung erfolgt. Ist die gesperrt, läuft der Tageszähler weiter.

Also, jeden Abend geht der WR aus, und sobald er beim ersten Tageslicht wieder startet möchte ich Datum und Uhrzeit setzen und anschliessend die Werte kontinuierlich auslesen und über MQTT ausgeben. Bis dahin ist der WR aber halt nicht erreichbar.

Installation

Das Projekt von Github laden und in einem Verzeichnis speichern, Python ist normalerweise ja schon installiert.

git clone https://github.com/kbialek/deye-inverter-mqtt
pip install paho-mqtt
cd deye-inverter-mqtt

Unter Debian habe ich im Skript deye_cli.sh python durch python3 ersetzen müssen:

#!/bin/bash
set -a; source config.env; set +a
python3 deye_cli.py "$@"

config.env

Die Konfiguration meines DEYE Wechselrichters vom Typ Micro-Inverter: config.env

DEYE_LOGGER_IP_ADDRESS=<IP Wechselrichter im WLAN>
DEYE_LOGGER_PORT=8899
DEYE_LOGGER_SERIAL_NUMBER=<Seriennummer des WR>

MQTT_HOST=<IP von MQTT Server>
MQTT_PORT=1883
MQTT_USERNAME=
MQTT_PASSWORD=
MQTT_TOPIC_PREFIX=deye

LOG_LEVEL=ERROR
DEYE_DATA_READ_INTERVAL=60
DEYE_METRIC_GROUPS=micro

deye_inverter.sh

Ausserdem habe ich ein kleines Wrapper Skript geschrieben, um Daten des WR einfacher lesen und schreiben zu können:

deye_inverter.sh [--check <pause>] --read <register> | --write <register> <value>

#!/bin/bash
# Read / Write DEYE Inverters
#   via deye_inverter_mqtt python package
#   https://github.com/kbialek/deye-inverter-mqtt

retries=2       # retry deye_inverter command multiple times
sleep=5         # sleep time between retries

# locate script in deye_inverter_mqtt directory
cd $(dirname $0)
if [ ! -f ./deye_cli.sh ]; then
  echo "Error: ./deye_cli.sh not found."
  echo "       Please move $(basename $0) in deye-inverter-mqtt directory."
  exit 1
fi
. ./config.env

log_info ()
{
    if [ ${LOG_LEVEL} = "INFO" ]; then
        logger -t $(basename $0) $@
    fi
}

log_error ()
{
    logger -t $(basename $0) $@
    echo 2>&1 $@
}

# read parameters: mode [rw], register, value and optional -c <check pause>
pause=0
while [ $# -gt 0 ]; do
    case $1 in
        -c|--check) shift
            [ $# -ge 3 ] || exit 1
            pause=$1
            ;;
        -w|--write) mode=w
            shift
            [ $# -eq 2 ] || exit 1
            reg=$1
            val=$2
            shift
            ;;
        -r|--read) mode=r
            shift
            [ $# -eq 1 ] || exit 1
            reg=$1
            ;;
    esac
    shift
done

# handle offline deye_inverter by
#   ${pause} > 0 -> endless loop
#   ${pause} = 0 -> error after ${retries}
while true; do

    # try deye_cli.sh $retries times every $sleep secs
    check=${retries}
    while [ ${check} -gt 0 ]; do

        case ${mode} in
            r) result=$(./deye_cli.sh $mode $reg | grep 'int: ')
                if [ -n "$result" ]; then
                    echo "$result"
                    log_info -t $0 deye_cli.sh $mode $reg
                    exit 0
                fi
               ;;
            w) result=$(./deye_cli.sh $mode $reg $val)
                if [ "$result" = "Ok" ]; then
                    log_info -t $0 deye_cli.sh $mode $reg $val
                    exit 0
               fi
               ;;
        esac

        # wait ${sleep} or ${pause} secs
        sleep $(( ${pause} ? ${sleep} : ${pause} ))

        # try ${retries} times
        (( check-- ))
    done

    # error, or retry endless until wakeup of deye_inverter
    if [ ${pause} -eq 0 ]; then
        log_error "Error: deye_cli.sh $mode $reg $val failed."
        exit 1
    fi

done

Obiges Skript deye_inverter.sh -r 22 liest z.B. das Register 22 aus dem Wechselrichter aus, deye_inverter.sh -w 22 5892 schreibt das Jahr 2023 und den Monat April (23 *256 + 4) in Register 22.

Das benötigt man, um dem WR jeden Morgen zum Start Datum und Uhrzeit beizubringen. Damit wird auch der Tageszähler täglich auf 0 gesetzt, ansonsten bliebe der alte Wert stehen und der Tageszähler würde einfach stetig addieren, wie der Gesamtertragszähler.

Also, jede Nacht geht der WR aus, und sobald er beim ersten Tageslicht wieder startet möchte ich Datum und Uhrzeit setzen und anschliessend die Werte kontinuierlich auslesen und über MQTT ausgeben.

Folgendes Skript starte ich jeden Morgen um 04:00 Uhr per Cron (und in rc.local fürs booten):

deye_mqtt_loop.sh

Dieses Script liegt bei mir unter /root/sbin und startet eine Endlosschleife. Dazu löscht es alte (vorher gestartete) Instanzen von sich selbst - dadurch kann es jederzeit (zB. in der crontab) erneut gestartet werden. Es wartet auf den Sonnenaufgang, der mittels der sunrise() von FHEM ermittelt wird. Es wartet auf den Sonnenuntergang per sunset() von FHEM, um danach die Abfrage zu pausieren bis zum nächsten Sonnenaufgang.

Ausserdem setzt es beim Start den Tageszähler des Wechselrichters bedarfsweise zurück, bevor die Werte des DEYE Wechselrichters per Endlosschleife abgefragt werden.

#!/bin/bash
# reset Daily_Power of DEYE Inverters
#   via deye_inverter_mqtt python package
#   https://github.com/kbialek/deye-inverter-mqtt

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# config:
deye_inverter_dir=/opt/deye-inverter-mqtt
fhem_dir=/opt/fhem
horizon="-2"            # sunset CIVIL=-6, REAL=0

debug=info

myPID=$$
myname=$(basename $0)
arg="$1"

deye_inverter_sh=$deye_inverter_dir/deye_inverter.sh
deye_inverter_mqtt_cfg=$deye_inverter_dir/config.env
deye_inverter_mqtt_cmd="python3 deye_docker_entrypoint.py"

sunrise_cmd="perl $fhem_dir/fhem.pl localhost:7072 {sunrise_abs('HORIZON=$horizon')}"
sunset_cmd="perl $fhem_dir/fhem.pl localhost:7072 {sunset_abs('HORIZON=$horizon')}"

# installation:
# locate FHEM, used for sunrise(), sunset()
if [ ! -x ${fhem_dir/fhem.pl} ]; then
  echo "Error: ${fhem_dir} not found."
  echo "       Is FHEM installed in $fhem_dir ?"
  exit 1
fi

# locate script in deye_inverter_mqtt directory
if [ ! -f ${deye_inverter_mqtt_cfg} ]; then
  echo "Error: ${deye_inverter_mqtt_cfg} not found."
  echo "       Please update \$deye_inverter_sh path in $0"
  exit 1
fi

# read my config
set -a; source ${deye_inverter_mqtt_cfg}; set +a

[ "$debug" = "full" ] && set -x

# ----- functions () ------------------------------------
log_info ()
{
    [ "$debug" = "yes" -o "$debug" = "true" -o "$debug" = "info" ] &&
        echo $myname: $@
    [ ${LOG_LEVEL} = "INFO" ] && logger -t $myname $@
}

log_error ()
{
    logger -t $myname $@
    echo 1>&2 $@
}

# store actual date + time into vars
set_date_time_vars ()
{
    year=$(date +%y)
    month=$(date +%m | sed 's/^0//g')
    day=$(date +%d | sed 's/^0//g')
    hour=$(date +%H | sed 's/^0//g')
    minute=$(date +%m | sed 's/^0//g')
    second=$(date +%S | sed 's/^0//g')
}

# kill deye_inverter_loop ()
kill_old_instance ()
{
    # kill old instances of me
    my_PS=$(ps ax | grep -v grep | grep "${myname}" | awk '{print $1}')
    for ps in $my_PS; do
        [ $ps -lt $myPID ] && kill $ps
    done
}

# get deye_inverter_mqtt PIDs
get_deye_mqtt_pid ()
{
    deye_mqtt_pid=$(ps ax | grep -v grep | grep "${deye_inverter_mqtt_cmd}" | awk '{print $1}')
}

# kill deye_inverter_instance ()
kill_deye_inverter_instance ()
{
    get_deye_mqtt_pid
    if [ ! -z "${deye_mqtt_pid}" ]; then
        kill ${deye_mqtt_pid}
        log_info "Info: KILL running deye_mqtt ${deye_mqtt_pid}"
    fi

    # kill any running deye requests
    deye_ps=$(ps ax | grep -v grep | grep "$deye_inverter_sh " | awk '{print $1}')
    [ ! -z "$deye_ps" ] && kill $deye_ps
}

# stop all runnning instances
deye_inverter_mqtt_stop ()
{
    kill_old_instance
    kill_deye_inverter_instance
}

# start deye_inverter_mqtt
deye_inverter_mqtt_start ()
{
    log_info "Deye MQTT start"
    cd ${deye_inverter_dir}
    ${deye_inverter_mqtt_cmd}
}

# read year_month register 0x16 = 22
read_year_month ()
{
    ${deye_inverter_sh} -c 60 -r 22 | sed 's/int: //g' | cut -d, -f1
}

# read day register 0x17 = 23
read_day ()
{
    ${deye_inverter_sh} -c 10 -r 23 | sed 's/.*h: //g' | cut -d, -f1
}

# read Daily power register 0x3c = 60
read_daily_Power ()
{
    ${deye_inverter_sh} -c 10 -r 60 | sed 's/int: //g' | cut -d, -f1
}

# read actual power register 0x56 = 86
read_actual_Power ()
{
    # doubleRegisterSensor("AC Active Power", 0x56, 0.1, mqtt_topic_suffix='ac/active_power', groups=['string', 'micro'])
    # (int.from_bytes(high_word, 'big') * 65536 + int.from_bytes(low_word, 'big')) * self.factor
    # val86=${deye_inverter_sh} -c 10 -r 86 | sed 's/int: //g' | cut -d, -f1
    # val87=${deye_inverter_sh} -c 10 -r 87 | sed 's/int: //g' | cut -d, -f1
    # echo $(( (${val87} * 65536 + ${val86}) * 0.1 ))

    # simple value is enough for our purpose
    ${deye_inverter_sh} -c 10 -r 86 | sed 's/int: //g' | cut -d, -f1
}

# write actual date/time into deye inverter
reset_deye_inverter ()
{
    # daily reset phase
    daily_reset=init

    # ensure by loop, that all register values are written
    while [ ${daily_reset} != "done" ]; do

        # wait until we have enough power
        while [ "$(read_actual_Power)" -le 10 ]; do
            sleep 600
        done

        # read Daily power register
        dailyPower=$(read_daily_Power)

        # read (old) year_month and day
        val22_old=$(read_year_month)
        val23_day=$(read_day)
        log_info "date/time update initialized"

        # set variables year, month, day, hour, minute, second
        set_date_time_vars

        # calculate register 22-24 with date and time vars
        val22=$(( ${year}   * 256 + ${month} ))
        val23=$(( ${day}    * 256 + ${hour} ))
        val24=$(( ${minute} * 256 + ${second} ))

        # reset power/date/time only once a day or exit loop
        [ "$dailyPower" -eq 0 -a
          "${val22_old}" = "${val22}" -a
          "${val23_day}" = "${day}" ] && break

        # reset daily_power by
        # setting register 22-24 with actual date + time
        ${deye_inverter_sh} -w 22 $val22 || continue
        ${deye_inverter_sh} -w 23 $val23 || continue
        ${deye_inverter_sh} -w 24 $val24 || continue

        # test success
        # read Daily power register, should be resetted to 0 now
        dailyPower=$(read_daily_Power)

        # exit loop if power is updated, else log that reset failed
        if [ "$dailyPower" -eq 0 ]; then
            daily_reset=done

        ### skip loop here, reset doesn't work always :-(
        # elif [ $daily_reset = "init" ]; then
        #    daily_reset=failed
        #    sleep 300

        else
            log_info "daily power reset failed"
            break
        fi
    done
    log_info "date/time reset done"
}

# wait for (next) daylight
wait_for_sunrise ()
{
    sunrise=$($sunrise_cmd)
    sunset=$($sunset_cmd)

    current_time=$(date +%s)
    sunrise=$(date -d"$sunrise" +%s)
    sunset=$(date -d"$sunset" +%s)

    # calulate time in secs until (next) sunrise
    ((sleep_sunrise=$sunrise - $current_time)) # this morning
    [ $sleep_sunrise -lt 0 ] &&
        ((sleep_sunrise=$sunrise + 86400)) # next morning

    # wait for daylight only
    if [ $current_time -lt $sunrise -o
         $current_time -gt $sunset ]; then
        log_info sleep $sleep_sunrise
        sleep $sleep_sunrise
    fi
}

# wait for sunset
wait_for_sunset ()
{
    sunset=$($sunset_cmd)

    current_time=$(date +%s)
    sunset=$(date -d"$sunset" +%s)

    # calulate time in secs until next sunset
    ((sleep_sunset=$sunset - $current_time))

    # wait until sunset only
    if [ $sleep_sunset -gt 0 ]; then
        log_info sleep $sleep_sunset
        sleep $sleep_sunset
    fi
}

# ------------------------
# MAIN: Script starts here
# ------------------------

# sunset_stop: wait until (next) sunset
[ "$arg" = "sunset_stop" ] &&
    wait_for_sunset

# stop any runnning deye_inverter_mqtt processes
deye_inverter_mqtt_stop

# arg stop: don't start again
[ "$arg" = "stop" ] && exit 0

# wait for (next) daylight
wait_for_sunrise

# setting date/time also resets daily power register
reset_deye_inverter

# start deye_mqtt_loop
# check every 15 min for running deye_inverter_mqtt
while :; do

    # wait for sunrise
    wait_for_sunrise

    # start deye_inverter_mqtt
    get_deye_mqtt_pid
    if [ -z "$deye_mqtt_pid" ]; then
        deye_inverter_mqtt_start
    fi

    # wait 15 min
    sleep 900
done

Contrab und /etc/rc.local

Mein Crontab Eintrag:

# stop DEYE monitoring after sunset (FHEM)
0 18 * * * /usr/local/sbin/deye_mqtt.sh sunset_stop 2>/dev/null

und /etc/rc.local:

# start monitoring of PV-System: DEYE inverter
$(sleep 10; /root/sbin/deye_mqtt_loop.sh 2>dev/null)&

Dashboard

So sieht das aus, wenn es den ganzen Tag im April regnet und bedeckt ist: midi

Die Eigenverbrauchsquote habe ich mittels Grafana ermittelt: Eigenverbrauchsquote