hardware:channels:solar_inverters:sma
Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen angezeigt.
| Nächste Überarbeitung | Vorhergehende Überarbeitung | ||
| hardware:channels:solar_inverters:sma [2018/01/25 13:32] – angelegt jau | hardware:channels:solar_inverters:sma [2020/12/18 13:30] (aktuell) – [SMA Sunny Boy] wasserma | ||
|---|---|---|---|
| Zeile 1: | Zeile 1: | ||
| - | #redirect | + | ===== SMA Wechselrichter ===== |
| + | Der Hersteller SMA bietet Solarwechselrichter [[http:// | ||
| + | deren Daten über verschiedenen Schnittstellen ausgelesen werden können. SMA bietet zum Auslesen die | ||
| + | Software "Sunny Explorer" | ||
| + | Für die Nutzung mit der Volkszähler Lösung ist die Quelloffene Software SMASpot besser geeignet, die platttformübergreifend genutzt werden kann, und sich in die Volkszaehler Infrastruktur auf unterschiedliche Weise | ||
| + | einbinden lässt. Auf dieser Seite sind einige Beispiele gezeigt. | ||
| + | |||
| + | ==== SMAspot ==== | ||
| + | Die Software SMAspot ist bei [[https:// | ||
| + | <note important> | ||
| + | === Kompilieren von SMAspot unter Linux === | ||
| + | Die folgenden Punkte wurden unter Ubuntu 12.04 LTS getestet sollten aber auch in anderen Debian-kompatiblen | ||
| + | Distributionen funktionieren: | ||
| + | |||
| + | - Herunterladen des Sourcecodes z.B. als SMAspot_SRC_244_Linux_Win32.tar.gz z.B. / | ||
| + | - Anlegen eines Verzeichnisses z.b. " | ||
| + | - Wechseln in dieses Verzeichnis | ||
| + | - Auspacken mit ../tar xvfz SMAspot_SRC_244_Linux_Win32.tar.gz | ||
| + | - Installieren der nötigen Libraries sudo apt-get install libbluetooth-dev libcurl4-openssl-dev libboost-all-dev | ||
| + | - Übersetzen mit sudo make | ||
| + | - Kopieren des Ergebnisses sudo cp bin/ | ||
| + | |||
| + | === Anpassen der Konfiguration für Bluetooth === | ||
| + | == Bluetooth Adapter prüfen == | ||
| + | Beispiel: Cambridge Silicon Radio Bluetooth Adapter | ||
| + | < | ||
| + | lsusb | grep -i blue | ||
| + | Bus 002 Device 003: ID 0a12:0001 Cambridge Silicon Radio, Ltd Bluetooth Dongle (HCI mode) | ||
| + | </ | ||
| + | == SMA Geräte mit Bluetooth suchen == | ||
| + | XXXX verdeckt die Angaben des konkreten Beispiels | ||
| + | < | ||
| + | hcitool scan | grep -i SMA | ||
| + | 00: | ||
| + | 00: | ||
| + | </ | ||
| + | |||
| + | == Ergebnisse in SMASpot.cfg eintragen == | ||
| + | Wenn mehrere Inverter vorliegen einfach mehrere cfg Dateien erstellen z.B. 4000Watt.cfg, | ||
| + | |||
| + | == SMASpot testen == | ||
| + | < | ||
| + | SMASpot -v | ||
| + | </ | ||
| + | oder | ||
| + | < | ||
| + | SMASpot -v -cfg4000Watt.cfg | ||
| + | </ | ||
| + | |||
| + | ==== Beispiel STP 9000 TL-20 mit SMASpot per Ethernet / Perl-> | ||
| + | |||
| + | |||
| + | Ich besitze den Wechselrichter STP 9000TL-20 [[http:// | ||
| + | SMAspot -sp0 -v | ||
| + | |||
| + | Ich möchte den Gesamtertrag, | ||
| + | Hierzu habe ich für den Gesamtertrag einen Kanal | ||
| + | El. Energie (Zählerstände) | ||
| + | und für den Ertrag je String | ||
| + | El. Energie (Leistungswerte) | ||
| + | angelegt. Die UUID sind später im Script einzutragen. | ||
| + | Anbei das Script: | ||
| + | |||
| + | <code perl sma.pl> | ||
| + | #!/usr/ | ||
| + | use LWP:: | ||
| + | open STATUS, "/ | ||
| + | or die " | ||
| + | while (< | ||
| + | if (/ETotal:[ ]*(.*)kWh/ | ||
| + | & | ||
| + | |||
| + | } | ||
| + | if (/String 1 Pdc:[ ]*(.*)kW/ | ||
| + | $value = $1; | ||
| + | $value =~ s/\.//g; | ||
| + | & | ||
| + | |||
| + | |||
| + | } | ||
| + | if (/String 2 Pdc:[ ]*(.*)kW/ | ||
| + | $value = $1; | ||
| + | $value =~ s/\.//g; | ||
| + | & | ||
| + | } | ||
| + | } | ||
| + | close STATUS or die "bad netstat: $! $?"; | ||
| + | |||
| + | #------ | ||
| + | sub submitt | ||
| + | { | ||
| + | $uuid = $_[0] ; | ||
| + | $val = $_[1] ; | ||
| + | print $uuid . " : " . $val . " | ||
| + | |||
| + | |||
| + | my $server_endpoint = " | ||
| + | # get(" | ||
| + | #print " | ||
| + | |||
| + | # set custom HTTP request header fields | ||
| + | my $req = HTTP:: | ||
| + | $req-> | ||
| + | $req-> | ||
| + | |||
| + | # add POST data to HTTP request body | ||
| + | $req-> | ||
| + | |||
| + | my $ua = LWP:: | ||
| + | my $resp = $ua-> | ||
| + | if ($resp-> | ||
| + | my $message = $resp-> | ||
| + | print " | ||
| + | } else { | ||
| + | print "HTTP GET error code: ", $resp-> | ||
| + | print "HTTP GET error message: ", $resp-> | ||
| + | } | ||
| + | |||
| + | } | ||
| + | </ | ||
| + | |||
| + | Das Script wird dabei einmal pro Minute mit cron aufgerufen. | ||
| + | < | ||
| + | * * * * * / | ||
| + | </ | ||
| + | |||
| + | Um den Eigenverbrauch zu bestimmen bilde ich die Differenz zwischen der Einspeisung, | ||
| + | Änderung 04.06.2014: Die Berechnung des Gesamtverbrauchs ist aktualisiert. | ||
| + | |||
| + | == Originalscript von Markus == | ||
| + | <code bash eigenverbrauch.sh> | ||
| + | # | ||
| + | echo ' | ||
| + | delete from `data` where `timestamp` = (select * from (select max(timestamp) from data where channel_id = 7) x) and channel_id = 7 | ||
| + | ' | mysql --user=vz --password=fdfdfdfdf volkszaehler -T | ||
| + | echo ' | ||
| + | INSERT INTO `data`( `channel_id`, | ||
| + | SELECT | ||
| + | max(case when `channel_id` | ||
| + | max(case when `channel_id` | ||
| + | FROM `data` where channel_id in (2,4) | ||
| + | and timestamp > (select max(timestamp) from data where `channel_id` = 7) | ||
| + | group by floor(`timestamp`/ | ||
| + | inner join data data_4 on timestamp_4 | ||
| + | ' | mysql --user=vz --password=fdfdfdfdf volkszaehler | ||
| + | |||
| + | echo ' | ||
| + | delete from `data` where channel_id = 8 | ||
| + | ' | mysql --user=vz --password=fdfdfdfdf volkszaehler -T | ||
| + | #and timestamp > (select max(timestamp) from data where `channel_id` = 8) | ||
| + | |||
| + | echo ' | ||
| + | INSERT INTO `data`( `channel_id`, | ||
| + | ( | ||
| + | SELECT | ||
| + | max(case when `channel_id` | ||
| + | max(case when `channel_id` | ||
| + | FROM `data` where channel_id in (1,7) | ||
| + | group by floor(`timestamp`/ | ||
| + | ) a inner join data data_1 on timestamp_1 | ||
| + | ' | mysql --user=vz --password=dsdsdsdsds volkszaehler | ||
| + | |||
| + | </ | ||
| + | auch diese Script wird mit cron gestartet, aber nur ein mal die Stunde. | ||
| + | < | ||
| + | 1 * * * * bash / | ||
| + | </ | ||
| + | |||
| + | == Modifiziertes Script (2. Versuch von Markus) == | ||
| + | Läuft Fix, und berechnet den Eigenverbrauch sehr gut, insbesondere bei Lücken in der AUfzeichnung. | ||
| + | |||
| + | <code bash eigenverbrauch.sh> | ||
| + | # | ||
| + | |||
| + | use Time:: | ||
| + | use LWP:: | ||
| + | use DBI; | ||
| + | # | ||
| + | $debug = 0; | ||
| + | #$debug = 1; | ||
| + | |||
| + | my $dbh = DBI-> | ||
| + | my $ideigen = " | ||
| + | # | ||
| + | # | ||
| + | step_copy_SMA(); | ||
| + | step_max_10(); | ||
| + | step_lin(); | ||
| + | add_missig_dates(1); | ||
| + | step_gesamtverbrauch(); | ||
| + | |||
| + | sub step_get_SMA { # ad300 -> 300 tage | ||
| + | system("/ | ||
| + | } | ||
| + | |||
| + | sub step_copy_SMA { | ||
| + | my $sth = $dbh-> | ||
| + | my $numrows = $sth-> | ||
| + | $dbh-> | ||
| + | |||
| + | print " | ||
| + | } | ||
| + | |||
| + | sub step_clean { | ||
| + | my $sth = $dbh-> | ||
| + | my $numrows = $sth-> | ||
| + | $dbh-> | ||
| + | print " | ||
| + | } | ||
| + | |||
| + | sub step_max_10 {xRe9AnfH7b9uNPS8 | ||
| + | my $inserted = 0; | ||
| + | # Neue Idee zuerst die 10 Minuten lücken füllen ... | ||
| + | # Danach Linear zur Einspeisung ... | ||
| + | my $sth_4_solar = $dbh-> | ||
| + | my $sth_2_einsp = $dbh-> | ||
| + | my $sth = $dbh-> | ||
| + | |||
| + | $sth_4_solar-> | ||
| + | $sth_2_einsp-> | ||
| + | my $timestamp3 | ||
| + | |||
| + | while (my ($id, $channel_id, | ||
| + | while ( $timestamp3 | ||
| + | $timestamp3_old | ||
| + | $value3_old = $value3; | ||
| + | $timestamp3 | ||
| + | $value3 = $value2/ | ||
| + | # | ||
| + | } | ||
| + | my $time_diff_tot = $timestamp3 | ||
| + | my $einsp_MS = ($value3 | ||
| + | my $time_diff = $timestamp | ||
| + | my $einsp_estim = $value3_old + ( ($time_diff | ||
| + | my $value_eigenv = $value - $einsp_estim ; | ||
| + | |||
| + | if ($debug > 0) { | ||
| + | |||
| + | print " | ||
| + | print localtime($timestamp/ | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " NEW" .localtime($timestamp | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | } | ||
| + | if ($time_diff_tot< | ||
| + | $inserted += 1; | ||
| + | $sth-> | ||
| + | $sth-> | ||
| + | } | ||
| + | } | ||
| + | $dbh-> | ||
| + | print " | ||
| + | } | ||
| + | |||
| + | sub step_lin{ | ||
| + | my $inserted = 0; | ||
| + | # Neue Idee zuerst die 10 Minuten lücken füllen ... | ||
| + | # Danach Linear zur Einspeisung ... | ||
| + | my $sth_4_solar = $dbh-> | ||
| + | my $sth_2_einsp = $dbh-> | ||
| + | (SELECT | ||
| + | (SELECT | ||
| + | order by timestamp4" | ||
| + | my $sth = $dbh-> | ||
| + | |||
| + | $sth_4_solar-> | ||
| + | | ||
| + | my $timestamp3 | ||
| + | while (my ($id, $channel_id, | ||
| + | #print localtime($timestamp | ||
| + | while ( $timestamp3 | ||
| + | $timestamp3_old | ||
| + | $value1029_old = $value1029_new; | ||
| + | | ||
| + | $timestamp3 | ||
| + | $value1029_new = $value1029; | ||
| + | | ||
| + | # | ||
| + | } | ||
| + | |||
| + | #my $einsp_MS = ($value3 | ||
| + | #my $time_diff = $timestamp | ||
| + | #my $einsp_estim = $value3_old + ( ($time_diff | ||
| + | #my $value_eigenv = $value - $einsp_estim ; | ||
| + | my $diffeinspeisung = $value1029_new - $value1029_old; | ||
| + | my $differz | ||
| + | my $differz_daz | ||
| + | if ($differz > 0){ | ||
| + | |||
| + | |||
| + | my $anteil | ||
| + | my $diff_ber = $differz_daz * $anteil; | ||
| + | my $value_ber = $diff_ber + $value1029_old; | ||
| + | my $value_ber_einsp = $value- $value_ber; | ||
| + | if ($debug > 0) { | ||
| + | |||
| + | print " | ||
| + | |||
| + | print " | ||
| + | print " ALT:" .localtime($timestamp3_old/ | ||
| + | print " DAZ:" .localtime($timestamp | ||
| + | print " NEU:" .localtime($timestamp3 | ||
| + | print " | ||
| + | print " | ||
| + | print " NEU: $value1029_new\n" | ||
| + | print " ALT: $value1029_old\n" | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " NEU: $value4_new\n" | ||
| + | print " ALT: $value4_old\n" | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " DAZ: $value\n" | ||
| + | print " ALT: $value4_old\n" | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | } | ||
| + | $inserted += 1; | ||
| + | $sth-> | ||
| + | $sth-> | ||
| + | |||
| + | } | ||
| + | } | ||
| + | $dbh-> | ||
| + | print " | ||
| + | } | ||
| + | |||
| + | |||
| + | sub step_gesamtverbrauch { | ||
| + | #my $sth = $dbh-> | ||
| + | #my $numrows = $sth-> | ||
| + | |||
| + | my $inserted = 0; | ||
| + | # Neue Idee zuerst die 10 Minuten lücken füllen ... | ||
| + | # Danach Linear zur Einspeisung ... | ||
| + | my $sth_4_solar = $dbh-> | ||
| + | my $sth_2_einsp = $dbh-> | ||
| + | my $sth = $dbh-> | ||
| + | |||
| + | $sth_4_solar-> | ||
| + | $sth_2_einsp-> | ||
| + | my $timestamp3 | ||
| + | |||
| + | while (my ($id, $channel_id, | ||
| + | while ( $timestamp3 | ||
| + | $timestamp3_old | ||
| + | $value3_old = $value3; | ||
| + | $timestamp3 | ||
| + | $value3 = $value2; | ||
| + | # | ||
| + | } | ||
| + | my $time_diff_tot = $timestamp3 | ||
| + | my $einsp_MS = ($value3 | ||
| + | my $time_diff = $timestamp | ||
| + | my $einsp_estim = $value3_old + ( ($time_diff | ||
| + | my $value_eigenv = $value/1000 + $einsp_estim ; | ||
| + | |||
| + | if ($debug > 0) { | ||
| + | |||
| + | print " | ||
| + | print localtime($timestamp/ | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " NEW" .localtime($timestamp | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | print " | ||
| + | } | ||
| + | #if ($time_diff_tot< | ||
| + | $inserted += 1; | ||
| + | $sth-> | ||
| + | # | ||
| + | #} | ||
| + | } | ||
| + | $dbh-> | ||
| + | print " | ||
| + | } | ||
| + | |||
| + | |||
| + | sub add_missig_dates { | ||
| + | my ($id) = @_; | ||
| + | $inserted = 0; | ||
| + | #my $id = 1; | ||
| + | my $timestamp3 | ||
| + | |||
| + | my $sth = $dbh-> | ||
| + | $sth-> | ||
| + | my $sth2 = $dbh-> | ||
| + | |||
| + | my $t = Time:: | ||
| + | print $t-> | ||
| + | for (my $i= $t-> | ||
| + | # print " | ||
| + | my $timestamp = $i*1000; | ||
| + | while ( $timestamp3 | ||
| + | $timestamp3_old | ||
| + | $value3_old = $value3; | ||
| + | $timestamp3 | ||
| + | $value3 = $value2; | ||
| + | # | ||
| + | } | ||
| + | my $time_diff_tot = $timestamp3 | ||
| + | my $einsp_MS | ||
| + | my $time_diff = $timestamp | ||
| + | my $value | ||
| + | |||
| + | if ($time_diff_tot> | ||
| + | if ($debug > 0) { | ||
| + | print " | ||
| + | print localtime($timestamp/ | ||
| + | |||
| + | print " | ||
| + | print "NEW " .localtime($timestamp | ||
| + | print " | ||
| + | print " | ||
| + | } | ||
| + | $inserted += 1; | ||
| + | $sth2-> | ||
| + | } | ||
| + | } | ||
| + | $dbh-> | ||
| + | print " | ||
| + | } | ||
| + | |||
| + | |||
| + | </ | ||
| + | == Modifiziertes Script (Versuch von Wolfgang) == | ||
| + | <code bash eigenverbrauch.sh> | ||
| + | # | ||
| + | # calculate own PV usage | ||
| + | # Author: Markus 2014-05-20 | ||
| + | # latest version: | ||
| + | # $Header: / | ||
| + | |||
| + | # | ||
| + | # get the delete query for the given channel | ||
| + | # | ||
| + | deletequery() { | ||
| + | local l_channel=$1 | ||
| + | cat << EOF | ||
| + | DELETE | ||
| + | FROM `data` | ||
| + | WHERE `timestamp` = | ||
| + | | ||
| + | AND channel_id = $l_channel; | ||
| + | EOF | ||
| + | } | ||
| + | |||
| + | # | ||
| + | # get the insert query for the given channels | ||
| + | # l_chanel the channel to insert into | ||
| + | # l_c2 - the channel to substract | ||
| + | # l_c3 - the channel to substract the value from | ||
| + | # | ||
| + | insertquery() { | ||
| + | local l_channel=$1 | ||
| + | local l_c2=$2 | ||
| + | local l_c3=$3 | ||
| + | cat << EOF | ||
| + | INSERT INTO `data`( `channel_id`, | ||
| + | SELECT $l_chanel, | ||
| + | FROM ( | ||
| + | SELECT | ||
| + | MAX(CASE WHEN `channel_id` = $l_c2 then timestamp else 0 end) timestamp_2, | ||
| + | MAX(CASE WHEN `channel_id` = $l_c3 then timestamp else 0 end) timestamp_3 | ||
| + | FROM `data` | ||
| + | WHERE channel_id in ($l_c2, | ||
| + | AND timestamp > ( | ||
| + | SELECT MAX(timestamp) FROM data WHERE `channel_id` = $l_channel | ||
| + | ) | ||
| + | GROUP by floor(`timestamp`/ | ||
| + | INNER JOIN data data_2 | ||
| + | ON timestamp_2 | ||
| + | AND data_2.channel_id = $l_c2 | ||
| + | INNER JOIN data data_3 | ||
| + | ON timestamp_3 | ||
| + | AND data_3.channel_id = $l_c3 | ||
| + | EOF | ||
| + | } | ||
| + | |||
| + | # modify according to your volkszaehler mysql database settings | ||
| + | user=vz | ||
| + | password=fdfdfdfdf | ||
| + | db=volkszaehler | ||
| + | |||
| + | # debug setting | ||
| + | debug= | ||
| + | # uncomment to debug sql | ||
| + | #debug=-T | ||
| + | |||
| + | # modify to suite your channel settings | ||
| + | |||
| + | # channel 7 is calculated from as channel 4 - channel 2 / 1000 | ||
| + | deletequery 7 | egrep -v " | ||
| + | insertquery 7 2 4 | egrep -v " | ||
| + | |||
| + | # channel 8 is calculated from as channel 7 - channel 1 / 1000 | ||
| + | echo " | ||
| + | insertquery 8 1 7 | egrep -v " | ||
| + | |||
| + | </ | ||
| + | Und so sieht es aus: | ||
| + | |||
| + | {{:howto: | ||
| + | |||
| + | ==== Beispiel SB 4000 TL-21 + SB 1300TL-10 mit SMASpot und PHP ==== | ||
| + | The solution consists of three parts: | ||
| + | - the bash script sma2vz | ||
| + | - the php script sma2vz.php | ||
| + | - the php helper script vzapihelper.php (same as in the youless page) | ||
| + | |||
| + | the bash script sma2vz has a function configure - this needs to run once you might want | ||
| + | to configure the here-document part in the function inverters of this script | ||
| + | |||
| + | there is a help screen available: | ||
| + | <code bash sma2vz> | ||
| + | ./sma2vz --help | ||
| + | | ||
| + | [ --daytimeonly --lat=lattitude --lon=longitude] | ||
| + | | ||
| + | | [--help] | ||
| + | | [--configure] | ||
| + | |||
| + | | ||
| + | volkszaehler middleware url | ||
| + | |||
| + | | ||
| + | channel uuid for power (watt) PV output | ||
| + | |||
| + | | ||
| + | | ||
| + | |||
| + | | ||
| + | do not post data at night (e.g. if your device does not supply data) | ||
| + | |||
| + | | ||
| + | plant longitude geo coordinate | ||
| + | |||
| + | | ||
| + | plant lattitude geo coordinate | ||
| + | |||
| + | | ||
| + | poll SMA inverters in a loop | ||
| + | |||
| + | | ||
| + | how many secs to wait between each reading (default: 15 secs) | ||
| + | |||
| + | | ||
| + | | ||
| + | | ||
| + | here document to fit your plant' | ||
| + | </ | ||
| + | |||
| + | <code bash configure example> | ||
| + | ./sma2vz --lat 51.244 --lon 6.52 --configure | ||
| + | </ | ||
| + | |||
| + | <code bash loop example> | ||
| + | ./sma2vz \ | ||
| + | --vzurl " | ||
| + | --cuuid_pwr " | ||
| + | --cuuid_kwh " | ||
| + | --daytimeonly --lat 51.244 --lon 6.52 \ | ||
| + | --loop --delay 15 | ||
| + | </ | ||
| + | |||
| + | <code bash cron example> | ||
| + | cd / | ||
| + | / | ||
| + | --vzurl " | ||
| + | --cuuid_pwr " | ||
| + | --cuuid_kwh " | ||
| + | --daytimeonly --lat 51.244 --lon 6.52 >> / | ||
| + | </ | ||
| + | |||
| + | <code | logoutput example> | ||
| + | 2014-06-01 12:52:08 PV: 1409 Watt | ||
| + | 2014-06-01 12:53:07 PV: 2176 Watt | ||
| + | 2014-06-01 12:54:07 PV: 1699 Watt | ||
| + | 2014-06-01 12:55:07 PV: 3375 Watt | ||
| + | 2014-06-01 12:56:07 PV: 3578 Watt | ||
| + | </ | ||
| + | |||
| + | Any feedback please to wf (at) bitplan.com - enjoy! | ||
| + | |||
| + | |||
| + | <code bash sma2vz> | ||
| + | #/ | ||
| + | # SMAspot with Volkszaehler | ||
| + | # WF 2014-05-29 | ||
| + | # $Header: / | ||
| + | |||
| + | # | ||
| + | # get a configuration | ||
| + | # param 1: bluetooth address | ||
| + | # param 2: name of configuration/ | ||
| + | # param 3: " | ||
| + | # param 4: latitude of plant | ||
| + | # param 5: longitude of plant | ||
| + | # param 6: path to output files of SMAspot | ||
| + | # | ||
| + | smaspot_config() { | ||
| + | local l_btaddr=$1 | ||
| + | local l_name=$2 | ||
| + | local l_password=$3 | ||
| + | local l_lon=$4 | ||
| + | local l_lat=$5 | ||
| + | local l_path=$6 | ||
| + | cat << EOF | ||
| + | ################################################################################ | ||
| + | # ____ __ __ _ _ | ||
| + | # / ___|| \/ | / \ ___ _ __ ___ | |_ | ||
| + | # \___ \| |\/| | / _ \ / __| '_ \ / _ \| __| | ||
| + | # ___) | | | |/ ___ \\__ \ |_) | (_) | |_ | ||
| + | # | ||
| + | # |_| | ||
| + | # | ||
| + | # SMAspot.cfg - Configuration file for SMAspot.exe | ||
| + | # SMAspot - Yet another tool to read power production of SMA solar inverters | ||
| + | # (c)2012-2014, | ||
| + | # | ||
| + | # DISCLAIMER: | ||
| + | # A user of SMAspot software acknowledges that he or she is receiving this | ||
| + | # software on an "as is" basis and the user is not relying on the accuracy | ||
| + | # or functionality of the software for any purpose. The user further | ||
| + | # acknowledges that any use of this software will be at his own risk | ||
| + | # and the copyright owner accepts no responsibility whatsoever arising from | ||
| + | # the use or application of the software. | ||
| + | # | ||
| + | ################################################################################ | ||
| + | |||
| + | # SMA Inverter' | ||
| + | # Windows: smaspot -scan | ||
| + | # Linux : hcitool scan | ||
| + | # IMPORTANT FOR SPEEDWIRE USERS: COMMENT OUT BTADDRESS (PUT # IN FRONT) | ||
| + | BTAddress=$l_btaddr | ||
| + | |||
| + | # SMA Inverter' | ||
| + | # If IP_Address is not set or is 0.0.0.0 SMAspot will try to detect the speedwire inverter by broadcast | ||
| + | # If IP_Address is set to a valid IP, SMAspot will try to connect directly to that IP without broadcast detection | ||
| + | # | ||
| + | |||
| + | # User password (default 0000) | ||
| + | Password=$l_password | ||
| + | |||
| + | # MIS_Enabled (Multi Inverter Support: Default=0 Disabled) | ||
| + | # +------------+-------+-------------+ | ||
| + | # | #Inverters | NetID | MIS_Enabled | | ||
| + | # +------------+-------+-------------+ | ||
| + | # | 1 | ||
| + | # +------------+-------+-------------+ | ||
| + | # | 1 | ||
| + | # +------------+-------+-------------+ | ||
| + | # | >1 | > | ||
| + | # +------------+-------+-------------+ | ||
| + | MIS_Enabled=0 | ||
| + | |||
| + | # Plantname | ||
| + | Plantname=$l_name | ||
| + | |||
| + | # OutputPath (Place to store CSV files) | ||
| + | # | ||
| + | # Windows: C: | ||
| + | # Linux : / | ||
| + | # %Y %m and %d will be expanded to Year Month and Day | ||
| + | OutputPath=$l_path/ | ||
| + | |||
| + | # OutputPathEvents (Place to store CSV files for events) | ||
| + | # If omitted, OutputPath is used | ||
| + | OutputPathEvents=$l_path/ | ||
| + | |||
| + | # Position of pv-plant http:// | ||
| + | # Example for Ukkel, Belgium | ||
| + | Latitude=$l_lat | ||
| + | Longitude=$l_lon | ||
| + | |||
| + | # Calculate Missing SpotValues | ||
| + | # If set to 1, values not provided by inverter will be calculated | ||
| + | # eg: Pdc1 = Idc1 * Udc1 | ||
| + | CalculateMissingSpotValues=1 | ||
| + | |||
| + | # DateTimeFormat (default %d/%m/%Y %H:%M:%S) | ||
| + | # For details see strftime() function | ||
| + | # http:// | ||
| + | DateTimeFormat=%Y-%m-%d %H:%M:%S | ||
| + | |||
| + | # DateFormat (default %d/%m/%Y) | ||
| + | DateFormat=%-%m-%d | ||
| + | |||
| + | # DecimalPoint (comma/ | ||
| + | DecimalPoint=point | ||
| + | |||
| + | # TimeFormat (default %H:%M:%S) | ||
| + | TimeFormat=%H: | ||
| + | |||
| + | # SynchTime (default 1 = On) | ||
| + | # If set to 1 the Inverter time is synchronised with pc time | ||
| + | # Some inverters don't have a real-time clock | ||
| + | SynchTime=1 | ||
| + | |||
| + | # SunRSOffset | ||
| + | # Offset to start before sunrise and end after sunset (0-3600 - default 900 seconds) | ||
| + | SunRSOffset=900 | ||
| + | |||
| + | # Locale | ||
| + | # Translate Entries in CSV files | ||
| + | # Surpported locales: de-DE; | ||
| + | # Default en-US | ||
| + | Locale=de-DE | ||
| + | |||
| + | # Timezone | ||
| + | # Select the right timezone in date_time_zonespec.csv | ||
| + | # e.g. Timezone=Europe/ | ||
| + | Timezone=Europe/ | ||
| + | |||
| + | ########################### | ||
| + | ### CSV Export Settings ### | ||
| + | ########################### | ||
| + | # With CSV_* settings you can define the CSV file format | ||
| + | |||
| + | # CSV_Export (default 1 = Enabled) | ||
| + | # Enables or disables the CSV Export functionality | ||
| + | CSV_Export=1 | ||
| + | |||
| + | # CSV_ExtendedHeader (default 1 = On) | ||
| + | # Enables or disables the SMA extended header info (8 lines) | ||
| + | # isep=; | ||
| + | # Version CSV1|Tool SMAspot|Linebreaks CR/ | ||
| + | # etc... | ||
| + | # This is usefull for manual data upload to pvoutput.org | ||
| + | CSV_ExtendedHeader=1 | ||
| + | |||
| + | # CSV_Header (default 1 = On) | ||
| + | # Enables or disables the CSV data header info (1 line) | ||
| + | # dd/MM/yyyy HH: | ||
| + | # This is usefull for manual data upload to pvoutput.org | ||
| + | # If CSV_ExtendedHeader is enabled, CSV_Header is also enabled | ||
| + | CSV_Header=1 | ||
| + | |||
| + | # CSV_SaveZeroPower (default 1 = On) | ||
| + | # When enabled, daily csv files contain all data from 00:00 to 23:55 | ||
| + | # This is usefull for manual data upload to pvoutput.org | ||
| + | CSV_SaveZeroPower=1 | ||
| + | |||
| + | # CSV_Delimiter (comma/ | ||
| + | CSV_Delimiter=semicolon | ||
| + | |||
| + | # CSV_Spot_TimeSource (Inverter|Computer default Inverter) | ||
| + | CSV_Spot_TimeSource=Inverter | ||
| + | |||
| + | # CSV_Spot_WebboxHeader (Default 0 = Off) | ||
| + | # When enabled, use Webbox style header (DcMs.Watt[A]; | ||
| + | CSV_Spot_WebboxHeader=0 | ||
| + | |||
| + | ################################# | ||
| + | ### Online Monitoring Systems ### | ||
| + | ################################# | ||
| + | # | ||
| + | # In the future, multiple online monitoring systems can be defined | ||
| + | # Here we can activate the ones we like | ||
| + | # | ||
| + | ################################ | ||
| + | ### PVoutput Upload Settings ### | ||
| + | ################################ | ||
| + | # PVoutput (default 0 = Disabled) | ||
| + | # Enables or disables the upload functionality to pvoutput.org | ||
| + | # When enabled, be sure to use -u switch on the command line | ||
| + | PVoutput=0 | ||
| + | |||
| + | # | ||
| + | #Sets PVoutput System ID | ||
| + | PVoutput_SID= | ||
| + | |||
| + | # | ||
| + | #Sets PVoutput API Key | ||
| + | PVoutput_Key= | ||
| + | |||
| + | # VoltageLogging sets AC or DC logging. | ||
| + | # Possible values are: | ||
| + | # NONE (disabled) | ||
| + | # MAX(AC) (default) | ||
| + | # AC(PH1) or AC(PH2) or AC(PH3) | ||
| + | # MAX(DC) or DC(ST1) or DC(ST2) | ||
| + | VoltLogging=MAX(AC) | ||
| + | |||
| + | # InverterTemp (default 0 = disabled) | ||
| + | # Enables or disables the upload of the inverter' | ||
| + | InverterTemp=0 | ||
| + | |||
| + | # InverterTempMapTo (default v5 = Use standard PVoutput Temperature Graph) | ||
| + | # In Donation Mode only, map inverter' | ||
| + | # For more info, see http:// | ||
| + | InverterTempMapTo=v5 | ||
| + | |||
| + | # CumulativeEnergy (default 0 = Today' | ||
| + | # Set the cumulative flag = 1 when you wish to pass lifetime energy or 0 for today' | ||
| + | # WARNING!!! DO NOT CHANGE THIS FLAG DURING DAYLIGHT AS THIS WILL MESS UP YOUR PVOUTPUT GRAPHS | ||
| + | CumulativeEnergy=0 | ||
| + | EOF | ||
| + | } | ||
| + | |||
| + | # | ||
| + | # | ||
| + | # | ||
| + | inverters() { | ||
| + | # insert your data from | ||
| + | # | ||
| + | # | ||
| + | # first column is bluetooth address | ||
| + | # second column is the name of the device (here just the wattage is used) | ||
| + | # third column is the password for " | ||
| + | # fourth column is the latitude | ||
| + | # fifth column is the longitude | ||
| + | cat << EOF | ||
| + | 00: | ||
| + | 00: | ||
| + | EOF | ||
| + | } | ||
| + | |||
| + | # | ||
| + | # create configuration files | ||
| + | # | ||
| + | configure() { | ||
| + | checklonlat " | ||
| + | inverters | while read btaddr name password path; do | ||
| + | echo " | ||
| + | smaspot_config $btaddr $name $password $lon $lat $path > ${name}.cfg | ||
| + | done | ||
| + | } | ||
| + | |||
| + | # | ||
| + | # get the sma meter reading | ||
| + | # | ||
| + | getsmameter() { | ||
| + | # temp filename base for SMAspor result | ||
| + | tmp=/ | ||
| + | # read data from all inverters | ||
| + | inverters | while read btaddr name password path; do | ||
| + | # single shot run of SMAspot with no CVS export | ||
| + | # uncomment to debug | ||
| + | #echo " | ||
| + | ./SMAspot -v -nocsv -cfg${name}.cfg > ${tmp}_${name} | ||
| + | # the lines we need look like: | ||
| + | # EToday: 3.358kWh | ||
| + | # ETotal: 5151.294kWh | ||
| + | # Total Pac : | ||
| + | |||
| + | # let's filter the result with awk | ||
| + | cat ${tmp}_${name} | awk ' | ||
| + | # set the field separator fitting the x: y format | ||
| + | BEGIN { FS=":"; | ||
| + | # check the input lines for the three patterns and remove | ||
| + | # the unit at the end - assign to the three variables | ||
| + | # etoday, etotal and totalpac | ||
| + | / | ||
| + | / | ||
| + | /Total Pac/ { totalpac=$2; | ||
| + | # at the end of all lines print out a single json formatted result line | ||
| + | END { | ||
| + | printf(" | ||
| + | json(" | ||
| + | json(" | ||
| + | json(" | ||
| + | } | ||
| + | # helper function to create json name values | ||
| + | function json(name, | ||
| + | # trim value | ||
| + | gsub(" ","", | ||
| + | result=quote(name)":" | ||
| + | return result | ||
| + | } | ||
| + | |||
| + | # helper function to quote a string | ||
| + | function quote(s, | ||
| + | result=doublequote s doublequote; | ||
| + | return result | ||
| + | }' | ||
| + | rm ${tmp}_${name} | ||
| + | done | ||
| + | } | ||
| + | |||
| + | # | ||
| + | # show usage | ||
| + | # | ||
| + | usage() { | ||
| + | cat << EOF | ||
| + | | ||
| + | [ --daytimeonly --lat=lattitude --lon=longitude] | ||
| + | | ||
| + | | [--help] | ||
| + | | [--configure] | ||
| + | |||
| + | | ||
| + | volkszaehler middleware url | ||
| + | |||
| + | | ||
| + | channel uuid for power (watt) PV output | ||
| + | |||
| + | | ||
| + | | ||
| + | |||
| + | | ||
| + | do not post data at night (e.g. if your device does not supply data) | ||
| + | |||
| + | | ||
| + | plant longitude geo coordinate | ||
| + | |||
| + | | ||
| + | plant lattitude geo coordinate | ||
| + | |||
| + | | ||
| + | poll SMA inverters in a loop | ||
| + | |||
| + | | ||
| + | how many secs to wait between each reading (default: 15 secs) | ||
| + | |||
| + | | ||
| + | | ||
| + | | ||
| + | here document to fit your plant' | ||
| + | EOF | ||
| + | exit 1 | ||
| + | } | ||
| + | |||
| + | # | ||
| + | # show error and exit | ||
| + | # | ||
| + | error() { | ||
| + | local l_msg=$1 | ||
| + | local l_hint=$2 | ||
| + | echo "error $0: $l_msg" | ||
| + | echo " | ||
| + | exit 1 | ||
| + | } | ||
| + | |||
| + | # | ||
| + | # check that longitude and lattitude are supplied | ||
| + | # | ||
| + | checklonlat() { | ||
| + | local l_title=" | ||
| + | if [ " | ||
| + | then | ||
| + | error " | ||
| + | fi | ||
| + | if [ " | ||
| + | then | ||
| + | error " | ||
| + | fi | ||
| + | } | ||
| + | |||
| + | # defaults | ||
| + | delay=15 | ||
| + | maxloops=1 | ||
| + | daytimeonly=0 | ||
| + | |||
| + | # check command line arguments | ||
| + | while [ $# -gt 0 ] | ||
| + | do | ||
| + | arg=" | ||
| + | #echo " | ||
| + | case " | ||
| + | | ||
| + | | ||
| + | ;; | ||
| + | |||
| + | | ||
| + | | ||
| + | | ||
| + | ;; | ||
| + | |||
| + | | ||
| + | | ||
| + | | ||
| + | ;; | ||
| + | |||
| + | | ||
| + | | ||
| + | #echo $cuuid_kwh | ||
| + | | ||
| + | ;; | ||
| + | |||
| + | | ||
| + | | ||
| + | ;; | ||
| + | |||
| + | | ||
| + | | ||
| + | | ||
| + | ;; | ||
| + | |||
| + | | ||
| + | | ||
| + | | ||
| + | ;; | ||
| + | |||
| + | | ||
| + | | ||
| + | | ||
| + | ;; | ||
| + | |||
| + | | ||
| + | | ||
| + | ;; | ||
| + | |||
| + | | ||
| + | |||
| + | echo " | ||
| + | | ||
| + | | ||
| + | ;; | ||
| + | *) | ||
| + | echo >&2 " | ||
| + | exit 1 | ||
| + | ;; | ||
| + | | ||
| + | | ||
| + | done | ||
| + | |||
| + | # check that options are set | ||
| + | if [ " | ||
| + | then | ||
| + | error " | ||
| + | fi | ||
| + | if [ " | ||
| + | then | ||
| + | error " | ||
| + | fi | ||
| + | if [ " | ||
| + | then | ||
| + | error " | ||
| + | fi | ||
| + | if [ " | ||
| + | then | ||
| + | checklonlat " | ||
| + | fi | ||
| + | |||
| + | loop=0 | ||
| + | while [ $loop -lt $maxloops ] | ||
| + | do | ||
| + | # get the SMA meter readings | ||
| + | getsmameter | php sma2vz.php --vzurl=$vzurl --cuuid_pwr=$cuuid_pwr --cuuid_kwh=$cuuid_kwh --daytimeonly=$daytimeonly --lat=$lat --lon=$lon | ||
| + | # sleep a while | ||
| + | if [ $maxloops -gt 1 ] | ||
| + | then | ||
| + | sleep $delay | ||
| + | fi | ||
| + | loop=`expr $loop + 1 ` | ||
| + | done | ||
| + | |||
| + | </ | ||
| + | <code php sma2vz.php> | ||
| + | <?php | ||
| + | /** | ||
| + | * read meter data from SMA device | ||
| + | * and post it to volkszaehler | ||
| + | * $Header: / | ||
| + | */ | ||
| + | |||
| + | // common code for reading and posting | ||
| + | require __DIR__.'/ | ||
| + | |||
| + | /** | ||
| + | * check the daytime values | ||
| + | */ | ||
| + | function daytime($latitude, | ||
| + | $result=array(); | ||
| + | // 08:53 CEST | ||
| + | // $time_format = 'H:i T'; | ||
| + | // 08:53 | ||
| + | $time_format = ' | ||
| + | |||
| + | // find time offset in hours | ||
| + | $tzoffset = date(" | ||
| + | |||
| + | $zenith = 90+(50/60); // True sunrise/ | ||
| + | |||
| + | // determine sunrise time | ||
| + | $sunrise = date_sunrise(time(), | ||
| + | $sunrise_time = date($time_format, | ||
| + | // determine sunset time | ||
| + | $sunset = date_sunset(time(), | ||
| + | $sunset_time = date($time_format, | ||
| + | |||
| + | // check whether it's daytime | ||
| + | $sunrise_epoch = date_sunrise(time(), | ||
| + | $sunset_epoch | ||
| + | $time_epoch = time(); // time now | ||
| + | |||
| + | $result[" | ||
| + | |||
| + | $result[" | ||
| + | $result[" | ||
| + | $result[" | ||
| + | return $result; | ||
| + | } | ||
| + | |||
| + | $loop=0; | ||
| + | // possible command line options | ||
| + | // --vzurl= --cuuid_pwr= --cuuid_kwh= --daytimeonly | ||
| + | $longopts=array(" | ||
| + | $shortopts=""; | ||
| + | $options=getopt($shortopts, | ||
| + | // the volkszaehler middleware url | ||
| + | $vzurl=checkoption(" | ||
| + | // channel uuids | ||
| + | // power (watts) | ||
| + | $cuuid_pwr=checkoption(" | ||
| + | // energy (kWh) | ||
| + | $cuuid_kwh=checkoption(" | ||
| + | // daytimeonly? | ||
| + | $daytimeonly=checkoption(" | ||
| + | if ($daytimeonly) { | ||
| + | $latitude=checkoption(" | ||
| + | $longitude=checkoption(" | ||
| + | } | ||
| + | |||
| + | $jsonlines=file(" | ||
| + | $tmeter=array(); | ||
| + | foreach ($jsonlines as $line_nume => $json) { | ||
| + | // get the meter reading | ||
| + | $meter=json_decode($json, | ||
| + | # | ||
| + | foreach ($meter as $name => $value) { | ||
| + | if (!array_key_exists($name, | ||
| + | $tmeter[$name]=0; | ||
| + | $tmeter[$name]+=$value; | ||
| + | } | ||
| + | } | ||
| + | # | ||
| + | //array(3) { [" | ||
| + | // [" | ||
| + | // | ||
| + | $etotal=$tmeter[" | ||
| + | $totalpac=$tmeter[" | ||
| + | if ($daytimeonly) { | ||
| + | $daytime=daytime($latitude, | ||
| + | if (!$daytime[" | ||
| + | printf(" | ||
| + | exit(2); | ||
| + | } | ||
| + | } | ||
| + | # total wattage of all inverters | ||
| + | post2vz($vzurl, | ||
| + | # total kwh of all inverters | ||
| + | post2vz($vzurl, | ||
| + | if ($daytimeonly) { | ||
| + | printf(" | ||
| + | } else { | ||
| + | printf(" | ||
| + | } | ||
| + | exit(0); | ||
| + | ?> | ||
| + | |||
| + | </ | ||
| + | |||
| + | <code php vzapihelper.php> | ||
| + | <?php | ||
| + | /** | ||
| + | * vzapi helper functions | ||
| + | * $Header: / | ||
| + | */ | ||
| + | |||
| + | /** | ||
| + | * get a curl channel | ||
| + | */ | ||
| + | function curl($url) { | ||
| + | // Initiate curl | ||
| + | $ch = curl_init(); | ||
| + | // Disable SSL verification | ||
| + | curl_setopt($ch, | ||
| + | // Will return the response, if false it print the response | ||
| + | curl_setopt($ch, | ||
| + | // Set the url | ||
| + | curl_setopt($ch, | ||
| + | |||
| + | return $ch; | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * read the given url | ||
| + | * @param $url:the url to read from | ||
| + | */ | ||
| + | function readUrl($url) | ||
| + | $ch=curl($url); | ||
| + | |||
| + | // Execute | ||
| + | $result=curl_exec($ch); | ||
| + | return $result; | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * post to the given url | ||
| + | */ | ||
| + | function postUrl($url, | ||
| + | | ||
| + | | ||
| + | // | ||
| + | foreach($fields as $key=> | ||
| + | $fields_string .= $key.' | ||
| + | } | ||
| + | rtrim($fields_string, | ||
| + | curl_setopt($ch, | ||
| + | curl_setopt($ch, | ||
| + | $result=curl_exec($ch); | ||
| + | return $result; | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * post data to vz middleware | ||
| + | * param 1: vzurl - middleware url of volkszaehler | ||
| + | * param 2: channel uuid | ||
| + | * param 3: value to post | ||
| + | */ | ||
| + | | ||
| + | // post data to middleware according to: | ||
| + | // http:// | ||
| + | |||
| + | // adapt timestamp to volkszaehler conventions | ||
| + | $timestamp=time()*1000; | ||
| + | |||
| + | # first | ||
| + | | ||
| + | | ||
| + | | ||
| + | if ($debug) { | ||
| + | echo $presult; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | /** | ||
| + | * check that option $opt is available in $options | ||
| + | * return the value if available | ||
| + | */ | ||
| + | function checkoption($opt, | ||
| + | if (array_key_exists($opt, | ||
| + | return $options[$opt]; | ||
| + | else | ||
| + | die(" | ||
| + | } | ||
| + | |||
| + | ?> | ||
| + | |||
| + | </ | ||
| + | |||
| + | ===== SMA Sunny Boy ===== | ||
| + | Den SMA Sunny Boy kann man per URL auslesen, dazu muss man die Default seite von dem Sunny Boy freischalten, | ||
| + | Dann kann man per request die JSON daten abfragen. | ||
| + | |||
| + | http:// | ||
| + | json response example: | ||
| + | < | ||
| + | ... | ||
| + | " | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | } | ||
| + | ] | ||
| + | }, | ||
| + | ... | ||
| + | </ | ||
| + | |||
| + | Hierzu ein python script, dass den aktullen Wert in eine Datei schreibt, die wir dann mit dem vzlogger auslesen können: | ||
| + | |||
| + | < | ||
| + | import requests | ||
| + | import time | ||
| + | import logging | ||
| + | import argparse | ||
| + | |||
| + | |||
| + | logging.basicConfig( | ||
| + | level=logging.INFO, | ||
| + | format=" | ||
| + | handlers=[logging.StreamHandler()], | ||
| + | ) | ||
| + | |||
| + | sma_host = : str = None | ||
| + | out_put_file | ||
| + | |||
| + | def pullData(): | ||
| + | try: | ||
| + | data = requests.get( | ||
| + | " | ||
| + | ).json() | ||
| + | value = data[" | ||
| + | logging.debug(data) | ||
| + | if value: | ||
| + | return value | ||
| + | except Exception as e: | ||
| + | logging.error(" | ||
| + | return -1 | ||
| + | |||
| + | |||
| + | def main(): | ||
| + | while True: | ||
| + | value = pullData() | ||
| + | if value != -1: | ||
| + | if out_put_file: | ||
| + | f = open(out_put_file, | ||
| + | f.write(str(value)) | ||
| + | f.close() | ||
| + | else: | ||
| + | print(value) | ||
| + | time.sleep(5) | ||
| + | |||
| + | |||
| + | if __name__ == " | ||
| + | print(" | ||
| + | parser = argparse.ArgumentParser() | ||
| + | parser.add_argument(" | ||
| + | parser.add_argument(" | ||
| + | args = parser.parse_args() | ||
| + | |||
| + | if not args.sma: | ||
| + | print(" | ||
| + | exit(-1) | ||
| + | |||
| + | #global sma_host | ||
| + | |||
| + | sma_host = args.sma | ||
| + | print(" | ||
| + | |||
| + | if args.out: | ||
| + | out_put_file = args.out | ||
| + | print(" | ||
| + | else: | ||
| + | print(" | ||
| + | |||
| + | |||
| + | main() | ||
| + | |||
| + | </ | ||
| + | |||
| + | |||
| + | vzlogger.conf file meter: | ||
| + | |||
| + | < | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | ], | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | </code> | ||
hardware/channels/solar_inverters/sma.1516883555.txt.gz · Zuletzt geändert: von jau