File Coverage

blib/lib/Device/CurrentCost/Message.pm
Criterion Covered Total %
statement 115 115 100.0
branch 47 48 97.9
condition 2 2 100.0
subroutine 30 30 100.0
pod 21 21 100.0
total 215 216 99.5


line stmt bran cond sub pod time code
1 5     5   28 use strict;
  5         9  
  5         171  
2 5     5   24 use warnings;
  5         10  
  5         258  
3             package Device::CurrentCost::Message;
4             $Device::CurrentCost::Message::VERSION = '1.142240';
5             # ABSTRACT: Perl modules for Current Cost energy monitor messages
6              
7              
8 5     5   28 use constant DEBUG => $ENV{DEVICE_CURRENT_COST_DEBUG};
  5         7  
  5         303  
9              
10 5     5   26 use Carp qw/croak carp/;
  5         10  
  5         282  
11 5     5   28 use Device::CurrentCost::Constants;
  5         10  
  5         31  
12 5     5   28 use List::Util qw/min/;
  5         11  
  5         9246  
13              
14              
15             sub new {
16 18     18 1 119 my ($pkg, %p) = @_;
17 18 100       229 croak $pkg.'->new: message parameter is required' unless (exists $p{message});
18 17         90 my $self = bless { %p }, $pkg;
19 17         73 $self;
20             }
21              
22              
23             sub device_type {
24 5     5 1 9 my $self = shift;
25 5 100       18 return $self->{device_type} if (exists $self->{device_type});
26 4 100       10 $self->{device_type} =
27             $self->message =~ m!! ? CURRENT_COST_CLASSIC : CURRENT_COST_ENVY;
28             }
29              
30              
31             sub device {
32 19     19 1 28 my $self = shift;
33 19 100       81 return $self->{device}->[0] if (exists $self->{device});
34 12         30 $self->_find_device->[0]
35             }
36              
37              
38             sub device_version {
39 19     19 1 30 my $self = shift;
40 19 100       87 return $self->{device}->[1] if (exists $self->{device});
41 3         8 $self->_find_device->[1]
42             }
43              
44             sub _find_device {
45 15     15   20 my $self = shift;
46 15         35 my $name = $self->_parse_field('name');
47 15 100       55 $self->{device} =
48             defined $name ?
49             [ $name, 'v'.$self->_parse_field('sver')] :
50             [ split /-/, $self->_parse_field('src'), 2 ];
51             }
52              
53              
54 187     187 1 2378 sub message { shift->{message} }
55              
56             sub _parse_field {
57 191     191   281 my ($self, $field, $default) = @_;
58 191 100       728 return $self->{$field} if (exists $self->{$field});
59 92 100       184 if ($self->message =~ m!<$field>(.*?)!s) {
    100          
60 58         120 my $v = $1;
61 58 100       560 $self->{$field} =
62             ($v =~ m!<([^>]+)>(.*?)!s) ? { value => $2, units => $1 } : $v;
63             } elsif (defined $default) {
64 17         106 return $default;
65             } else {
66             return
67 17         48 }
68             }
69              
70              
71 5     5 1 11 sub dsb { shift->_parse_field('dsb') }
72              
73              
74              
75 5     5 1 11 sub days_since_boot { shift->dsb }
76              
77              
78             sub time {
79 4     4 1 21 my $self = shift;
80 4         8 my $time = $self->_parse_field('time');
81 4 100       18 return $time if (defined $time);
82 2         7 $self->{time} =
83             $self->_parse_field('hr').':'.
84             $self->_parse_field('min').':'.
85             $self->_parse_field('sec')
86             }
87              
88              
89             sub time_in_seconds {
90 5     5 1 8 my $self = shift;
91 5 100       19 return $self->{time_in_seconds} if (exists $self->{time_in_seconds});
92 4         21 my ($h, $m, $s) = split /:/, $self->time, 3;
93 4         30 $self->{time_in_seconds} = $h*3600 + $m*60 + $s;
94             }
95              
96              
97             sub boot_time {
98 5     5 1 11 my $self = shift;
99 5         15 $self->days_since_boot * 86400 + $self->time_in_seconds
100             }
101              
102              
103 9     9 1 24 sub sensor { shift->_parse_field('sensor', 0) }
104              
105              
106 8     8 1 19 sub id { shift->_parse_field('id') }
107              
108              
109 5     5 1 12 sub type { shift->_parse_field('type') }
110              
111              
112 3     3 1 9 sub tmpr { shift->_parse_field('tmpr') }
113              
114              
115 3     3 1 10 sub temperature { shift->tmpr }
116              
117              
118 59     59 1 6485 sub has_history { shift->message =~ // }
119              
120              
121 21     21 1 42 sub has_readings { shift->message =~ // }
122              
123              
124             sub units {
125 76     76 1 96 my $self = shift;
126 76 100       132 my $ch1 = $self->_parse_field('ch1') or return;
127 74         238 $ch1->{units}
128             }
129              
130              
131             sub value {
132 56     56 1 105 my ($self, $channel) = @_;
133 56 100       101 $self->units || return; # return if no units can be found - historic only
134 55 100       120 if ($channel) {
135 45         177 return $self->_parse_field('ch'.$channel, { value => undef })->{value};
136             }
137 10 100       41 return $self->{total} if (exists $self->{total});
138 7         25 foreach (1 .. 3) {
139 21   100     81 $self->{total} += $self->value($_)||0;
140             }
141 7         72 $self->{total}
142             }
143              
144              
145             sub summary {
146 15     15 1 3266 my ($self, $prefix) = @_;
147 15 100       65 $prefix = '' unless (defined $prefix);
148 15         59 my $str = $prefix.'Device: '.$self->device.' '.$self->device_version."\n";
149 15         54 $prefix .= ' ';
150 15 100       39 if ($self->has_readings) {
151 5         16 $str .= $prefix.'Sensor: '.$self->sensor;
152 5         18 $str .= (' ['.$self->id.','.$self->type."]\n".
153             $prefix.'Total: '.$self->value.' '.$self->units."\n");
154 5         15 foreach my $phase (1..3) {
155 15         33 my $v = $self->value($phase);
156 15 100       44 next unless (defined $v);
157 11         39 $str .= $prefix.'Phase '.$phase.': '.($v+0)." ".$self->units."\n";
158             }
159             }
160 15 100       39 if ($self->has_history) {
161 11         26 $str .= $prefix."History\n";
162 11         23 my $hist = $self->history;
163 11         72 foreach my $sensor (sort keys %$hist) {
164 101         137 $str .= $prefix.' Sensor '.$sensor."\n";
165 101         88 foreach my $span (sort keys %{$hist->{$sensor}}) {
  101         215  
166 104         95 foreach my $age (sort { $a <=> $b } keys %{$hist->{$sensor}->{$span}}) {
  544         626  
  104         281  
167 400         1167 $str .= $prefix.' -'.$age.' '.$span.': '.
168             (0+$hist->{$sensor}->{$span}->{$age})."\n";
169             }
170             }
171             }
172             }
173             $str
174 15         120 }
175              
176              
177             sub history {
178 24     24 1 35 my $self = shift;
179 24 100       112 return $self->{history} if (exists $self->{history});
180 12         20 my %hist = ();
181 12         23 $self->{history} = \%hist;
182 12 100       23 return $self->{history} unless ($self->has_history);
183 11         29 my $xml = $self->message;
184 11 100       49 if ($xml =~ //) {
185             # envy
186 10         103 foreach my $data (split qr!!, $xml) {
187 100 50       502 my ($sensor) = ($data =~ /(\d+)
188 100         149 my %rec = ();
189 100         163 $hist{$sensor} = _parse_history($data);
190             }
191             } else {
192             # classic
193 1         3 $hist{$self->sensor} = _parse_history($xml);
194             }
195 11         59 \%hist;
196             }
197              
198             sub _parse_history {
199 101     101   122 my $string = shift;
200 101         115 my %rec = ();
201 101         133 foreach my $span (qw/hours days months years/) {
202 404         554 my $first = substr $span, 0, 1;
203 404         7275 while ($string =~ m!<$first(\d+)>([^<]+)!mg) {
204 400         2784 $rec{$span}->{0+$1} = 0+$2;
205             }
206             }
207 101         393 \%rec;
208             }
209              
210             1;
211              
212             =pod
213              
214             =encoding UTF-8
215              
216             =head1 NAME
217              
218             Device::CurrentCost::Message - Perl modules for Current Cost energy monitor messages
219              
220             =head1 VERSION
221              
222             version 1.142240
223              
224             =head1 SYNOPSIS
225              
226             use Device::CurrentCost::Message;
227             my $msg = Device::CurrentCost::Message->new(message => '...');
228             print 'Device: ', $msg->device, ' ', $msg->device_version, "\n";
229             if ($msg->has_readings) {
230             print 'Sensor: ', $msg->sensor, '.', $msg->id, ' (', $msg->type, ")\n";
231             print 'Total: ', $msg->value, ' ', $msg->units, "\n";
232             foreach my $phase (1..3) {
233             print 'Phase ', $phase, ': ',
234             $msg->value($phase)+0, " ", $msg->units, "\n";
235             }
236             }
237              
238             use Data::Dumper;
239             print Data::Dumper->Dump([$msg->history]) if ($msg->has_history);
240              
241             # or
242             print $msg->summary, "\n";
243              
244             =head1 DESCRIPTION
245              
246             =head1 METHODS
247              
248             =head2 C
249              
250             This constructor returns a new Current Cost message object.
251             The supported parameters are:
252              
253             =over
254              
255             =item message
256              
257             The message data. Usually a string like 'C<< ... >>'.
258             This parameter is required.
259              
260             =back
261              
262             =head2 C
263              
264             Returns the type of the device that created the message.
265              
266             =head2 C
267              
268             Returns the name of the device that created the message.
269              
270             =head2 C
271              
272             Returns the version of the device that created the message.
273              
274             =head2 C
275              
276             Returns the raw data of the message.
277              
278             =head2 C
279              
280             Returns the days since boot field of the message.
281              
282             =head2 C
283              
284             Returns the days since boot field of the message.
285              
286             =head2 C
287              
288             Returns the time field of the message in C format.
289              
290             =head2 C
291              
292             Returns the time field of the message in seconds.
293              
294             =head2 C
295              
296             Returns the time since boot reported by the message in seconds.
297              
298             =head2 C
299              
300             Returns the sensor number field of the message. A classic monitor
301             supports only one sensor so 0 is returned.
302              
303             =head2 C
304              
305             Returns the id field of the message.
306              
307             =head2 C
308              
309             Returns the sensor type field of the message.
310              
311             =head2 C
312              
313             Returns the tmpr/temperature field of the message.
314              
315             =head2 C
316              
317             Returns the temperature field of the message.
318              
319             =head2 C
320              
321             Returns true if the message contains history data.
322              
323             =head2 C
324              
325             Returns true if the message contains current data.
326              
327             =head2 C
328              
329             Returns the units of the current data readings in the message.
330              
331             =head2 C
332              
333             Returns the value of the current data reading for the given channel
334             (phase) in the message. If no channel is given then the total of all
335             the current data readings for all channels is returned.
336              
337             =head2 C
338              
339             Returns the string summary of the data in the message. Each line of the
340             string is prefixed by the given prefix or the empty string if the prefix
341             is not supplied.
342              
343             =head2 C
344              
345             Returns a data structure contain any history data from the message.
346              
347             =head1 AUTHOR
348              
349             Mark Hindess
350              
351             =head1 COPYRIGHT AND LICENSE
352              
353             This software is copyright (c) 2014 by Mark Hindess.
354              
355             This is free software; you can redistribute it and/or modify it under
356             the same terms as the Perl 5 programming language system itself.
357              
358             =cut
359              
360             __END__