Der Hersteller SMA bietet Solarwechselrichter http://www.sma.de/produkte/solarwechselrichter.html an, deren Daten über verschiedenen Schnittstellen ausgelesen werden können. SMA bietet zum Auslesen die Software „Sunny Explorer“ an, die unter Windows läuft. 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.
Die Software SMAspot ist bei https://smaspot.codeplex.com/ zu finden.
Die folgenden Punkte wurden unter Ubuntu 12.04 LTS getestet sollten aber auch in anderen Debian-kompatiblen Distributionen funktionieren:
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)
XXXX verdeckt die Angaben des konkreten Beispiels
hcitool scan | grep -i SMA 00:80:25:24:XX:XX SMA002d SN: 200221XXXX SN200221XXXX 00:80:25:29:XX:XX SMA002d SN: 213010XXXX SN213010XXXX
Wenn mehrere Inverter vorliegen einfach mehrere cfg Dateien erstellen z.B. 4000Watt.cfg, 1300Watt.cfg
SMASpot -v
oder
SMASpot -v -cfg4000Watt.cfg
Ich besitze den Wechselrichter STP 9000TL-20 http://www.sma.de/produkte/solar-wechselrichter-ohne-transformator/sunny-tripower-5000tl-6000tl-7000tl-8000tl-9000tl.html. Dieser hat direkt einen Ethernetanschluss, der mit dem Tool SMAspot angesprochen werden kann. Das Tool ist zu kompilieren. Sobald es läuft, findet es den Wechselrichter und gibt ein paar Infos aus: SMAspot -sp0 -v
Ich möchte den Gesamtertrag, sowie den Ertrag je String in den Volkszaehler übernehmen: Hierzu habe ich für den Gesamtertrag einen Kanal El. Energie (Zählerstände) und für den Ertrag je String je einen Kanal El. Energie (Leistungswerte) angelegt. Die UUID sind später im Script einzutragen. Anbei das Script:
#!/usr/bin/perl use LWP::UserAgent; open STATUS, "/home/markus/hack/sma/smaspot/bin/Release/SMAspot -sp0 -v |" or die "can't fork: $!"; while (<STATUS>) { if (/ETotal:[ ]*(.*)kWh/){ &submitt("e63106b0-dd25-11e3-9cd5-27aa144849cd", $1); } if (/String 1 Pdc:[ ]*(.*)kW/){ $value = $1; $value =~ s/\.//g; &submitt("103ec300-dd27-11e3-84b2-a98f0b16e92d", $value); } if (/String 2 Pdc:[ ]*(.*)kW/){ $value = $1; $value =~ s/\.//g; &submitt("2d130060-dd27-11e3-b78b-738251a19608", $value); } } close STATUS or die "bad netstat: $! $?"; #------ sub submitt { $uuid = $_[0] ; $val = $_[1] ; print $uuid . " : " . $val . "\n"; my $server_endpoint = "http://localhost/volkszaehler.org/middleware.php/data/${uuid}.json?value=" . $val; # get("http://localhost/volkszaehler.org/middleware.php/data/2d130060-dd27-11e3-b78b-738251a19608.json?value=" . $1)."\n"; #print "serverget = " . $server_endpoint . "\n"; # set custom HTTP request header fields my $req = HTTP::Request->new(POST => $server_endpoint); $req->header('content-type' => 'application/json'); $req->header('x-auth-token' => 'kfksj48sdfj4jd9d'); # add POST data to HTTP request body $req->content(" "); my $ua = LWP::UserAgent->new; my $resp = $ua->request($req); if ($resp->is_success) { my $message = $resp->decoded_content; print "Received reply: $message\n"; } else { print "HTTP GET error code: ", $resp->code, "\n"; print "HTTP GET error message: ", $resp->message, "\n"; } }
Das Script wird dabei einmal pro Minute mit cron aufgerufen.
* * * * * /usr/bin/perl /home/markus/hack/Volksz/sma.pl
Um den Eigenverbrauch zu bestimmen bilde ich die Differenz zwischen der Einspeisung, gemessen am Stromzähler. ( mit vzlogger) und der erzeugten Strommenge des SMA. Da der SMA immer etwas verzögert die Werte anzeigt, lege ich nur einen Messpunkt alle 15 Minuten ab: Die Kanäle sind zuvor in der Weboberfläche anzulegen, und mit mysql die channel_id zu selektieren. Änderung 04.06.2014: Die Berechnung des Gesamtverbrauchs ist aktualisiert.
#!/bin/bash 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`, `timestamp`, `value`) select 7, timestamp_2, data_4.value - data_2.value /1000 from ( SELECT max(case when `channel_id` = 2 then timestamp else 0 end) timestamp_2, max(case when `channel_id` = 4 then timestamp else 0 end) timestamp_4 FROM `data` where channel_id in (2,4) and timestamp > (select max(timestamp) from data where `channel_id` = 7) group by floor(`timestamp`/60/1000/15) ) a inner join data data_2 on timestamp_2 = data_2.timestamp and data_2.channel_id = 2 inner join data data_4 on timestamp_4 = data_4.timestamp and data_4.channel_id = 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`, `timestamp`, `value`) select 8, timestamp_1, (select data.value from data where channel_id = 7 and data.timestamp <= data_1.timestamp order by timestamp desc limit 1) + data_1.value/1000 from ( SELECT max(case when `channel_id` = 1 then timestamp else 0 end) timestamp_1, max(case when `channel_id` = 7 then timestamp else 0 end) timestamp_7 FROM `data` where channel_id in (1,7) group by floor(`timestamp`/60/1000/15) ) a inner join data data_1 on timestamp_1 = data_1.timestamp and data_1.channel_id = 1 ' | mysql --user=vz --password=dsdsdsdsds volkszaehler
auch diese Script wird mit cron gestartet, aber nur ein mal die Stunde.
1 * * * * bash /home/markus/hack/Volksz/eigenverbrauch.sh
Läuft Fix, und berechnet den Eigenverbrauch sehr gut, insbesondere bei Lücken in der AUfzeichnung.
#!/usr/bin/perl use Time::Piece; use LWP::UserAgent; use DBI; #Gesamtverbrauch einbauen $debug = 0; #$debug = 1; my $dbh = DBI->connect("DBI:mysql:database=volkszaehler;", "vz", "Geheimagent",{AutoCommit => 0} ) or die $DBI::errstr; my $ideigen = "7"; #step_clean(); #step_get_SMA(); step_copy_SMA(); step_max_10(); step_lin(); add_missig_dates(1); step_gesamtverbrauch(); sub step_get_SMA { # ad300 -> 300 tage system("/home/markus/hack/sma/SBFspot-3.7.0/SBFspot/mariadb/bin/SBFspot -finq -nocsv -am2 -ad300 -v"); } sub step_copy_SMA { my $sth = $dbh->prepare ("insert into volkszaehler.data (channel_id, timestamp, value) SELECT 4 , TimeStamp * 1000, TotalYield/1000 FROM SMA.DayData where cast(TimeStamp/60/10 as int) not in (select cast(timestamp/1000/60/10 as int) from volkszaehler.data where channel_id = 4) and Serial = 304951132"); my $numrows = $sth->execute(); $dbh->commit(); print "step_copy_SMA done - $numrows copy\n"; } sub step_clean { my $sth = $dbh->prepare ("delete FROM volkszaehler.`data` where channel_id in (1029, 1030, 1031, 8, ".$ideigen.")"); my $numrows = $sth->execute(); $dbh->commit(); print "step_clean done - $numrows delted\n"; } 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->prepare ("SELECT id, channel_id, `timestamp`, value FROM volkszaehler.`data` left outer join (select timestamp as ts1029 from volkszaehler.`data` where channel_id = ".$ideigen." ) x on ts1029 = timestamp where channel_id = 4 and ts1029 is null order by timestamp"); my $sth_2_einsp = $dbh->prepare ("SELECT id, channel_id, `timestamp`, value FROM volkszaehler.`data` where channel_id = 2 order by timestamp"); my $sth = $dbh->prepare ("INSERT into volkszaehler.`data` (channel_id, `timestamp`, value) VALUES (?,?, ?) "); $sth_4_solar->execute (); $sth_2_einsp->execute (); my $timestamp3 = 0; while (my ($id, $channel_id, $timestamp, $value) = $sth_4_solar->fetchrow_array()) { while ( $timestamp3 < $timestamp and my ($id2, $channel_id2, $timestamp2, $value2) = $sth_2_einsp->fetchrow_array()){ $timestamp3_old = $timestamp3; $value3_old = $value3; $timestamp3 = $timestamp2; $value3 = $value2/1000; #print " " .$timestamp3 ."\n"; } my $time_diff_tot = $timestamp3 - $timestamp3_old; my $einsp_MS = ($value3 - $value3_old)/ $time_diff_tot; my $time_diff = $timestamp - $timestamp3_old; my $einsp_estim = $value3_old + ( ($time_diff ) * $einsp_MS ); my $value_eigenv = $value - $einsp_estim ; if ($debug > 0) { print "-------------------------"; print localtime($timestamp/1000)->strftime('%Y-%m-%d-%H:%M:%S'."\n"); print "Zaehlerstand WR: $value \n"; print "Einspeisung:\n"; print " " .localtime($timestamp3_old/1000)->strftime('%Y-%m-%d-%H:%M:%S'.": $value3_old \n"); print " NEW" .localtime($timestamp /1000)->strftime('%Y-%m-%d-%H:%M:%S'.": $einsp_estim \n"); print " " .localtime($timestamp3 /1000)->strftime('%Y-%m-%d-%H:%M:%S'.": $value3 \n"); print "Zeitdifferenz Einspeisung:". ( $timestamp3 - $timestamp3_old ) ." \n"; print "Zaehlerdifferenz Einspeisung:". ( $value3 - $value3_old ) ." \n"; print "einspeisung MS:". $einsp_MS ." \n"; print "eigenverbrauch:". $value_eigenv." \n"; } if ($time_diff_tot< (1000*60*10)){ # nur bis 10 Minuten pausen $inserted += 1; $sth->execute($ideigen, $timestamp, $value_eigenv); $sth->execute(2, $timestamp, $einsp_estim*1000); } } $dbh->commit(); print "step_max_10 done $inserted affected\n"; } 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->prepare ("SELECT id, channel_id, `timestamp`, value FROM volkszaehler.`data` left outer join (select timestamp as ts1029 from volkszaehler.`data` where channel_id = ".$ideigen." ) x on ts1029 = timestamp where channel_id = 4 and ts1029 is null order by timestamp"); my $sth_2_einsp = $dbh->prepare ("select timestamp4, value1029, value4 FROM (SELECT `timestamp` as timestamp4, value as value4 FROM volkszaehler.`data` where channel_id = 4 ) as z4 inner join (SELECT channel_id, `timestamp` as timestamp1029, value as value1029 FROM volkszaehler.`data` where channel_id = ".$ideigen." ) as z2 on timestamp4 = timestamp1029 order by timestamp4"); my $sth = $dbh->prepare ("INSERT into volkszaehler.`data` (channel_id, `timestamp`, value) VALUES (?,?, ?) "); $sth_4_solar->execute (); $sth_2_einsp->execute (); my $timestamp3 = 0; while (my ($id, $channel_id, $timestamp, $value) = $sth_4_solar->fetchrow_array()) { #print localtime($timestamp /1000)->strftime('%Y-%m-%d-%H:%M:%S'."\n"); while ( $timestamp3 < $timestamp and my ($timestamp2, $value1029, $value4, ) = $sth_2_einsp->fetchrow_array()){ $timestamp3_old = $timestamp3; $value1029_old = $value1029_new; $value4_old = $value4_new; $timestamp3 = $timestamp2; $value1029_new = $value1029; $value4_new = $value4; #print " " .$timestamp3 ."\n"; } #my $einsp_MS = ($value3 - $value3_old)/ ( $timestamp3 - $timestamp3_old); #my $time_diff = $timestamp - $timestamp3_old; #my $einsp_estim = $value3_old + ( ($time_diff ) * $einsp_MS ); #my $value_eigenv = $value - $einsp_estim ; my $diffeinspeisung = $value1029_new - $value1029_old; my $differz = $value4_new - $value4_old; my $differz_daz = $value - $value4_old; if ($differz > 0){ my $anteil = $diffeinspeisung /$differz ; my $diff_ber = $differz_daz * $anteil; my $value_ber = $diff_ber + $value1029_old; my $value_ber_einsp = $value- $value_ber; if ($debug > 0) { print "-----------------------------------\n"; print "Zeit:\n"; print " ALT:" .localtime($timestamp3_old/1000)->strftime('%Y-%m-%d-%H:%M:%S'." \n"); print " DAZ:" .localtime($timestamp /1000)->strftime('%Y-%m-%d-%H:%M:%S'." \n"); print " NEU:" .localtime($timestamp3 /1000)->strftime('%Y-%m-%d-%H:%M:%S'." \n"); print "\n"; print "Zaehlerdifferenz EIGENVERB\n"; print " NEU: $value1029_new\n" ; print " ALT: $value1029_old\n" ; print " --------------\n" ; print " $diffeinspeisung\n"; print "\n"; print "Zaehlerdifferenz Erzeugung\n"; print " NEU: $value4_new\n" ; print " ALT: $value4_old\n" ; print " --------------\n" ; print " $differz\n"; print "\n"; print "ANTEIL: $anteil\n"; print "\n"; print "Zaehlerdifferenz Erzeugung2\n"; print " DAZ: $value\n" ; print " ALT: $value4_old\n" ; print " --------------\n" ; print " $differz_daz \n"; print "\n"; print "Differenz Berechnet: $diff_ber\n"; print "Zähler NEU: $value_ber \n"; print "Zähler Einspeisung: $value_ber_einsp \n"; print "\n"; } $inserted += 1; $sth->execute($ideigen, $timestamp, $value_ber); $sth->execute(2, $timestamp, $value_ber_einsp*1000); } } $dbh->commit(); print "step_lin done $inserted affected\n"; } sub step_gesamtverbrauch { #my $sth = $dbh->prepare ("delete FROM volkszaehler.`data` where channel_id in ( 8)"); #my $numrows = $sth->execute(); my $inserted = 0; # Neue Idee zuerst die 10 Minuten lücken füllen ... # Danach Linear zur Einspeisung ... my $sth_4_solar = $dbh->prepare ("SELECT id, channel_id, `timestamp`, value FROM volkszaehler.`data` left outer join (select timestamp as ts1029 from volkszaehler.`data` where channel_id = 8 ) x on ts1029 = timestamp where channel_id = 1 and ts1029 is null and timestamp < (SELECT max(`timestamp`) FROM volkszaehler.`data` where channel_id = 7 ) order by timestamp"); my $sth_2_einsp = $dbh->prepare ("SELECT id, channel_id, `timestamp`, value FROM volkszaehler.`data` where channel_id = 7 order by timestamp"); my $sth = $dbh->prepare ("INSERT into volkszaehler.`data` (channel_id, `timestamp`, value) VALUES (?,?, ?) "); $sth_4_solar->execute (); $sth_2_einsp->execute (); my $timestamp3 = 0; while (my ($id, $channel_id, $timestamp, $value) = $sth_4_solar->fetchrow_array()) { while ( $timestamp3 < $timestamp and my ($id2, $channel_id2, $timestamp2, $value2) = $sth_2_einsp->fetchrow_array()){ $timestamp3_old = $timestamp3; $value3_old = $value3; $timestamp3 = $timestamp2; $value3 = $value2; #print " " .$timestamp3 ."\n"; } my $time_diff_tot = $timestamp3 - $timestamp3_old; my $einsp_MS = ($value3 - $value3_old)/ $time_diff_tot; my $time_diff = $timestamp - $timestamp3_old; my $einsp_estim = $value3_old + ( ($time_diff ) * $einsp_MS ); my $value_eigenv = $value/1000 + $einsp_estim ; if ($debug > 0) { print "-------------------------"; print localtime($timestamp/1000)->strftime('%Y-%m-%d-%H:%M:%S'."\n"); print "Zaehlerstand BEZUG: $value \n"; print "Eigenverbrauch:\n"; print " " .localtime($timestamp3_old/1000)->strftime('%Y-%m-%d-%H:%M:%S'.": $value3_old \n"); print " NEW" .localtime($timestamp /1000)->strftime('%Y-%m-%d-%H:%M:%S'.": $einsp_estim \n"); print " " .localtime($timestamp3 /1000)->strftime('%Y-%m-%d-%H:%M:%S'.": $value3 \n"); print "Zeitdifferenz Einspeisung:". ( $timestamp3 - $timestamp3_old ) ." \n"; print "Zaehlerdifferenz Einspeisung:". ( $value3 - $value3_old ) ." \n"; print "einspeisung MS:". $einsp_MS ." \n"; print "Gesamtverbauch:". $value_eigenv." \n"; } #if ($time_diff_tot< (1000*60*10)){ # nur bis 10 Minuten pausen $inserted += 1; $sth->execute(8, $timestamp, $value_eigenv); #$sth->execute(1031, $timestamp, $einsp_estim*1000); #} } $dbh->commit(); print "step_max_10 done $inserted affected\n"; } sub add_missig_dates { my ($id) = @_; $inserted = 0; #my $id = 1; my $timestamp3 = 0; my $sth = $dbh->prepare ("SELECT id, channel_id, `timestamp`, value FROM volkszaehler.`data` where channel_id = $id order by timestamp"); $sth->execute (); my $sth2 = $dbh->prepare ("INSERT into volkszaehler.`data` (channel_id, `timestamp`, value) VALUES (?,?, ?) "); my $t = Time::Piece->strptime("2014-06-01", "%Y-%m-%d"); print $t->epoch*1000; for (my $i= $t->epoch - 60*60*2-1; $i <= localtime; $i = $i + (24*60*60)) { # print "$i\n"; my $timestamp = $i*1000; while ( $timestamp3 < $timestamp and my ($id2, $channel_id2, $timestamp2, $value2) = $sth->fetchrow_array()){ $timestamp3_old = $timestamp3; $value3_old = $value3; $timestamp3 = $timestamp2; $value3 = $value2; #print " " .$timestamp3 ."\n"; } my $time_diff_tot = $timestamp3 - $timestamp3_old; my $einsp_MS = ($value3 - $value3_old)/ ( $time_diff_tot); my $time_diff = $timestamp - $timestamp3_old; my $value = ($value3_old + ( ($time_diff ) * $einsp_MS )) ; if ($time_diff_tot> (1000*60*60*5)){ # nur bis 10 Minuten pausen if ($debug > 0) { print "-------------------------\n$id\n"; print localtime($timestamp/1000)->strftime('%Y-%m-%d-%H:%M:%S'."\n"); print " " .localtime($timestamp3_old/1000)->strftime('%Y-%m-%d-%H:%M:%S'.": $value3_old \n"); print "NEW " .localtime($timestamp /1000)->strftime('%Y-%m-%d-%H:%M:%S'.": $value \n"); print " " .localtime($timestamp3 /1000)->strftime('%Y-%m-%d-%H:%M:%S'.": $value3 \n"); print "Zeitdifferenz Einspeisung:". ( $timestamp3 - $timestamp3_old ) ." \n"; } $inserted += 1; $sth2->execute($id, $timestamp, $value); } } $dbh->commit(); print "step_max_10 done $inserted affected - $id\n"; }
#!/bin/bash # calculate own PV usage # Author: Markus 2014-05-20 # latest version: # $Header: /home/wf/smaspot/RCS/eigenverbrauch.bash,v 1.5 2014/05/29 10:17:11 wf Exp wf $ # # get the delete query for the given channel # deletequery() { local l_channel=$1 cat << EOF DELETE FROM `data` WHERE `timestamp` = (SELECT MAX(timestamp) FROM data WHERE channel_id = $l_channel) 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`, `timestamp`, `value`) SELECT $l_chanel, timestamp_2, data_3.value - data_2.value /1000 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,$l_c3) AND timestamp > ( SELECT MAX(timestamp) FROM data WHERE `channel_id` = $l_channel ) GROUP by floor(`timestamp`/60/1000/15) ) a INNER JOIN data data_2 ON timestamp_2 = data_2.timestamp AND data_2.channel_id = $l_c2 INNER JOIN data data_3 ON timestamp_3 = data_3.timestamp 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 "^#" | mysql --user=$user --password=$password $db $debug insertquery 7 2 4 | egrep -v "^#" | mysql --user=$user --password=$password $db $debug # channel 8 is calculated from as channel 7 - channel 1 / 1000 echo "DELETE FROM `data` WHERE channel_id = 8" | mysql --user=$user --password=$password $db $debug insertquery 8 1 7 | egrep -v "^#" | mysql --user=$user --password=$password $db $debug
Und so sieht es aus:
The solution consists of three parts:
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:
./sma2vz --help usage: ./sma2vz --vzurl=vzurl --cuuid_pwr=x --cuuid_kwh=y [ --daytimeonly --lat=lattitude --lon=longitude] [--loop --delay=delay] | [--help] | [--configure] --vzurl=<url> volkszaehler middleware url --cuuid_pwr=<uuid> channel uuid for power (watt) PV output --cuuid_kwh=<uuid> channel uuid for energy (kwH) PV total --daytimeonly do not post data at night (e.g. if your device does not supply data) --lon=<longitude> plant longitude geo coordinate --lat=<lattitude> plant lattitude geo coordinate --loop poll SMA inverters in a loop --delay=<secs> how many secs to wait between each reading (default: 15 secs) --configure create SMAsport configuration file(s) modify ./sma2vz inverters shell function here document to fit your plant's data
./sma2vz --lat 51.244 --lon 6.52 --configure
./sma2vz \ --vzurl "http://capri/vz/middleware.php/data" \ --cuuid_pwr "7744bbf0-e74d-11e3-9ec7-xxxx" \ --cuuid_kwh "460feba0-e74f-11e3-8a0d-xxxx" \ --daytimeonly --lat 51.244 --lon 6.52 \ --loop --delay 15
cd /home/wf/smaspot /home/wf/smaspot/sma2vz \ --vzurl "http://capri/vz/middleware.php/data" \ --cuuid_pwr "7744bbf0-e74d-11e3-9ec7-xxxx" \ --cuuid_kwh "460feba0-e74f-11e3-8a0d-xxxx" \ --daytimeonly --lat 51.244 --lon 6.52 >> /var/log/energymeter.log
2014-06-01 12:52:08 PV: 1409 Watt 6699.801 kwH sun rise:05:21 set:21:42 2014-06-01 12:53:07 PV: 2176 Watt 6699.826 kwH sun rise:05:21 set:21:42 2014-06-01 12:54:07 PV: 1699 Watt 6699.855 kwH sun rise:05:21 set:21:42 2014-06-01 12:55:07 PV: 3375 Watt 6699.898 kwH sun rise:05:21 set:21:42 2014-06-01 12:56:07 PV: 3578 Watt 6699.957 kwH sun rise:05:21 set:21:42
Any feedback please to wf (at) bitplan.com - enjoy!
#/bin/bash # SMAspot with Volkszaehler # WF 2014-05-29 # $Header: /home/wf/smaspot/RCS/sma2vz,v 1.8 2014/06/01 10:49:38 wf Exp wf $ # # get a configuration # param 1: bluetooth address # param 2: name of configuration/plant # param 3: "USER" password for SMA device # 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, SBF # # 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's Bluetooth address # Windows: smaspot -scan # Linux : hcitool scan # IMPORTANT FOR SPEEDWIRE USERS: COMMENT OUT BTADDRESS (PUT # IN FRONT) BTAddress=$l_btaddr # SMA Inverter's Speedwire IP address # 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 #IP_Address=0.0.0.0 # User password (default 0000) Password=$l_password # MIS_Enabled (Multi Inverter Support: Default=0 Disabled) # +------------+-------+-------------+ # | #Inverters | NetID | MIS_Enabled | # +------------+-------+-------------+ # | 1 | 1 | Don't Care | # +------------+-------+-------------+ # | 1 | >1 | 0 | # +------------+-------+-------------+ # | >1 | >1 | 1 | # +------------+-------+-------------+ MIS_Enabled=0 # Plantname Plantname=$l_name # OutputPath (Place to store CSV files) # # Windows: C:\TEMP\SMA\%Y # Linux : /home/sbf/Documents/sma/%Y # %Y %m and %d will be expanded to Year Month and Day OutputPath=$l_path/%Y # OutputPathEvents (Place to store CSV files for events) # If omitted, OutputPath is used OutputPathEvents=$l_path/Events # Position of pv-plant http://itouchmap.com/latlong.html # 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://www.cplusplus.com/reference/clibrary/ctime/strftime/ DateTimeFormat=%Y-%m-%d %H:%M:%S # DateFormat (default %d/%m/%Y) DateFormat=%-%m-%d # DecimalPoint (comma/point default comma) DecimalPoint=point # TimeFormat (default %H:%M:%S) TimeFormat=%H:%M:%S # 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;en-US;fr-FR;nl-NL;es-ES;it-IT # Default en-US Locale=de-DE # Timezone # Select the right timezone in date_time_zonespec.csv # e.g. Timezone=Europe/Brussels Timezone=Europe/Berlin ########################### ### 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/LF|Delimiter semicolon|Decimalpoint comma|Precision 3 # 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:mm:ss;kWh;kW # 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/semicolon default semicolon) 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];DcMs.Watt[B]...) 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 #PVoutput_SID #Sets PVoutput System ID PVoutput_SID= #PVoutput_Key #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's temperature InverterTemp=0 # InverterTempMapTo (default v5 = Use standard PVoutput Temperature Graph) # In Donation Mode only, map inverter's temperature to extended data field (v7..v12) # For more info, see http://pvoutput.org/help.html#donations InverterTempMapTo=v5 # CumulativeEnergy (default 0 = Today's Energy) # Set the cumulative flag = 1 when you wish to pass lifetime energy or 0 for today's energy # WARNING!!! DO NOT CHANGE THIS FLAG DURING DAYLIGHT AS THIS WILL MESS UP YOUR PVOUTPUT GRAPHS CumulativeEnergy=0 EOF } # #inverters # inverters() { # insert your data from # hcitool scan | grep SMA # # 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 "USER" that the SMA Device expects # fourth column is the latitude # fifth column is the longitude cat << EOF 00:80:25:24:xx:xy 1300Watt password /home/wf/smaspot 00:80:25:29:xx:xy 4000Watt password /home/wf/smaspot EOF } # # create configuration files # configure() { checklonlat "configure" inverters | while read btaddr name password path; do echo "creating ${name}.cfg for bluetooth addr $btaddr" 1>&2 smaspot_config $btaddr $name $password $lon $lat $path > ${name}.cfg done } # # get the sma meter reading # getsmameter() { # temp filename base for SMAspor result tmp=/tmp/smaspot$$ # 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 "running smaspot with ${name}.cfg for bluetooth addr $btaddr" ./SMAspot -v -nocsv -cfg${name}.cfg > ${tmp}_${name} # the lines we need look like: # EToday: 3.358kWh # ETotal: 5151.294kWh # Total Pac : 0.442kW # let's filter the result with awk cat ${tmp}_${name} | awk ' # set the field separator fitting the x: y format BEGIN { FS=":";doublequote="\x22" } # check the input lines for the three patterns and remove # the unit at the end - assign to the three variables # etoday, etotal and totalpac /EToday:/ { etoday =$2;gsub("kWh","",etoday) } /ETotal:/ { etotal =$2;gsub("kWh","",etotal) } /Total Pac/ { totalpac=$2;gsub("kW" ,"",totalpac) } # at the end of all lines print out a single json formatted result line END { printf("{%s,%s,%s}\n", json("etoday",etoday), json("etotal",etotal), json("totalpac",totalpac)) } # helper function to create json name values function json(name,value,result) { # trim value gsub(" ","",value); result=quote(name)":"quote(value); return result } # helper function to quote a string function quote(s,result) { result=doublequote s doublequote; return result }' rm ${tmp}_${name} done } # # show usage # usage() { cat << EOF usage: $0 --vzurl=vzurl --cuuid_pwr=x --cuuid_kwh=y [ --daytimeonly --lat=lattitude --lon=longitude] [--loop --delay=delay] | [--help] | [--configure] --vzurl=<url> volkszaehler middleware url --cuuid_pwr=<uuid> channel uuid for power (watt) PV output --cuuid_kwh=<uuid> channel uuid for energy (kwH) PV total --daytimeonly do not post data at night (e.g. if your device does not supply data) --lon=<longitude> plant longitude geo coordinate --lat=<lattitude> plant lattitude geo coordinate --loop poll SMA inverters in a loop --delay=<secs> how many secs to wait between each reading (default: 15 secs) --configure create SMAsport configuration file(s) modify $0 inverters shell function here document to fit your plant's data EOF exit 1 } # # show error and exit # error() { local l_msg=$1 local l_hint=$2 echo "error $0: $l_msg" 1>&2 echo " you might want to $l_hint !" 1>&2 exit 1 } # # check that longitude and lattitude are supplied # checklonlat() { local l_title="$1" if [ "$lon" == "" ] then error "option --lon" "supply lon longitude setting for $l_title to work" fi if [ "$lat" == "" ] then error "option --lat" "supply lat lattitude setting for $l_title to work" fi } # defaults delay=15 maxloops=1 daytimeonly=0 # check command line arguments while [ $# -gt 0 ] do arg="$1" #echo "$arg" case "$arg" in --help) usage ;; --vzurl) vzurl="$2" shift ;; --cuuid_pwr) cuuid_pwr="$2" shift ;; --cuuid_kwh) cuuid_kwh="$2" #echo $cuuid_kwh shift ;; --daytimeonly) daytimeonly=1; ;; --lat) lat="$2" shift ;; --lon) lon="$2" shift ;; --delay) delay=$2 shift ;; --loop) maxloops=10000000; ;; --configure) echo "creating configuration files" configure exit ;; *) echo >&2 "Invalid argument: $1" exit 1 ;; esac shift done # check that options are set if [ "$vzurl" == "" ] then error "option --vzurl is missing" "check and use volkszaehler middleware url" fi if [ "${cuuid_pwr}" == "" ] then error "option --cuuid_pwr is missing" "check the uuid for the PV power (Watt) channel" fi if [ "$cuuid_kwh" == "" ] then error "option --cuuid_kwh" "check the uuid for the PV energy (kwH) channel" fi if [ "$daytimeonly" -eq 1 ] then checklonlat "daytimeonly" 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
<?php /** * read meter data from SMA device * and post it to volkszaehler * $Header: /home/wf/smaspot/RCS/sma2vz.php,v 1.6 2014/06/01 10:40:27 wf Exp wf $ */ // common code for reading and posting require __DIR__.'/vzapihelper.php'; /** * check the daytime values */ function daytime($latitude,$longitude) { $result=array(); // 08:53 CEST // $time_format = 'H:i T'; // 08:53 $time_format = 'H:i'; // find time offset in hours $tzoffset = date("Z")/60 / 60; $zenith = 90+(50/60); // True sunrise/sunset // determine sunrise time $sunrise = date_sunrise(time(), SUNFUNCS_RET_STRING, $latitude, $longitude, $zenith, $tzoffset); $sunrise_time = date($time_format, strtotime(date("Y-m-d") . ' '. $sunrise)); // determine sunset time $sunset = date_sunset(time(), SUNFUNCS_RET_STRING, $latitude, $longitude, $zenith, $tzoffset); $sunset_time = date($time_format, strtotime(date("Y-m-d") . ' '. $sunset)); // check whether it's daytime $sunrise_epoch = date_sunrise(time(), SUNFUNCS_RET_TIMESTAMP, $latitude, $longitude, $zenith, $tzoffset); $sunset_epoch = date_sunset(time(), SUNFUNCS_RET_TIMESTAMP, $latitude, $longitude, $zenith, $tzoffset); $time_epoch = time(); // time now $result["daytime"]=($time_epoch < $sunset_epoch and $time_epoch > $sunrise_epoch); $result["rise"] = $sunrise_time; $result["set"] = $sunset_time; $result["time"] = date("Y-m-d H:i:s",time()); return $result; } $loop=0; // possible command line options // --vzurl= --cuuid_pwr= --cuuid_kwh= --daytimeonly $longopts=array("vzurl:","cuuid_pwr:","cuuid_kwh:","lat:","lon:","daytimeonly:"); $shortopts=""; $options=getopt($shortopts,$longopts); // the volkszaehler middleware url $vzurl=checkoption("vzurl",$options); // channel uuids // power (watts) $cuuid_pwr=checkoption("cuuid_pwr",$options); // energy (kWh) $cuuid_kwh=checkoption("cuuid_kwh",$options); // daytimeonly? $daytimeonly=checkoption("daytimeonly",$options); if ($daytimeonly) { $latitude=checkoption("lat",$options); $longitude=checkoption("lon",$options); } $jsonlines=file("php://stdin"); $tmeter=array(); foreach ($jsonlines as $line_nume => $json) { // get the meter reading $meter=json_decode($json, true); #var_dump($meter); foreach ($meter as $name => $value) { if (!array_key_exists($name,$tmeter)) $tmeter[$name]=0; $tmeter[$name]+=$value; } } #var_dump($tmeter); //array(3) { ["etoday"]=> float(2.52) // ["etotal"]=> float(6632.462) //["totalpac"]=> float(2.478) } $etotal=$tmeter["etotal"]; $totalpac=$tmeter["totalpac"]*1000; if ($daytimeonly) { $daytime=daytime($latitude,$longitude); if (!$daytime["daytime"]) { printf("%s after sunset: %s wait for sunrise: %s\n",$daytime["time"],$daytime["set"],$daytime["rise"]); exit(2); } } # total wattage of all inverters post2vz($vzurl,$cuuid_pwr,$totalpac); # total kwh of all inverters post2vz($vzurl,$cuuid_kwh,$etotal); if ($daytimeonly) { printf("%s PV: % 5d Watt % 10.3f kwH sun rise:%s set:%s\n",$daytime["time"],$totalpac,$etotal,$daytime["rise"],$daytime["set"]); } else { printf("PV Current/Total: %4.0f Watt %.3f kwH\n",$totalpac,$etotal); } exit(0); ?>
<?php /** * vzapi helper functions * $Header: /home/wf/youless/RCS/vzapihelper.php,v 1.3 2014/05/30 06:57:23 wf Exp $ */ /** * get a curl channel */ function curl($url) { // Initiate curl $ch = curl_init(); // Disable SSL verification curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Will return the response, if false it print the response curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Set the url curl_setopt($ch, CURLOPT_URL,$url); 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,$fields) { $ch=curl($url); $fields_string=""; //url-ify the data for the POST foreach($fields as $key=>$value) { $fields_string .= $key.'='.$value.'&'; } rtrim($fields_string, '&'); curl_setopt($ch,CURLOPT_POST, count($fields)); curl_setopt($ch,CURLOPT_POSTFIELDS, $fields_string); $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 */ function post2vz($vzurl,$cuuid,$value,$debug=false) { // post data to middleware according to: // http://wiki.volkszaehler.org/development/api/reference // adapt timestamp to volkszaehler conventions $timestamp=time()*1000; # first $posturl=$vzurl."/".$cuuid.".json"; $fields=array("ts"=>$timestamp,"value" => $value ); $presult=postUrl($posturl,$fields); if ($debug) { echo $presult; } } /** * check that option $opt is available in $options * return the value if available */ function checkoption($opt,$options) { if (array_key_exists($opt,$options)) return $options[$opt]; else die("option $opt missing!\n"); } ?>
Den SMA Sunny Boy kann man per URL auslesen, dazu muss man die Default seite von dem Sunny Boy freischalten, so dass man auch online login die Daten sehen kann. Dann kann man per request die JSON daten abfragen.
http://sunnyboyurl/dyn/getDashValues.json json response example:
... "6100_40263F00": { "1": [ { "val": 1488 } ] }, ...
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="%(asctime)s [%(name)-12.12s] [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s", handlers=[logging.StreamHandler()], ) sma_host = : str = None out_put_file : str = None def pullData(): try: data = requests.get( "http://" + sma_host + "/dyn/getDashValues.json", timeout=5 ).json() value = data["result"]["012F-730C09D7"]["6100_40263F00"]["1"][0]["val"] logging.debug(data) if value: return value except Exception as e: logging.error("Unexpected error: {}".format(e)) return -1 def main(): while True: value = pullData() if value != -1: if out_put_file: f = open(out_put_file, "w") f.write(str(value)) f.close() else: print(value) time.sleep(5) if __name__ == "__main__": print("SMA sunny boy file writer logger v0.1") parser = argparse.ArgumentParser() parser.add_argument("--sma", "-s", type=str, help="sma sunny boy host name") parser.add_argument("--out", "-o", type=str, help="output file name is not set sysout is used") args = parser.parse_args() if not args.sma: print("missing sma host name parameter") exit(-1) #global sma_host sma_host = args.sma print("connect to sma host: " + sma_host) if args.out: out_put_file = args.out print("use output file: " + out_put_file) else: print("output file not set use sysout") main()
vzlogger.conf file meter:
{ "enabled": true, "allowskip": false, "aggtime": 300, "channels": [ { "api": "volkszaehler", "uuid": "60964f00-9f85-11ea-8200-37539aba2eb7", "identifier": "", "middleware": "http://localhost/middleware.php", "aggmode": "AVG", "duplicates": 0 } ], "protocol": "file", "path": "/home/pi/sma/sma.log", "rewind": true }