Skip to content

Instantly share code, notes, and snippets.

@TheSkorm
Created November 5, 2012 11:02
Show Gist options
  • Select an option

  • Save TheSkorm/4016647 to your computer and use it in GitHub Desktop.

Select an option

Save TheSkorm/4016647 to your computer and use it in GitHub Desktop.
read-growatt modifications from http://snafu.priv.at/mystuff/read-growatt
#!/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