File Coverage

blib/lib/Device/Inverter/Aurora.pm
Criterion Covered Total %
statement 145 202 71.7
branch 40 104 38.4
condition 30 91 32.9
subroutine 38 50 76.0
pod 41 43 95.3
total 294 490 60.0


line stmt bran cond sub pod time code
1             package Device::Inverter::Aurora;
2              
3 1     1   131258 use 5.008008;
  1         3  
  1         38  
4 1     1   6 use strict;
  1         2  
  1         34  
5 1     1   5 use warnings;
  1         7  
  1         33  
6 1     1   5 use Carp qw/croak carp confess/;
  1         2  
  1         155  
7              
8             require Exporter;
9              
10             our @ISA = qw(Exporter);
11             our $TEST = 0;
12              
13             my $onWindows = $^O eq "MSWin32" || $^O eq "cygwin";
14             if ($onWindows) {
15             require Win32::SerialPort;
16             }
17             else {
18             require Device::SerialPort;
19             }
20              
21 1     1   580 use Device::Inverter::Aurora::Constants;
  1         3  
  1         24  
22 1     1   725 use Device::Inverter::Aurora::Strings;
  1         3  
  1         7001  
23              
24             our %EXPORT_TAGS = (
25             CumulatedPeriod => [map {s/^.+:://; $_} grep {/Device::Inverter::Aurora::CUMULATED_/} keys %constant::declared],
26             DSP => [map {s/^.+:://; $_} grep {/Device::Inverter::Aurora::DSP_/} keys %constant::declared],
27             Counters => [map {s/^.+:://; $_} grep {/Device::Inverter::Aurora::COUNTER_/} keys %constant::declared],
28             Operations => [map {s/^.+:://; $_} grep {/Device::Inverter::Aurora::OP_/} keys %constant::declared],
29             );
30              
31             # Combine all the tags to an :all tag
32             {my %s; push @{$EXPORT_TAGS{all}}, grep {!$s{$_}++} @{$EXPORT_TAGS{$_}} foreach keys %EXPORT_TAGS;}
33              
34             our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } );
35              
36             our @EXPORT = qw( );
37              
38             our $VERSION = '0.05';
39              
40             sub _error {
41 0     0   0 my $self = shift;
42 0         0 my $error = shift;
43              
44 0         0 $self->{error} = $error;
45 0 0       0 carp $error unless $self->{quiet};
46 0         0 return 1;
47             }
48              
49             sub lastError {
50 0     0 1 0 my $self = shift;
51 0         0 my $error = $self->{error};
52 0         0 $self->{error} = undef;
53 0         0 return $error;
54             }
55              
56             sub new {
57 1     1 1 1483 my $caller = shift;
58 1         3 my $caller_is_ref = ref $caller;
59 1   33     7 my $class = $caller_is_ref || $caller;
60              
61 1 50       7 my %args = ref $_ eq 'HASH' ? %{shift @_} : @_;
  0         0  
62              
63             # Extract some configuration from the given arguments
64 1   50     7 my $debug = $args{debug} || 0;
65 1   50     6 my $retries = $args{retries} || 0;
66 1   50     6 my $backoff = $args{backoff} || 1;
67 1   50     7 my $address = $args{address} || 2;
68 1   50     8 my $port_str = $args{port} || '/dev/ttyS0';
69 1   50     15 my $raw = $args{raw} || 0;
70 1   50     6 my $quiet = $args{quiet} || 0;
71              
72             # Configure the serial port
73 1 0       14 my $port = ($TEST
    50          
    50          
74             ? new Test::Device::SerialPort($port_str, debug => $debug)
75             : ($onWindows
76             ? new Win32::SerialPort($port_str, debug => $debug)
77             : new Device::SerialPort($port_str, debug => $debug)
78             )
79             ) or croak "Can't open $port_str: $^E";
80              
81             # Again, mostly from the arguments provided
82 1   50     70 $port->baudrate($args{baudrate} || 19200);
83 1   50     20 $port->parity($args{parity} || 'none');
84 1   50     18 $port->databits($args{databits} || 8);
85 1   50     25 $port->stopbits($args{stopbits} || 1);
86 1   50     25 $port->datatype($args{datatype} || 'raw');
87 1   50     17 $port->handshake($args{handshake} || 'none');
88 1   50     17 $port->read_const_time($args{read_const_time} || 150);
89              
90 1 50       14 $port->write_settings or warn "Unable to write settings to $port_str";
91              
92             # Does this even work?
93 1         9 $port->purge_all;
94              
95 1         18 my $self = bless {
96             port_str => $port_str,
97             debug => $debug,
98             quiet => $quiet,
99             retries => $retries,
100             backoff => $backoff,
101             port => $port,
102             address => $address,
103             error => undef,
104             }, $class;
105              
106 1         4 return $self;
107             }
108              
109             sub raw {
110 0     0 1 0 my ($self, $raw) = @_;
111 0 0       0 $self->{raw} = $raw if defined $raw;
112 0         0 return $self->{raw};
113             }
114              
115             sub communicate {
116 25     25 1 85 my ($self, $address, $command, @args) = @_;
117 25 50       139 $address = $self->{address} unless defined $address;
118              
119             # Build a 8 byte space padded buffer based on address, command, and given arguments
120 25         63 my @buffer = ($address, $command);
121 25 100       78 push @buffer, map {defined $args[$_] ? $args[$_] : 32} 0..5;
  150         378  
122              
123             # Pack that buffer into a binary string, append CRC to it
124 25         147 my $str = pack 'C8', @buffer;
125 25         98 $str .= pack 'v', crc($str);
126              
127             # Try as many times as permitted to send to the inverter and get a reply
128 25         43 my $try = -1;
129 25         168 while ($try++ < $self->{retries}) {
130 25         184 $self->{port}->purge_all;
131              
132             # Give the coms a break for a $backoff period
133 25 50 33     317 sleep $self->{backoff} if $try > 1 && $self->{backoff};
134              
135             # Transmit data, make sure 10 bytes are sent
136 25 50       101 warn "Sending " , hexstr($str) , "\n" if $self->{debug};
137 25         154 my $sent = $self->{port}->write($str);
138 25 50 0     6028401 $self->_error("Failed to write all bytes") and next unless defined $sent and $sent == 10;
      33        
139              
140             # Receive data, make sure that 8 bytes are read
141 25         267 my $read = $self->{port}->read(8);
142 25 50       985 warn "Received " , hexstr($read) , "\n" if $self->{debug};
143 25 50 0     153 $self->_error("Failed to read in all bytes") and next unless defined $read and length $read == 8;
      33        
144              
145             # First 6 bytes of the reply are unsigned characters of data, the last 2 are a short CRC
146 25         293 my @reply = unpack 'C6v', $read;
147 25         67 my $reply_crc = pop @reply;
148              
149             # Verify the CRC matches
150 25 50 0     355 $self->_error("CRC failure") and next unless crc(substr($read,0,6)) == $reply_crc;
151              
152 25 50       331 return wantarray ? @reply : \@reply;
153             }
154              
155 0         0 confess "Read failure, $try attempts made.";
156             }
157              
158             sub translate {
159 36     36 1 73 my ($input, $matrix) = @_;
160              
161             # Given a hash and a valid $input, return the translation
162 36 100 66     349 if (ref $matrix eq 'HASH' && defined $matrix->{$input}) {
    50 33        
163 3         28 return $matrix->{$input}
164             }
165             # Given an array and a valid $input, return the translation
166             elsif (ref $matrix eq 'ARRAY' && defined $matrix->[$input]) {
167 33         155 return $matrix->[$input];
168             }
169              
170             # Input was invalid or otherwise unknown
171 0         0 return 'unknown';
172             }
173              
174             sub transmissionCheck {
175 23     23 1 57 my ($self, $input) = @_;
176              
177             # Get a translation for debugging
178 23         112 my $translation = translate($input, \@TransmissionStates);
179 23 50       121 warn "Transmission state check: $translation ($input)\n" if $self->{debug};
180              
181             # All is good in the world
182 23 50       198 return 1 if $input == 0;
183              
184             # All is not so good, complain and return false
185 0         0 $self->_error("Transmission State: $translation ($input)");
186 0         0 return 0;
187             }
188              
189             sub crc {
190 50     50 0 223 my $str = shift;
191 50         94 my $crc = 0xffff;
192 50         282 foreach my $chr (unpack 'C*', $str) {
193 350         1137 for (my $i = 0, my $data =int 0xff & $chr; $i < 8; $i++, $data >>= 1) {
194 2800 100       28258 $crc = ($crc & 0x0001) ^ ($data & 0x0001) ? ($crc >> 1) ^ 0x8408 : $crc >> 1;
195             }
196             }
197 50         369 return 0xffff & ~$crc;
198             }
199              
200             sub hexstr {
201 0     0 0 0 my $str = shift;
202 0         0 return join(' ', unpack('H2'x(length $str), $str));
203             }
204              
205              
206             sub commCheck {
207 1     1 1 2351 my ($self, $address) = @_;
208              
209 1         7 my @reply = $self->communicate($address, OP_GET_VERSION, 0);
210 1 50 33     18 if (@reply && $self->transmissionCheck($reply[0])) {
211 1         7 my $translation = translate(chr($reply[2]), \%ProductNames);
212 1 50       5 warn "Product name: $translation\n" if $self->{debug};
213 1         11 return 1;
214             }
215 0         0 return 0;
216             }
217              
218             sub getState {
219 1     1 1 2792 my ($self, $address) = @_;
220              
221 1         8 my @reply = $self->communicate($address, OP_GET_STATE, 0);
222 1 50 33     14 if (@reply && $self->transmissionCheck($reply[0])) {
223 1         6 my %result = (
224             globalState => [$reply[1], translate($reply[1], \@GlobalStates)],
225             inverterState => [$reply[2], translate($reply[2], \@InverterStates)],
226             channel1DCDCState => [$reply[3], translate($reply[3], \@DCDCStates)],
227             channel2DCDCState => [$reply[4], translate($reply[4], \@DCDCStates)],
228             alarmState => [$reply[5], translate($reply[5], \@AlarmStates)],
229             );
230 1 50       13 return wantarray ? %result : \%result;
231             }
232 0 0       0 return wantarray ? () : undef;
233             }
234              
235             sub getLastAlarms {
236 1     1 1 3191 my ($self, $address) = @_;
237              
238 1         6 my @reply = $self->communicate($address, 86, 0);
239 1 50 33     16 if (@reply && $self->transmissionCheck($reply[0])) {
240 1         5 my @result = map {[$_, translate($_, \@AlarmStates)]} @reply[2,3,4,5];
  4         12  
241 1 50       13 return wantarray ? @result : \@result;
242             }
243 0 0       0 return wantarray ? () : undef;
244             }
245              
246             sub getPartNumber {
247 1     1 1 3805 my ($self, $address) = @_;
248              
249 1         8 my @reply = $self->communicate($address, OP_GET_PART_NUMBER, 0);
250             # Simple 6 character string to return
251 1         17 return pack('C*', @reply);
252             }
253              
254             sub getSerialNumber {
255 1     1 1 2352 my ($self, $address) = @_;
256              
257 1         5 my @reply = $self->communicate($address, OP_GET_SERIAL_NUMBER, 0);
258             # Simple 6 character string to return
259 1         17 return pack('C*', @reply);
260             }
261              
262             sub getVersion {
263 1     1 1 2589 my ($self, $address) = @_;
264              
265 1         15 my @reply = $self->communicate($address, OP_GET_VERSION, 0, 46);
266 1 50 33     17 if (@reply && $self->transmissionCheck($reply[0])) {
267 1 50       11 my %result = (
    50          
268             model => [$reply[2], translate(chr($reply[2]), \%ProductNames)],
269             regulation => [$reply[3], translate(chr($reply[3]), \%ProductSpec)],
270             transformer => [$reply[4], $reply[4] == 84 ? 'transformer' : 'transformerless'],
271             type => [$reply[5], $reply[5] == 87 ? 'wind' : 'photovoltic'],
272             );
273 1 50       16 return wantarray ? %result : \%result;
274             }
275 0 0       0 return wantarray ? () : undef;
276             }
277              
278             sub getManufactureDate {
279 1     1 1 4784 my ($self, $address) = @_;
280              
281 1         8 my @reply = $self->communicate($address, OP_GET_MANUFACTURING_DATE, 0);
282 1 50 33     15 if (@reply && $self->transmissionCheck($reply[0])) {
283             # Two simple strings, month is first two bytes, year is last two.
284 1         15 my %result = (
285             year => pack('C*', @reply[4, 5]),
286             month => pack('C*', @reply[2, 3]),
287             );
288 1 50       19 return wantarray ? %result : \%result;
289             }
290 0 0       0 return wantarray ? () : undef;
291             }
292              
293             sub getFirmwareVersion {
294 1     1 1 4754 my ($self, $address) = @_;
295              
296 1         7 my @reply = $self->communicate($address, OP_GET_FIRMWARE_VERSION, 0);
297 1 50 33     16 if (@reply && $self->transmissionCheck($reply[0])) {
298             # Dot delimited characters
299 1         7 return join '.', map{chr} @reply[2, 3, 4, 5];
  4         24  
300             }
301 0         0 return undef;
302             }
303              
304             sub getConfiguration {
305 1     1 1 9965 my ($self, $address) = @_;
306              
307 1         9 my @reply = $self->communicate($address, OP_GET_CONFIGURATION, 0);
308 1 50 33     53 if (@reply && $self->transmissionCheck($reply[0])) {
309 1         8 my @result = ($reply[2],translate($reply[2], \@ConfigurationStrings));
310 1 50       21 return wantarray ? @result : \@result;
311             }
312 0 0       0 return wantarray ? () : undef;
313             }
314              
315             sub getCumulatedEnergy {
316 6     6 1 21 my ($self, $period, $address) = @_;
317              
318 6         38 my @reply = $self->communicate($address, OP_GET_CUMULATED_ENERGY, $period, 0);
319 6 50 33     58 if (@reply && $self->transmissionCheck($reply[0])) {
320             # Data returned is a long expressing watts, pack the 4 bytes
321 6         45 my $packed = pack 'C*', @reply[2, 3, 4, 5];
322             # Return raw, or a long.
323 6 50       84 return $self->{raw} ? $packed : unpack 'N', $packed;
324             }
325 0         0 return undef;
326             }
327              
328 1     1 1 10667 sub getDailyEnergy {shift->getCumulatedEnergy(CUMULATED_DAILY, shift);}
329 1     1 1 7853 sub getWeeklyEnergy {shift->getCumulatedEnergy(CUMULATED_WEEKLY, shift);}
330 1     1 1 9362 sub getMonthlyEnergy {shift->getCumulatedEnergy(CUMULATED_MONTHLY, shift);}
331 1     1 1 2386 sub getYearlyEnergy {shift->getCumulatedEnergy(CUMULATED_YEARLY, shift);}
332 1     1 1 16468 sub getTotalEnergy {shift->getCumulatedEnergy(CUMULATED_TOTAL, shift);}
333 1     1 1 3771 sub getPartialEnergy {shift->getCumulatedEnergy(CUMULATED_PARTIAL, shift);}
334              
335             sub getDSPData {
336 10     10 1 30 my ($self, $parameter, $address) = @_;
337              
338 10         91 my @reply = $self->communicate($address, OP_GET_DSP, $parameter, 0);
339 10 50 33     108 if (@reply && $self->transmissionCheck($reply[0])) {
340             # Data returned is a single precision float, pack the 4 bytes
341 10         76 my $packed = pack 'C*', @reply[5, 4, 3, 2];
342             # Return raw or float.
343 10 50       144 return $self->{raw} ? $packed : unpack 'f', $packed;
344             }
345 0         0 return undef;
346             }
347              
348 1     1 1 2183 sub getFrequency {shift->getDSPData(DSP_FREQUENCY, shift);}
349 1     1 1 1811 sub getGridVoltage {shift->getDSPData(DSP_GRID_VOLTAGE, shift);}
350 1     1 1 6029 sub getGridCurrent {shift->getDSPData(DSP_GRID_CURRENT, shift);}
351 1     1 1 4956 sub getGridPower {shift->getDSPData(DSP_GRID_POWER, shift);}
352 1     1 1 2203 sub getInput1Voltage {shift->getDSPData(DSP_INPUT_1_VOLTAGE, shift);}
353 1     1 1 3205 sub getInput1Current {shift->getDSPData(DSP_INPUT_1_CURRENT, shift);}
354 1     1 1 2267 sub getInput2Voltage {shift->getDSPData(DSP_INPUT_2_VOLTAGE, shift);}
355 1     1 1 11320 sub getInput2Current {shift->getDSPData(DSP_INPUT_2_CURRENT, shift);}
356 1     1 1 3418 sub getInverterTemperature {shift->getDSPData(DSP_INVERTER_TEMPERATURE, shift);}
357 1     1 1 9066 sub getBoosterTemperature {shift->getDSPData(DSP_BOOSTER_TEMPERATURE, shift);}
358              
359             sub getJoules {
360 0     0 1   my ($self, $address) = @_;
361              
362 0           my @reply = $self->communicate($address, OP_GET_LAST_10_SEC_ENERGY, 0);
363 0 0 0       if (@reply && $self->transmissionCheck($reply[0])) {
364             # Data returned is a short, only pack the first two bytes
365 0           my $packed = pack 'C*', @reply[2, 3];
366             # Return raw or short.
367 0 0         return $self->{raw} ? $packed : (unpack 'n', $packed) * 0.319509;
368             }
369             return undef
370 0           }
371              
372             sub getTime {
373 0     0 1   my ($self, $address) = @_;
374              
375 0           my @reply = $self->communicate($address, OP_GET_TIME, 0);
376 0 0 0       if (@reply && $self->transmissionCheck($reply[0])) {
377             # Data returned is a long, pack the 4 bytes
378 0           my $packed = pack 'C*', @reply[2, 3, 4, 5];
379             # Return as raw or as long offset for unix epoch (Inverter epoch is 946706400 later than unix)
380 0 0         return $self->{raw} ? $packed : (unpack 'N', $packed) + 946706400;
381             }
382 0           return undef;
383             }
384              
385             sub setTime {
386 0     0 1   my ($self, $time, $address) = @_;
387              
388             # Convert from unix epoch to inverter epoch
389 0           $time -= 946706400;
390              
391             # Pack the long and unpack into a 4 character array
392 0           my @args = unpack 'C*', pack 'N', $time;
393 0           my @reply = $self->communicate($address, OP_SET_TIME, @args, 0);
394 0 0 0       if (@reply && $self->transmissionCheck($reply[0])) {
395 0           return 1;
396             }
397 0           return undef;
398             }
399              
400             sub getCounterData {
401 0     0 1   my ($self, $counter, $address) = @_;
402              
403 0           my @reply = $self->communicate($address, OP_GET_COUNTERS, $counter);
404 0 0 0       if (@reply && $self->transmissionCheck($reply[0])) {
405             #Data returned is a long, pack the 4 bytes
406 0           my $packed = pack 'C*', @reply[2, 3, 4, 5];
407             # Return raw or long
408 0 0         return $self->{raw} ? $packed : unpack 'N', $packed;
409             }
410 0           return undef;
411             }
412              
413 0     0 1   sub getTotalRunTime {shift->getCounterData(COUNTER_TOTAL, shift);}
414 0     0 1   sub getPartialRunTime {shift->getTCounterData(COUNTER_PARTIAL, shift);}
415 0     0 1   sub getGridRunTime {shift->getCounterData(COUNTER_GRID, shift);}
416 0     0 1   sub getResetRunTime {shift->getCounterData(COUNTER_RESET, shift);}
417              
418             1;
419             __END__