Created
November 5, 2012 11:02
-
-
Save TheSkorm/4016647 to your computer and use it in GitHub Desktop.
read-growatt modifications from http://snafu.priv.at/mystuff/read-growatt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/perl | |
| # $Id: read-growatt,v 1.3 2012/03/13 00:46:43 az Exp $ | |
| # | |
| # File: read-growatt | |
| # Date: Sun Jun 5 15:46:42 2011 | |
| # Author: Alexander Zangerl (az) | |
| # | |
| # Abstract: | |
| # read current statistics values from a growatt inverter | |
| # | |
| # License: GPL v1 or v2 | |
| # | |
| use strict; | |
| use Data::Dumper; | |
| use LWP::UserAgent; | |
| use POSIX qw(strftime); | |
| use Template; | |
| my ($invaddr,$port,$sysid,$apikey)=@ARGV; | |
| die "usage: $0 <inverteraddress> </dev/ttyXYZ> [<sysid> <apikey>]\ndata is uploaded to pvoutput.org if sysid and apikey are set.\n" | |
| if (!$invaddr || !-r $port || ($sysid && !$apikey) || (!$sysid && $apikey)); | |
| # startup: prep the serial port | |
| # cheapest to do it via stty; termios from perl sucks | |
| # note: must disable all the echo stuff! | |
| die "stty failed: $!\n" if (0xffff & system("stty","-F",$port,qw(9600 raw cs8 -cstopb -parenb -parodd | |
| -crtscts -hup -onlcr -echo -echoe -echok -ctlecho))); | |
| open(F,"+<",$port) or die "can't open device $port: $!\n"; | |
| binmode(F); | |
| # reset all dynaddys, gives me address (and, oddly enough, the whole energy reading!) | |
| # same for resetting this inverter... | |
| # my @res=sendrecv(0x3f,0x23,0x7e,0x31,0x44,0); | |
| # gimme serial, anybody? | |
| my $loopa = 0; | |
| my @res=sendrecv(0x3f,0x23,0x7e,0x32,0x53,0); | |
| while ($loopa < 20 && $res[2] ne $invaddr){ | |
| @res=sendrecv(0x3f,0x23,0x7e,0x32,0x53,0); | |
| $loopa = $loopa +1; | |
| sleep(1); | |
| } | |
| if ($loopa == 20){ | |
| die("something went wrong"); | |
| } | |
| $loopa = 0; | |
| my %status; | |
| while (($status{status} == 0 || $status{etoday} == 0 || $status{etotal} == 0 || $status{gridpower} == 0 || $status{gridamp} == 0 || $status{gridvolt} == 0 || $status{gridfreq} == 0 || $status{pvpower} == 0 || $status{pvvolt1} == 0 || $status{temp} == 0 || $status{gridvolt} == 0 ) && $loopa < 20){ | |
| %status=(serial=>pack("C*",@res[6..15]), &readinv(0),&readinv(1)); | |
| $loopa = $loopa +1; | |
| # ask for model and fw | |
| @res=sendrecv(0x3f,0x23,0x7e,0x32,0x43,0); | |
| $status{firmware}=$res[15]; # spec says nothing about the structure of the fw id | |
| $status{model}=sprintf("P%X U%d M%d S%d",($res[13]&0xf0)>>4,($res[13]&0x0f), | |
| ($res[14]&0xf0)>>4,($res[14]&0x0f)); | |
| # ...which also includes pmax and vdc rating | |
| $status{pmax}=(($res[7]<<24)+($res[8]<<16)+($res[9]<<8)+$res[10])/10.0; | |
| $status{vdcnormal}=(($res[11]<<8)+$res[12])/10.0; | |
| $status{stattext}=$status{status}==0?'waiting':$status{status}==1?'normal':'FAULT!'; | |
| sleep(1); | |
| } | |
| print $loopa; | |
| if ($loopa == 20){ | |
| die("something went wrong"); | |
| } | |
| close(F); | |
| # now produce output in a somewhat decent format | |
| my $template='Inverter Status: | |
| ================ | |
| Model: $model Serial: $serial Firmware: $firmware | |
| Rating max: ${pmax}W Vdc: ${vdcnormal}V | |
| Status: $stattext (Fault type: $faulttype) | |
| Temperature: ${temp}°C | |
| PV1: ${pvvolt1}V PV2: ${pvvolt2}V | |
| Input: ${pvpower}W | |
| Grid Voltage: ${gridvolt}V Freq: ${gridfreq}Hz | |
| Output: ${gridpower}W ${gridamp}A | |
| Energy Today: ${etoday}kWh | |
| Energy Total: ${etotal}kWh Time Total: ${hrstotal}hrs | |
| '; | |
| my $t=Template->new({INTERPOLATE=>1,EVAL_PERL=>1}); | |
| $t->process(\$template,\%status); | |
| # now upload to pvoutput.org if asked to | |
| if ($sysid && $apikey) | |
| { | |
| my $updateurl="http://pvoutput.org/service/r2/addstatus.jsp"; | |
| my $ua=LWP::UserAgent->new( | |
| default_headers=>HTTP::Headers->new("X-Pvoutput-Apikey"=>$apikey, | |
| "X-Pvoutput-SystemId"=>$sysid)); | |
| $ua->env_proxy; | |
| my @data=("d"=>strftime("%Y%m%d",localtime), | |
| "t"=>strftime("%H:%M",localtime), | |
| "c1"=>0, | |
| "v1"=>$status{etoday}*1000, | |
| "v2"=>$status{gridpower}, | |
| "v5"=>$status{temp}, | |
| "v6"=>$status{gridvolt}); | |
| my $res=$ua->post($updateurl, | |
| \@data); | |
| if (!$res->is_success) | |
| { | |
| die "pvoutput upload failed: ".$res->decoded_content."\n"; | |
| } | |
| } | |
| exit 0; | |
| # talks to inverter, returns power or energy readings as hash | |
| sub readinv | |
| { | |
| my ($wantenergy)=@_; | |
| my @cmd=(0x3f,0x23,$invaddr,0x32,($wantenergy?0x42:0x41),0); | |
| debug("reading ".($wantenergy?"energy\n":"power\n")); | |
| my @res=sendrecv(@cmd); | |
| my @d=@res[6..($#res-2)]; | |
| if ($wantenergy) | |
| { | |
| return (etoday=>(($d[7]<<8)+$d[8])/10.0, | |
| etotal=>(($d[9]<<24)+($d[10]<<16)+($d[11]<<8)+$d[12])/10.0, | |
| hrstotal=>(($d[13]<<24)+($d[14]<<16)+($d[15]<<8)+$d[16])/10.0); | |
| } | |
| else | |
| { | |
| return (status=>$d[0], | |
| pvvolt1=>(($d[1]<<8)+$d[2])/10.0, | |
| pvvolt2=>(($d[3]<<8)+$d[4])/10.0, | |
| pvpower=>(($d[5]<<8)+$d[6])/10.0, | |
| gridvolt=>(($d[7]<<8)+$d[8])/10.0, | |
| gridamp=>(($d[9]<<8)+$d[10])/10.0, | |
| gridfreq=>(($d[11]<<8)+$d[12])/100.0, | |
| gridpower=>(($d[13]<<8)+$d[14])/10.0, | |
| isofault=>(($d[15]<<8)+$d[16]), | |
| gcfifault=>(($d[17]<<8)+$d[18]), | |
| dcifault=>(($d[19]<<8)+$d[20]), | |
| pvvoltfault=>(($d[21]<<8)+$d[22]), | |
| gridvoltfault=>(($d[23]<<8)+$d[24]), | |
| gridfreqfault=>(($d[25]<<8)+$d[26]), | |
| tempfault=>(($d[27]<<8)+$d[28]), | |
| faulttype=>(($d[29]<<8)+$d[30]), | |
| temp=>(($d[31]<<8)+$d[32])/10.0); | |
| } | |
| } | |
| # sends command to inverter, returns response | |
| # input: command list of bytes (pre-checksum) | |
| # output: response list of bytes - or undef if the read didn't work out | |
| sub sendrecv | |
| { | |
| my (@cmd)=@_; | |
| my $timeout=50; | |
| my $cs=checksum(@cmd); | |
| debug("cmd out: ".hexdump(@cmd,pack("n",$cs))); | |
| my $out=pack("C".@cmd."n",@cmd,$cs); | |
| my $wrote=syswrite(F,$out); | |
| die "write to device failed, wrote $wrote bytes: $!\n" if ($wrote != @cmd+2); | |
| # now try to read a response, but give it a little time before giving up | |
| # format: 0x3f, 0x23, addr, c0 c1 dlen d0 .... dl-1 s0 s1 | |
| my ($read,$response); | |
| eval { | |
| local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required | |
| alarm($timeout); | |
| $read=sysread(F,$response,6); | |
| alarm 0; | |
| }; | |
| die if ($@ and $@ ne "alarm\n"); | |
| if ($read!=6) | |
| { | |
| debug("no header reveived, read $read bytes\n"); | |
| return undef; | |
| } | |
| # now read the rest: optional data and 2 checksum bytes | |
| my @header=unpack("C*",$response); | |
| if ($header[0] != 0x23 || $header[1] != 0x3f) | |
| { | |
| debug("header doesn't match response: got ".hexdump(@header)); | |
| return undef; | |
| } | |
| debug("received header: ".hexdump(@header)); | |
| my $toread=2+$header[5]; | |
| $response=""; | |
| while ($toread) | |
| { | |
| my $x; | |
| my $read=sysread(F,$x,$toread); | |
| die "couldn't read from device (wanted $toread): $!\n" if (!defined $read); | |
| $toread-=$read; | |
| $response.=$x; | |
| } | |
| my @result=unpack("C*",$response); | |
| debug("received remainder: ".hexdump(@result)); | |
| $cs=checksum(@header,@result[0..($#result-2)]); | |
| my $msgcs=($result[-2]<<8)+$result[-1]; | |
| die sprintf("checksum should be %04x but is %04x!\n",$cs,$msgcs) | |
| if ($cs != $msgcs); | |
| return (@header,@result); | |
| } | |
| # input: message as list of bytes | |
| # output: the checkup 16bit int. | |
| sub checksum | |
| { | |
| my @tosend=@_; | |
| my $sum=0; | |
| for my $i (0..$#tosend) | |
| { | |
| $sum+=$tosend[$i]^$i; | |
| } | |
| $sum=0xffff if (!$sum or $sum>0xffff); | |
| return $sum; | |
| } | |
| sub hexdump | |
| { | |
| return join(" ", map { sprintf("%02x",$_); } (@_)); | |
| } | |
| sub debug | |
| { | |
| return if (!$ENV{DEBUG}); | |
| print STDERR @_; | |
| print STDERR "\n" if ($_[$#_]!~/\n$/); | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment