File Coverage

blib/lib/Device/Plugwise.pm
Criterion Covered Total %
statement 159 357 44.5
branch 38 116 32.7
condition 7 21 33.3
subroutine 30 44 68.1
pod 12 12 100.0
total 246 550 44.7


line stmt bran cond sub pod time code
1 1     1   13806 use strict;
  1         1  
  1         31  
2 1     1   5 use warnings;
  1         1  
  1         47  
3              
4             package Device::Plugwise;
5             {
6             $Device::Plugwise::VERSION = '0.5';
7             }
8              
9 1     1   13 use Carp qw/croak carp/;
  1         2  
  1         44  
10 1     1   1615 use Device::SerialPort qw/:PARAM :STAT 0.07/;
  1         27976  
  1         345  
11 1     1   12 use Fcntl;
  1         2  
  1         345  
12 1     1   58997 use IO::Select;
  1         2072  
  1         64  
13 1     1   1097 use Socket;
  1         8465  
  1         687  
14 1     1   15 use Symbol qw(gensym);
  1         2  
  1         154  
15 1     1   1121 use Time::HiRes;
  1         2277  
  1         6  
16 1     1   73541 use Digest::CRC qw(crc);
  1         5933  
  1         100  
17 1     1   1883 use Math::Round;
  1         9045  
  1         91  
18              
19 1     1   12 use constant DEBUG => $ENV{DEVICE_PLUGWISE_DEBUG};
  1         3  
  1         83  
20 1     1   4 use constant XPL_DEBUG => $ENV{DEVICE_PLUGWISE_XPL_DEBUG};
  1         2  
  1         50  
21 1     1   5 use constant PHY_DEBUG => $ENV{DEVICE_PLUGWISE_PHY_DEBUG};
  1         2  
  1         9683  
22              
23             #use constant DEBUG => 1; # Print debug information on the module itself
24             #use constant XPL_DEBUG => 0; # Print debug information on the plugwise protocol
25             #use constant PHY_DEBUG => 0; # Print debug information on the physical link
26              
27             # ABSTRACT: Perl module to communicate with Plugwise hardware
28              
29              
30             sub new {
31 1     1 1 1205 my ( $pkg, %p ) = @_;
32              
33 1         16 my $self = bless {
34             _buf => '',
35             _q => [],
36             _response_queue => {},
37             _connected => 0,
38             baud => 115200,
39             device => '',
40             list_circles_count => 16,
41             %p
42             }, $pkg;
43              
44 1 50       7 if ( exists $p{filehandle} ) { # do not open device when a filehandle
45 1         7 delete $self->{device}; # was defined (this is for testing purposes)
46             }
47             else {
48 0         0 $self->_open();
49             }
50              
51 1         4 $self->_stick_init(); # connect to the USB stick
52              
53 1         7 my $msg = $self->read(3);
54              
55 1 50 33     15 if ( !defined($msg) || ( $msg ne 'connected' && !exists $p{filehandle} ) )
      33        
56             {
57 0         0 croak
58             "The device connected to $self->{device} does not appear to be a Stick";
59             }
60              
61             # Request the calibration info for the known Circles
62             # Set the 'dont_scan_network' parameter to skip this (for testing)
63 1 50       10 return $self if ( exists $p{dont_scan_network} );
64              
65 0         0 $self->_query_connected_circles();
66              
67             # And ensure all initialization commands in the queue are processed
68 0         0 PROCESS_QUEUE: do {
69 0         0 $msg = $self->read(3);
70             } while ( defined $msg );
71              
72 0         0 return $self;
73              
74             }
75              
76              
77 0     0 1 0 sub device { shift->{device} }
78              
79              
80 0     0 1 0 sub baud { shift->{baud} }
81              
82              
83 0     0 1 0 sub port { shift->{port} }
84              
85              
86 4     4 1 191 sub filehandle { shift->{filehandle} }
87              
88              
89 0     0 1 0 sub list_circles_count { shift->{list_circles_count} }
90              
91             sub _open {
92 0     0   0 my $self = shift;
93 0 0       0 if ( $self->{device} =~ m![/\\]! ) {
94 0         0 $self->_open_serial_port(@_);
95             }
96             else {
97 0 0       0 if ( $self->{device} eq 'discover' ) {
98 0         0 my $devices = $self->discover;
99 0         0 my ( $ip, $port ) = @{ $devices->[0] };
  0         0  
100 0         0 $self->{port} = $port;
101 0         0 $self->{device} = $ip . ':' . $port;
102             }
103 0         0 $self->_open_tcp_port(@_);
104             }
105             }
106              
107             sub _open_tcp_port {
108 0     0   0 my $self = shift;
109 0         0 my $dev = $self->{device};
110 0         0 print STDERR "Opening $dev as tcp socket\n" if DEBUG;
111 0         0 require IO::Socket::INET;
112 0         0 import IO::Socket::INET;
113 0 0       0 if ( $dev =~ s/:(\d+)$// ) {
114 0         0 $self->{port} = $1;
115             }
116 0 0       0 my $fh = IO::Socket::INET->new( $dev . ':' . $self->port )
117             or croak "TCP connect to '$dev' failed: $!";
118 0         0 return $self->{filehandle} = $fh;
119             }
120              
121             sub _open_serial_port {
122 0     0   0 my $self = shift;
123 0         0 $self->{type} = 'ISCP';
124 0         0 my $fh = gensym();
125 0   0     0 my $s = tie( *$fh, 'Device::SerialPort', $self->{device} )
126             || croak "Could not tie serial port to file handle: $!\n";
127 0         0 $s->baudrate( $self->baud );
128 0         0 $s->databits(8);
129 0         0 $s->parity("none");
130 0         0 $s->stopbits(1);
131 0         0 $s->datatype("raw");
132 0         0 $s->write_settings();
133              
134 0 0       0 sysopen( $fh, $self->{device}, O_RDWR | O_NOCTTY | O_NDELAY )
135             or croak "open of '" . $self->{device} . "' failed: $!\n";
136 0         0 $fh->autoflush(1);
137 0         0 return $self->{filehandle} = $fh;
138             }
139              
140              
141             sub read {
142 3     3 1 521 my ( $self, $timeout ) = @_;
143 3         17 my $res = $self->read_one( \$self->{_buf} );
144 3 100       19 return $res if ( defined $res );
145 1         3 my $fh = $self->filehandle;
146 1         12 my $sel = IO::Select->new($fh);
147 1         149 READ_RESPONSE: do {
148 1         4 my $start = $self->_time_now;
149 1 50       6 $sel->can_read($timeout) or return;
150 1         93 my $bytes = sysread $fh, $self->{_buf}, 2048, length $self->{_buf};
151 1         4 $self->{_last_read} = $self->_time_now;
152 1 50       10 $timeout -= $self->{_last_read} - $start if ( defined $timeout );
153 1 0       4 croak defined $bytes ? 'closed' : 'error: ' . $! unless ($bytes);
    50          
154 1         4 $res = $self->read_one( \$self->{_buf} );
155 1 50 33     9 $self->_write_now()
156             if ( defined $res && !$self->{_awaiting_stick_response} );
157 1 50       24 return $res if ( defined $res );
158             } while (1);
159             }
160              
161              
162             sub read_one {
163 4     4 1 8 my ( $self, $rbuf, $no_write ) = @_;
164 4 100       16 return unless ($$rbuf);
165              
166 3         4 print STDERR "rbuf=", _hexdump($$rbuf), "\n" if PHY_DEBUG;
167              
168 3 50       30 return unless ( $$rbuf =~ s/\x05\x05\x03\x03(\w+)\r\n// );
169 3         68 my $body = $self->_process_response($1);
170              
171             # If we received an 'ack' then we need to try to read the next message
172 3 50       12 if ( $body eq 'ack' ) {
173 3 50       39 return unless ( $$rbuf =~ s/\x05\x05\x03\x03(\w+)\r\n// );
174 3         16 $body = $self->_process_response($1);
175             }
176              
177             $self->_write_now
178 3 50 33     41 unless ( $no_write || $self->{_awaiting_stick_response} );
179 3         11 return $body;
180              
181             }
182              
183              
184             sub write {
185 3     3 1 8 my ( $self, $cmd, $cb ) = @_;
186 3         5 print STDERR "Queuing: $cmd\n" if XPL_DEBUG;
187 3         90 my $packet = "\05\05\03\03" . $cmd . $self->_plugwise_crc($cmd) . "\r\n";
188 3         21247 push @{ $self->{_q} }, [ $packet, $cmd, $cb ];
  3         22  
189 3 100       28 $self->_write_now unless ( $self->{_waiting} );
190 3         13 1;
191             }
192              
193              
194             sub queue_size {
195 1     1 1 5 my ($self) = @_;
196 1         2 return scalar @{ $self->{_q} };
  1         8  
197             }
198              
199             sub _write_now {
200 6     6   15 my $self = shift;
201 6         14 my $rec = shift @{ $self->{_q} };
  6         20  
202 6         21 my $wait_rec = delete $self->{_waiting};
203 6 100 66     38 if ( $wait_rec && $wait_rec->[1] ) {
204 3         6 my ( $str, $cmd, $cb ) = @{ $wait_rec->[1] };
  3         9  
205 3 50       8 $cb->() if ($cb);
206             }
207 6 100       22 return unless ( defined $rec );
208 3         14 $self->_real_write(@$rec);
209 3         15 $self->{_waiting} = [ $self->_time_now, $rec ];
210             }
211              
212             sub _real_write {
213 3     3   42 my ( $self, $str, $desc, $cb ) = @_;
214 3         4 print STDERR "Sending: $desc\n" if XPL_DEBUG;
215 3         5 print STDERR _hexdump($str), "\n" if PHY_DEBUG;
216 3         38 syswrite $self->filehandle, $str, length $str;
217 3         14 $self->{_awaiting_stick_response} = 1;
218             }
219              
220             sub _stick_init {
221              
222 1     1   4 my $self = shift();
223 1         5 $self->write("000A");
224              
225 1         36 return 1;
226             }
227              
228             #This is a helper function that returns the CRC for communication with the USB stick.
229             sub _plugwise_crc {
230 9     9   23 my ( $self, $data ) = @_;
231 9         58 sprintf( "%04X", crc( $data, 16, 0, 0, 0, 0x1021, 0, 0 ) );
232             }
233              
234             # This function processes a response received from the USB stick.
235             #
236             # In a first step, the ACK response from the stick is handled. This means that the
237             # communication sequence number is captured, and a new entry is made in the response queue.
238             #
239             # Second step, if we receive an error response from the stick, pass this message back
240             #
241             # Finally, of course, decode actual useful messages and return their value to the caller
242             #
243             # The input to this function is the message with CRC, with the header and trailing part removed
244             sub _process_response {
245 6     6   20 my ( $self, $frame ) = @_;
246              
247 6         11 print STDERR "Processing '$frame'\n" if XPL_DEBUG;
248              
249             # The default message is a plugwise.basic,
250             # can be overwritten when required.
251 6         19 my %xplmsg = ( schema => 'plugwise.basic', );
252              
253             # Check if the CRC matches
254 6 50       60 if (!( $self->_plugwise_crc( substr( $frame, 0, -4 ) ) eq
255             substr( $frame, -4, 4 )
256             )
257             )
258             {
259             # Send out notification...
260             #$xpl->ouch("PLUGWISE: received a frame with an invalid CRC");
261 0         0 $xplmsg{schema} = 'log.basic';
262 0         0 $xplmsg{body} = [
263             'type' => 'err',
264             'text' => "Received frame with invalid CRC",
265             'code' => $frame
266             ];
267 0         0 return \%xplmsg;
268             }
269              
270             # Strip CRC, we already know it is correct
271 6         51803 $frame =~ s/(.{4}$)//;
272              
273             # After a command is sent to the stick, we first receive an 'ACK'. This 'ACK' contains a sequence number that we want to track and that notifies us of errors.
274 6 100       46 if ( $frame =~ /^0000([[:xdigit:]]{4})([[:xdigit:]]{4})$/ ) {
275              
276             # ack | seq. nr. || response code |
277              
278 3         13 my $seqnr = $1;
279              
280 3 50       17 if ( $2 eq "00C1" ) {
    0          
281 3         27 $self->{_response_queue}->{ hex($1) }->{received_ok} = 1;
282 3         16 $self->{_response_queue}->{ hex($1) }->{type}
283             = $self->{_response_queue}->{last_type};
284              
285 3         19 return "ack";
286             }
287             elsif ( $2 eq "00C2" ) {
288              
289             # We sometimes get this reponse on the initial init
290             # request, re-init in this case
291 0         0 $self->write("000A");
292 0         0 return "re-init";
293             }
294             else {
295 0         0 carp("Received response code with error: $frame\n");
296 0         0 $xplmsg{schema} = 'log.basic';
297              
298             # Default error message
299 0         0 my $text = 'Received error response';
300 0         0 my $error = $2;
301              
302             # Catch known errors for more user friendly feedback,
303             # we overwrite the default text in this case
304 0         0 my $msg_causing_error = $self->{_waiting}[1][1];
305              
306 0 0       0 if ( $msg_causing_error =~ /^0026([[:xdigit:]]{16}$)/ ) {
307 0         0 my $device = $self->_addr_l2s($1);
308 0         0 $text = "No calibration response received for $device";
309              
310             # If we don't get a calibration response when we ask for it, we remove the Circle from the
311             # known Circles so it does not get reported when we request the list of Circles.
312             # This can be caused when a device is removed from the network. The Circle+ remembers
313             # the ID of the Circle that was removed, but of course the device will not respond to
314             # calibration requests.
315 0         0 delete $self->{_plugwise}->{circles}->{$device};
316             }
317             $xplmsg{body} = [
318 0         0 'type' => 'err',
319             'text' => $text,
320             'code' => $self->{_waiting}[1][1] . ":" . $error
321             ];
322 0         0 delete $self->{_response_queue}->{ hex($seqnr) };
323 0         0 $self->{_awaiting_stick_response} = 0;
324              
325 0         0 return \%xplmsg;
326              
327             }
328             }
329              
330 3         15 $self->{_awaiting_stick_response} = 0;
331              
332 3 100       15 if ( $frame
333             =~ /^0011([[:xdigit:]]{4})([[:xdigit:]]{16})([[:xdigit:]]{4})([[:xdigit:]]{16})([[:xdigit:]]{4})/
334             )
335              
336             # init resp | seq. nr.|| stick MAC addr || don't care || network key || short key
337             {
338             # Extract info
339 1         7 $self->{_plugwise}->{stick_MAC} = substr( $2, -6, 6 );
340 1         6 $self->{_plugwise}->{network_key} = $4;
341 1         5 $self->{_plugwise}->{short_key} = $5;
342 1         5 $self->{_plugwise}->{connected} = 1;
343              
344             # Update the response_queue, remove the entry corresponding to this reply
345 1         28 delete $self->{_response_queue}->{ hex($1) };
346              
347 1         2 print STDERR
348             "PLUGWISE: Received a valid response to the init request from the Stick. Connected!\n"
349             if DEBUG;
350 1         8 return "connected";
351             }
352              
353 2 100       16 if ( $frame =~ /^0000([[:xdigit:]]{4})00DE([[:xdigit:]]{16})$/ ) {
354              
355             # circle off resp | seq. nr. | | circle MAC
356 1         8 my $saddr = $self->_addr_l2s($2);
357              
358 1         6 $xplmsg{body}
359             = [ 'device' => $saddr, 'type' => 'output', 'onoff' => 'off' ];
360              
361             # Update the response_queue, remove the entry corresponding to this reply
362 1         8 delete $self->{_response_queue}->{ hex($1) };
363              
364 1         1 print STDERR "PLUGWISE: Stick reported Circle "
365             . $saddr
366             . " is OFF\n"
367             if DEBUG;
368 1         4 return \%xplmsg;
369             }
370              
371 1 50       7 if ( $frame =~ /^0000([[:xdigit:]]{4})00D8([[:xdigit:]]{16})$/ ) {
372              
373             # circle on resp | seq. nr. | | circle MAC
374 1         7 my $saddr = $self->_addr_l2s($2);
375              
376 1         7 $xplmsg{body}
377             = [ 'device' => $saddr, 'type' => 'output', 'onoff' => 'on' ];
378              
379             # Update the response_queue, remove the entry corresponding to this reply
380 1         10 delete $self->{_response_queue}->{ hex($1) };
381 1         2 print STDERR "PLUGWISE: Stick reported Circle " . $saddr . " is ON\n"
382             if DEBUG;
383 1         6 return \%xplmsg;
384             }
385              
386             # Process the response on a powerinfo request
387             # powerinfo resp | seq. nr. || Circle MAC || pulse1 || pulse8 | other stuff we don't care about
388 0 0       0 if ( $frame
389             =~ /^0013([[:xdigit:]]{4})([[:xdigit:]]{16})([[:xdigit:]]{4})([[:xdigit:]]{4})/
390             )
391             {
392 0         0 my $saddr = $self->_addr_l2s($2);
393 0         0 my $pulse1 = $3;
394 0         0 my $pulse8 = $4;
395              
396             # Assign the values to the data hash
397 0         0 $self->{_plugwise}->{circles}->{$saddr}->{pulse1} = $pulse1;
398 0         0 $self->{_plugwise}->{circles}->{$saddr}->{pulse8} = $pulse8;
399              
400             # Ensure we have the calibration info before we try to calc the power,
401             # if we don't have it, return an error reponse
402 0 0       0 if ( !defined $self->{_plugwise}->{circles}->{$saddr}->{gainA} ) {
403              
404             #$xpl->ouch("Cannot report the power, calibration data not received yet for $saddr\n");
405 0         0 $xplmsg{schema} = 'log.basic';
406 0         0 $xplmsg{body} = [
407             'type' => 'err',
408             'text' =>
409             "Report power failed, calibration data not retrieved yet",
410             'device' => $saddr
411             ];
412 0         0 delete $self->{_response_queue}->{ hex($1) };
413              
414 0         0 return \%xplmsg;
415             }
416              
417             # Calculate the live power
418 0         0 my ( $pow1, $pow8 ) = $self->_calc_live_power($saddr);
419              
420             # Update the response_queue, remove the entry corresponding to this reply
421 0         0 delete $self->{_response_queue}->{ hex($1) };
422              
423             # Create the corresponding xPL message
424 0         0 $xplmsg{body} = [
425             'device' => $saddr,
426             'type' => 'power',
427             'current' => $pow1 / 1000,
428             'current8' => $pow8 / 1000,
429             'units' => 'kW'
430             ];
431              
432 0         0 print STDERR "PLUGWISE: Circle "
433             . $saddr
434             . " live power 1/8 is: $pow1/$pow8 W\n"
435             if DEBUG;
436 0         0 return \%xplmsg;
437             }
438              
439             # Process the response on a query known circles command
440             # circle query resp| seq. nr. || Circle+ MAC || Circle MAC on || memory position
441 0 0       0 if ( $frame
442             =~ /^0019([[:xdigit:]]{4})([[:xdigit:]]{16})([[:xdigit:]]{16})([[:xdigit:]]{2})$/
443             )
444             {
445             # Store the node in the object
446 0 0       0 if ( $3 ne "FFFFFFFFFFFFFFFF" ) {
447 0         0 $self->{_plugwise}->{circles}->{ substr( $3, -6, 6 ) } = {
448             }; # Store the last 6 digits of the MAC address for later use
449             # And immediately queue a request for calibration info
450 0         0 $self->write( "0026" . $3 );
451             }
452              
453             # Update the response_queue, remove the entry corresponding to this reply
454 0         0 delete $self->{_response_queue}->{ hex($1) };
455              
456             # Only when we have walked the complete list
457 0         0 return "no_data";
458             }
459              
460             # Process the response on a status request
461             # status response | seq. nr. || Circle+ MAC || year,mon, min || curr_log_addr || powerstate
462 0 0       0 if ( $frame
463             =~ /^0024([[:xdigit:]]{4})([[:xdigit:]]{16})([[:xdigit:]]{8})([[:xdigit:]]{8})([[:xdigit:]]{2})/
464             )
465             {
466 0         0 my $saddr = $self->_addr_l2s($2);
467 0 0       0 my $onoff = $5 eq '00' ? 'off' : 'on';
468 0 0       0 my $current = $5 eq '00' ? 'LOW' : 'HIGH';
469 0         0 $self->{_plugwise}->{circles}->{$saddr}->{onoff} = $onoff;
470 0         0 $self->{_plugwise}->{circles}->{$saddr}->{curr_logaddr}
471             = ( hex($4) - 278528 ) / 8;
472              
473 0         0 my $circle_date_time = $self->_tstamp2time($3);
474              
475 0         0 print STDERR
476             "PLUGWISE: Received status response for circle $saddr: ($onoff, logaddr="
477             . $self->{_plugwise}->{circles}->{$saddr}->{curr_logaddr}
478             . ", datetime=$circle_date_time)\n"
479             if DEBUG;
480              
481 0         0 $xplmsg{body} = [
482             'device' => $saddr,
483             'type' => 'output',
484             'onoff' => $onoff,
485             'address' =>
486             $self->{_plugwise}->{circles}->{$saddr}->{curr_logaddr},
487             'datetime' => $circle_date_time
488             ];
489              
490             # Update the response_queue, remove the entry corresponding to this reply
491 0         0 delete $self->{_response_queue}->{ hex($1) };
492              
493 0         0 return \%xplmsg;
494             }
495              
496             # Process the response on a calibration request
497 0 0       0 if ( $frame
498             =~ /^0027([[:xdigit:]]{4})([[:xdigit:]]{16})([[:xdigit:]]{8})([[:xdigit:]]{8})([[:xdigit:]]{8})([[:xdigit:]]{8})$/
499             )
500             {
501             # calibration resp | seq. nr. || Circle+ MAC || gainA || gainB || offtot || offruis
502             #print "Received for $2 calibration response!\n";
503 0         0 my $saddr = $self->_addr_l2s($2);
504              
505             #print "Short address = $saddr\n";
506 0         0 print STDERR
507             "PLUGWISE: Received calibration reponse for circle $saddr\n"
508             if DEBUG;
509              
510 0         0 $self->{_plugwise}->{circles}->{$saddr}->{gainA}
511             = $self->_hex2float($3);
512 0         0 $self->{_plugwise}->{circles}->{$saddr}->{gainB}
513             = $self->_hex2float($4);
514 0         0 $self->{_plugwise}->{circles}->{$saddr}->{offtot}
515             = $self->_hex2float($5);
516 0         0 $self->{_plugwise}->{circles}->{$saddr}->{offruis}
517             = $self->_hex2float($6);
518              
519             # Update the response_queue, remove the entry corresponding to this reply
520 0         0 delete $self->{_response_queue}->{ hex($1) };
521              
522 0         0 return "no_data";
523             }
524              
525             # Process the response on a historic buffer readout
526 0 0       0 if ( $frame
527             =~ /^0049([[:xdigit:]]{4})([[:xdigit:]]{16})([[:xdigit:]]{16})([[:xdigit:]]{16})([[:xdigit:]]{16})([[:xdigit:]]{16})([[:xdigit:]]{8})$/
528             )
529             {
530             # history resp | seq. nr. || Circle+ MAC || info 1 || info 2 || info 3 || info 4 || address
531 0         0 my $s_id = $self->_addr_l2s($2);
532 0         0 my $log_addr = ( hex($7) - 278528 ) / 8;
533              
534             #print "Received history response for $2 and address $log_addr!\n";
535              
536             # Assign the values to the data hash
537 0         0 $self->{_plugwise}->{circles}->{$s_id}->{history}->{logaddress}
538             = $log_addr;
539 0         0 $self->{_plugwise}->{circles}->{$s_id}->{history}->{info1} = $3;
540 0         0 $self->{_plugwise}->{circles}->{$s_id}->{history}->{info2} = $4;
541 0         0 $self->{_plugwise}->{circles}->{$s_id}->{history}->{info3} = $5;
542 0         0 $self->{_plugwise}->{circles}->{$s_id}->{history}->{info4} = $6;
543              
544             # Ensure we have the calibration info before we try to calc the power,
545             # if we don't have it, return an error reponse
546 0 0       0 if ( !defined $self->{_plugwise}->{circles}->{$s_id}->{gainA} ) {
547              
548             #$xpl->ouch("Cannot report the power, calibration data not received yet for $s_id\n");
549 0         0 $xplmsg{schema} = 'log.basic';
550 0         0 $xplmsg{body} = [
551             'type' => 'err',
552             'text' =>
553             "Report power failed, calibration data not retrieved yet",
554             'device' => $s_id
555             ];
556 0         0 delete $self->{_response_queue}->{ hex($1) };
557              
558 0         0 return \%xplmsg;
559             }
560 0         0 my ( $tstamp, $energy ) = $self->_report_history($s_id);
561              
562             # If the timestamp is no good, we tried to retrieve a field that contains no valid data, generate an error response
563 0 0       0 if ( $tstamp eq "000000000000" ) {
564              
565             #$xpl->ouch("Cannot report the power for interval $log_addr of circle $s_id, it is in the future\n");
566 0         0 $xplmsg{schema} = 'log.basic';
567 0         0 $xplmsg{body} = [
568             'type' => 'err',
569             'text' =>
570             "Report power failed, no valid data in time interval",
571             'device' => $s_id
572             ];
573 0         0 delete $self->{_response_queue}->{ hex($1) };
574 0         0 return \%xplmsg;
575             }
576              
577             $xplmsg{body} = [
578 0         0 'device' => $s_id,
579             'type' => 'energy',
580             'current' => $energy,
581             'units' => 'kWh',
582             'datetime' => $tstamp
583             ];
584              
585 0         0 print STDERR "PLUGWISE: Historic energy for $s_id"
586             . "[$log_addr] is $energy kWh on $tstamp\n"
587             if DEBUG;
588              
589             # Update the response_queue, remove the entry corresponding to this reply
590 0         0 delete $self->{_response_queue}->{ hex($1) };
591              
592 0         0 return \%xplmsg;
593             }
594              
595             # We should not get here unless we receive responses that are not implemented...
596             #$xpl->ouch("Received unknown response: '$frame'");
597 0         0 return "no_data";
598              
599             }
600              
601              
602             sub status {
603 1     1 1 580 my ($self) = @_;
604 1         6 return $self->{_plugwise};
605             }
606              
607              
608             sub command {
609 2     2 1 781 my ( $self, $command, $target, $parameter ) = @_;
610              
611 2 50 33     14 if ( !defined($command) || !defined($target) ) {
612 0         0 carp(
613             "A command to the stick needs a command and a target ID as parameter"
614             );
615 0         0 return 0;
616             }
617              
618 2         4 if (DEBUG) {
619             print STDERR "Push to queue command '$command'";
620             print STDERR "to '$target'" if ( defined $target );
621             print STDERR "\n";
622             }
623              
624 2         5 my $packet = "";
625              
626 2 50       6 if ( defined $target ) {
627              
628             # Commands that target a specific device might need to be sent multiple times
629             # if multiple devices are defined
630 2         12 foreach my $circle ( split /,/, $target ) {
631 2         5 $circle = uc($circle);
632              
633 2 100       12 if ( $command eq 'on' ) {
    50          
    0          
    0          
    0          
634 1         5 $packet = "0017" . $self->_addr_s2l($circle) . "01";
635             }
636             elsif ( $command eq 'off' ) {
637 1         7 $packet = "0017" . $self->_addr_s2l($circle) . "00";
638             }
639             elsif ( $command eq 'status' ) {
640 0         0 $packet = "0023" . $self->_addr_s2l($circle);
641             }
642             elsif ( $command eq 'livepower' ) {
643              
644             # Ensure we have the calibration readings before we send the read command
645             # because the processing of the response of the read command required the
646             # calibration readings output to calculate the actual power
647 0 0       0 if (!defined(
648             $self->{_plugwise}->{circles}->{$circle}->{offruis}
649             )
650             )
651             {
652 0         0 my $longaddr = $self->_addr_s2l($circle);
653 0         0 $self->write( "0026" . $longaddr )
654             ; #, "Request calibration info");
655             }
656 0         0 $packet = "0012" . $self->_addr_s2l($circle);
657              
658             }
659             elsif ( $command eq 'history' ) {
660              
661             # Ensure we have the calibration readings before we send the read command
662             # because the processing of the response of the read command required the
663             # calibration readings output to calculate the actual power
664 0 0       0 if (!defined(
665             $self->{_plugwise}->{circles}->{$circle}->{offruis}
666             )
667             )
668             {
669 0         0 my $longaddr = $self->_addr_s2l($circle);
670 0         0 $self->write( "0026" . $longaddr )
671             ; #, "Request calibration info");
672             }
673              
674 0 0       0 if ( !defined $parameter ) {
675 0         0 carp(
676             "The 'history' command needs both a Circle ID and an address to read..."
677             );
678 0         0 return 0;
679             }
680              
681 0         0 my $address = $parameter * 8 + 278528;
682 0         0 $packet
683             = "0048"
684             . $self->_addr_s2l($circle)
685             . sprintf( "%08X", $address );
686             }
687             else {
688 0         0 croak("Received invalid command '$command'\n");
689 0         0 return 0;
690             }
691              
692             # Send the packet to the stick!
693 2 50       12 $self->write($packet) if ( defined $packet );
694              
695             }
696             }
697              
698 2         23 return 1;
699             }
700              
701             # Interrogate the network coordinator (Circle+) for all connected Circles
702             # This sub will generate the requests, and then the response parser function
703             # will generate a hash with all known circles
704             # When a circle is detected, a calibration request is sent to ge the relevant info
705             # required to calculate the power information.
706             # Circle info goes into a global hash like this:
707             # $object->{_plugwise}->{circles}
708             # A single circle entry contains the short id and the following info:
709             # short_id => { gainA => xxx,
710             # gainB => xxx,
711             # offtot => xxx,
712             # offruis => xxx }
713             sub _query_connected_circles {
714              
715 0     0   0 my ($self) = @_;
716              
717             # In this code we will scan all connected circles to be able to add them to the $self->{_plugwise}->{circles} hash
718 0         0 my $index = 0;
719              
720             # Interrogate the Circle+ and add its info into the circles hash
721 0         0 $self->{_plugwise}->{coordinator_MAC}
722             = $self->_addr_l2s( $self->{_plugwise}->{network_key} );
723 0         0 $self->{_plugwise}->{circles} = {}; # Reset known circles hash
724 0         0 $self->{_plugwise}->{circles}->{ $self->{_plugwise}->{coordinator_MAC} }
725             = {}; # Add entry for Circle+
726 0         0 $self->write(
727             "0026" . $self->_addr_s2l( $self->{_plugwise}->{coordinator_MAC} ) );
728              
729             # Interrogate the first x connected devices
730 0         0 while ( $index < $self->{list_circles_count} ) {
731 0         0 my $strindex = sprintf( "%02X", $index++ );
732 0         0 my $packet
733             = "0018"
734             . $self->_addr_s2l( $self->{_plugwise}->{coordinator_MAC} )
735             . $strindex;
736 0         0 $self->write($packet); #, "Query connected device $strindex");
737             }
738              
739 0         0 return;
740             }
741              
742             # Convert the long Circle address notation to short
743             sub _addr_l2s {
744 2     2   12 my ( $self, $address ) = @_;
745 2         5 my $saddr = substr( $address, -8, 8 );
746              
747             # We will return at least 6 bytes, more if required
748             # This is to keep compatibility with existing code that only supports 6 byte short addresses
749 2         14 return sprintf( "%06X", hex($saddr) );
750             }
751              
752             # Convert the short Circle address notation to long
753             sub _addr_s2l {
754 2     2   4 my ( $self, $address ) = @_;
755 2         16 return "000D6F00" . sprintf( "%08X", hex($address) );
756             }
757              
758             # Convert hex values to float for power readout
759             sub _hex2float {
760 0     0   0 my ( $self, $hexstr ) = @_;
761 0         0 my $floater = unpack( 'f', reverse pack( 'H*', $hexstr ) );
762 0         0 return $floater;
763             }
764              
765             # Return the time
766             sub _time_now {
767 5     5   30 Time::HiRes::time;
768             }
769              
770             # Print the data in hex
771             sub _hexdump {
772 0     0     my $s = shift;
773 0           my $r = unpack 'H*', $s;
774 0           $s =~ s/[^ -~]/./g;
775 0           $r . ' ' . $s;
776             }
777              
778             sub _report_history {
779 0     0     my ( $self, $id ) = @_;
780              
781             # Get the first data entry
782 0           my $data = $self->{_plugwise}->{circles}->{$id}->{history}->{info1};
783              
784 0           my $energy = 0;
785 0           my $tstamp = 0;
786              
787 0 0         if ( $data =~ /^([[:xdigit:]]{8})([[:xdigit:]]{8})$/ ) {
788              
789             # Calculate Wh
790 0           my $corrected_pulses = $self->_pulsecorrection( $id, hex($2) );
791 0           $energy = $corrected_pulses / 3600 / 468.9385193 * 1000;
792 0           $tstamp = $self->_tstamp2time($1);
793              
794             # Round to 1 Wh
795 0           $energy = round($energy);
796              
797             # Report kWh
798 0           $energy = $energy / 1000;
799              
800             #print "info1 date: $tstamp, energy $energy kWh\n";
801             }
802              
803 0           return ( $tstamp, $energy );
804              
805             }
806              
807             # Convert a Plugwise timestamp to a human-readable format
808             sub _tstamp2time {
809 0     0     my ( $self, $tstamp ) = @_;
810              
811             # Return empty time on empty timestamp
812 0 0         return "000000000000" if ( $tstamp eq "FFFFFFFF" );
813              
814             # Convert
815 0 0         if ( $tstamp =~ /([[:xdigit:]]{2})([[:xdigit:]]{2})([[:xdigit:]]{4})/ ) {
816 0           my $circle_date = sprintf( "%04i%02i%02i",
817             2000 + hex($1),
818             hex($2), int( hex($3) / 60 / 24 ) + 1 );
819 0           my $circle_time = hex($3) % ( 60 * 24 );
820 0           my $circle_hours = int( $circle_time / 60 );
821 0           my $circle_minutes = $circle_time % 60;
822 0           $circle_time = sprintf( "%02i%02i", $circle_hours, $circle_minutes );
823 0           return $circle_date . $circle_time;
824             }
825             else {
826 0           return "000000000000";
827             }
828             }
829              
830             # Calculate the live power consumption from the last report.
831             sub _calc_live_power {
832 0     0     my ( $self, $id ) = @_;
833              
834             #my ($pulse1, $pulse8) = $self->pulsecorrection($id);
835 0           my $pulse1 = $self->_pulsecorrection( $id,
836             hex( $self->{_plugwise}->{circles}->{$id}->{pulse1} ) );
837 0           my $pulse8 = $self->_pulsecorrection( $id,
838             hex( $self->{_plugwise}->{circles}->{$id}->{pulse8} ) / 8 );
839              
840 0           my $live1 = $pulse1 * 1000 / 468.9385193;
841 0           my $live8 = $pulse8 * 1000 / 468.9385193;
842              
843             # Round
844 0           $live1 = round($live1);
845 0           $live8 = round($live8);
846              
847 0           return ( $live1, $live8 );
848              
849             }
850              
851             # Correct the reported number of pulses based on the calibration values
852             sub _pulsecorrection {
853 0     0     my ( $self, $id, $pulses ) = @_;
854              
855             # Get the calibration values for the circle
856 0           my $offnoise = $self->{_plugwise}->{circles}->{$id}->{offruis};
857 0           my $offtot = $self->{_plugwise}->{circles}->{$id}->{offtot};
858 0           my $gainA = $self->{_plugwise}->{circles}->{$id}->{gainA};
859 0           my $gainB = $self->{_plugwise}->{circles}->{$id}->{gainB};
860              
861             # Correct the pulses with the calibration data
862 0           my $out
863             = ( ( $pulses + $offnoise ) ^ 2 ) * $gainB
864             + ( ( $pulses + $offnoise ) * $gainA )
865             + $offtot;
866              
867             # Never report negative values, can happen with really small values
868 0 0         $out = 0 if ( $out < 0 );
869              
870 0           return $out;
871              
872             }
873              
874              
875             1;
876              
877             __END__