File Coverage

blib/lib/Device/Gembird.pm
Criterion Covered Total %
statement 57 180 31.6
branch 0 60 0.0
condition 0 23 0.0
subroutine 19 33 57.5
pod 6 6 100.0
total 82 302 27.1


line stmt bran cond sub pod time code
1             package Device::Gembird;
2              
3 1     1   24500 use 5.006;
  1         3  
  1         49  
4 1     1   6 use strict;
  1         2  
  1         43  
5 1     1   5 use warnings FATAL => 'all';
  1         6  
  1         61  
6              
7 1     1   5 use Carp;
  1         1  
  1         69  
8 1     1   896 use IO::Socket;
  1         29534  
  1         5  
9 1     1   2788 use IO::Select;
  1         3092  
  1         123  
10              
11             our @ISA = qw(Exporter);
12              
13 1     1   13 use constant SOCK_ON => 0x01;
  1         5  
  1         93  
14 1     1   7 use constant SOCK_OFF => 0x02;
  1         3  
  1         51  
15 1     1   7 use constant SOCK_SKIP => 0x04;
  1         2  
  1         60  
16 1     1   9 use constant SOCK_ERROR => 0x08;
  1         2  
  1         83  
17              
18 1     1   8 use constant SOCK_STATE_DISCONNECTED => 0x00;
  1         4  
  1         68  
19 1     1   6 use constant SOCK_STATE_CONNECTED => 0x01;
  1         3  
  1         78  
20 1     1   7 use constant SOCK_STATE_AUTHENTICATED => 0x02;
  1         3  
  1         75  
21              
22 1     1   29 use constant SOCK_ERROR_OK => 0x00;
  1         3  
  1         65  
23 1     1   8 use constant SOCK_ERROR_HOST => 0x01;
  1         3  
  1         75  
24 1     1   6 use constant SOCK_ERROR_REFUSED => 0x02;
  1         2  
  1         59  
25 1     1   6 use constant SOCK_ERROR_SECRET => 0x04;
  1         2  
  1         59  
26 1     1   9 use constant SOCK_ERROR_WRITE => 0x08;
  1         2  
  1         78  
27 1     1   7 use constant SOCK_ERROR_READ => 0x10;
  1         4  
  1         3652  
28              
29             our @EXPORT = qw( SOCK_ON SOCK_OFF SOCK_SKIP );
30              
31             =head1 NAME
32              
33             Device::Gembird - control Gembird EG-PMS-LAN or similar device.
34              
35             =head1 VERSION
36              
37             Version 0.02
38              
39             =cut
40              
41             our $VERSION = '0.02';
42              
43             =head1 SYNOPSIS
44              
45             use Device::Gembird;
46              
47             my $foo = Device::Gembird->new( host => '192.168.1.67', secret => '1' );
48             $foo->socket1(SOCK_OFF);
49             $foo->socket2(SOCK_ON);
50             my $state = $foo->socket3();
51             my $new_state = $foo->socket4(SOCK_ON);
52             ...
53              
54             =head1 DESCRIPTION
55              
56             This module allows to control voltage
57             on Gembird EnerGenie EG-PMS-LAN Programmable surge protector
58             via LAN interface.
59              
60             =head1 METHODS
61              
62             =head2 new
63              
64             Method creates new object with parameters:
65             - host => host to connect to
66             - port => port to connect to. default is 5000.
67             - timeout => timeout for TCP operation. default is 3 sec.
68             - secret => secret of device. default is '1'.
69              
70             =cut
71              
72             sub new {
73 0     0 1   my $invocant = shift;
74 0   0       my $class = ref($invocant) || $invocant;
75 0           my $self = {
76             port => 5000,
77             secret => '1',
78             last_error => '',
79             timeout => 3,
80             'state' => 0,
81             @_
82             };
83 0           bless( $self, $class );
84 0           return $self;
85             }
86              
87             =head2 get_last_error
88              
89             Returns last error occurred while communicating
90              
91             =cut
92              
93             sub get_last_error {
94 0     0 1   my $self = shift;
95 0           return $self->{last_error};
96             }
97              
98             =head2 socket1
99              
100             Sets new state of Socket 1 (if was specified)
101             returns current state of Socket 1
102              
103             =cut
104              
105             sub socket1 {
106 0     0 1   my $self = shift;
107 0           my $control = shift;
108 0 0         if ($control) {
109 0           $self->_set_ctrl( $control, SOCK_SKIP, SOCK_SKIP, SOCK_SKIP );
110             }
111 0           return $self->_resolve_state(0);
112             }
113              
114             =head2 socket2
115              
116             Sets new state of Socket 2 (if was specified)
117             returns current state of Socket 2
118              
119             =cut
120              
121             sub socket2 {
122 0     0 1   my $self = shift;
123 0           my $control = shift;
124 0 0         if ($control) {
125 0           $self->_set_ctrl( SOCK_SKIP, $control, SOCK_SKIP, SOCK_SKIP );
126             }
127 0           return $self->_resolve_state(1);
128             }
129              
130             =head2 socket3
131              
132             Sets new state of Socket 3 (if was specified)
133             returns current state of Socket 3
134              
135             =cut
136              
137             sub socket3 {
138 0     0 1   my $self = shift;
139 0           my $control = shift;
140 0 0         if ($control) {
141 0           $self->_set_ctrl( SOCK_SKIP, SOCK_SKIP, $control, SOCK_SKIP );
142             }
143 0           return $self->_resolve_state(2);
144             }
145              
146             =head2 socket4
147              
148             Sets new state of Socket 4 (if was specified)
149             returns current state of Socket 4
150              
151             =cut
152              
153             sub socket4 {
154 0     0 1   my $self = shift;
155 0           my $control = shift;
156 0 0         if ($control) {
157 0           $self->_set_ctrl( SOCK_SKIP, SOCK_SKIP, SOCK_SKIP, $control );
158             }
159 0           return $self->_resolve_state(3);
160             }
161              
162             sub _resolve_state {
163 0     0     my $self = shift;
164 0           my $socket = shift;
165 0           my $res = SOCK_ERROR;
166 0 0 0       if ($self->{stat} and exists($self->{stat}->[$socket])) {
167 0           my $state = $self->{stat}->[$socket];
168 0 0         if ($state == 17) {
    0          
169 0           $res = SOCK_ON;
170             }
171             elsif ($state == 34) {
172 0           $res = SOCK_OFF;
173             }
174             }
175 0           return $res;
176             }
177              
178             sub _append_args {
179 0     0     my $self = shift;
180 0           my %args = @_;
181 0           while ( my( $k, $v ) = each %args ) {
182 0           $self->{$k} = $v;
183             }
184             }
185              
186             sub _ord {
187 0     0     my $self = shift;
188 0           my( $str, $offset ) = @_;
189 0           return ord(substr( $str, $offset, 1 ));
190             }
191              
192             sub _get_state {
193 0     0     my $self = shift;
194 0 0         if ($self->{poller}->can_read(1.0)) {
195 0           $self->{sock}->recv( my $state, 4 );
196              
197 0           for ( my $i = 0; $i < 4; $i++ ) {
198 0           $self->{stat}->[3-$i]=(
199             (
200             (
201             (
202             (
203             $self->_ord( $state, $i ) -
204             $self->_ord( $self->{secret}, 1 )
205             )
206             ) ^ $self->_ord( $self->{secret}, 0 )
207             ) - $self->_ord( $self->{task}, 3 )
208             ) ^ $self->_ord( $self->{task}, 2 )
209             ) & 0xFF;
210             }
211             }
212             else {
213 0           return $self->_set_last_error(SOCK_ERROR_READ);
214             }
215             }
216              
217             sub _set_ctrl {
218 0     0     my $self = shift;
219 0           my @ctrl = @_;
220 0           my $ctrl = '';
221 0           my $attempt = 0;
222 0           my $res = 0;
223 0   0       do {
224 0 0         if (( $self->{'state'} & SOCK_STATE_CONNECTED ) !=SOCK_STATE_CONNECTED )
225             {
226 0           $res = $self->_connect();
227             }
228 0 0 0       if ( !$res
229             and ( $self->{'state'} & SOCK_STATE_AUTHENTICATED ) !=
230             SOCK_STATE_AUTHENTICATED )
231             {
232 0           $res = $self->_auth();
233             }
234             } while ($res and $attempt++ < 3);
235 0 0         if ($res) {
236 0           return $res;
237             }
238 0           for ( my $i = 0; $i < 4; $i++ ) {
239 0   0       $ctrl[3-$i] ||= SOCK_SKIP;
240 0 0         if ($ctrl[3-$i] > SOCK_SKIP) {
241 0           $ctrl[3-$i] = SOCK_SKIP;
242             }
243             $ctrl .= chr(
244             (
245             (
246             (
247 0           ( $ctrl[3-$i] ^ $self->_ord( $self->{task}, 2 ) ) +
248             $self->_ord( $self->{task}, 3 )
249             ) ^ $self->_ord( $self->{secret}, 0 )
250             ) + $self->_ord( $self->{secret}, 1 )
251             ) & 0xFF
252             );
253             }
254 0           $attempt = 0;
255 0           $res = 0;
256 0   0       do {
257 0 0         if ($self->{poller}->can_write(1.0)) {
258 0           $self->{sock}->send($ctrl);
259             }
260             else {
261 0           $res = $self->_set_last_error(SOCK_ERROR_WRITE);
262             }
263             } while ( $res and $attempt++ < 3 );
264 0 0         if ($res) {
265 0           return $res;
266             }
267 0           $self->_get_state();
268             }
269              
270             sub _auth {
271 0     0     my $self = shift;
272 0           $self->_append_args(@_);
273 0 0         unless (exists($self->{secret})) {
274 0           return $self->_set_last_error(SOCK_ERROR_SECRET);
275             }
276 0           my $len = length($self->{secret});
277 0 0         if ( $len > 8 ) {
    0          
278 0           $self->{secret} = substr($self->{secret}, 0, 7);
279             }
280             elsif ( $len < 8 ) {
281 0           $self->{secret} .= ' ' x ( 8 - $len );
282             }
283 0 0         if ( ( $self->{'state'} & SOCK_STATE_CONNECTED ) == SOCK_STATE_CONNECTED ) {
284 0 0         if ($self->{poller}->can_write(1.0)) {
285 0           $self->{sock}->send(chr(0x11));
286             }
287             else {
288 0           return $self->_set_last_error(SOCK_ERROR_WRITE);
289             }
290 0 0         if ($self->{poller}->can_read(1.0)) {
291 0           $self->{sock}->recv( $self->{task}, 4 );
292             }
293             else {
294 0           return $self->_set_last_error(SOCK_ERROR_READ);
295             }
296 0 0         if (length($self->{task}) == 4) {
297 0           my $res10 =
298             (($self->_ord($self->{task},0)^$self->_ord($self->{secret},2))*
299             $self->_ord($self->{secret},0))
300             ^($self->_ord($self->{secret},6)|
301             ($self->_ord($self->{secret},4)<<8))
302             ^$self->_ord($self->{task},2);
303 0           my $res32 =
304             (($self->_ord($self->{task},1)^$self->_ord($self->{secret},3))*
305             $self->_ord($self->{secret},1))
306             ^($self->_ord($self->{secret},7)|
307             ($self->_ord($self->{secret},5)<<8))
308             ^$self->_ord($self->{task},3);
309 0           my $res =
310             chr($res10 & 0xFF)
311             . chr($res10 >> 8)
312             . chr($res32 & 0xFF)
313             . chr($res32 >> 8);
314              
315 0 0         if ($self->{poller}->can_write(1.0)) {
316 0           $self->{sock}->send($res);
317             }
318             else {
319 0           return $self->_set_last_error(SOCK_ERROR_WRITE);
320             }
321 0           $self->{'state'} |= SOCK_STATE_AUTHENTICATED;
322 0           $self->_get_state();
323             }
324             }
325 0           return SOCK_ERROR_OK;
326             }
327              
328             sub _connect {
329 0     0     my $self = shift;
330 0           $self->_append_args(@_);
331 0 0 0       unless (exists($self->{host}) and exists($self->{port})) {
332 0           return $self->_set_last_error(SOCK_ERROR_HOST);
333             }
334              
335 0           $self->{sock} = new IO::Socket::INET(
336             PeerHost => $self->{host},
337             PeerPort => $self->{port},
338             Timeout => $self->{timeout},
339             Proto => 'tcp',
340             );
341              
342 0 0 0       if ($self->{sock} && $self->{sock}->connected) {
343 0           $self->{'state'} |= SOCK_STATE_CONNECTED;
344 0           $self->{poller} = IO::Select->new();
345 0           $self->{poller}->add($self->{sock});
346             }
347             else {
348 0           return $self->_set_last_error(SOCK_ERROR_REFUSED);
349             }
350 0           return SOCK_ERROR_OK;
351             }
352              
353             sub _set_last_error {
354 0     0     my $self = shift;
355 0           my $code = shift;
356 0 0         if ($code == SOCK_ERROR_OK) {
    0          
    0          
    0          
    0          
    0          
357 0           $self->{last_error} = '';
358             }
359             elsif ($code == SOCK_ERROR_HOST) {
360 0           $self->{'state'} = SOCK_STATE_DISCONNECTED;
361 0           $self->{last_error} = "Host or port not specified";
362             }
363             elsif ($code == SOCK_ERROR_REFUSED) {
364 0           $self->{'state'} = SOCK_STATE_DISCONNECTED;
365 0           $self->{last_error} = "Failed to connect";
366             }
367             elsif ($code == SOCK_ERROR_SECRET) {
368 0           $self->{last_error} = "Secret not specified";
369             }
370             elsif ($code == SOCK_ERROR_WRITE) {
371 0           $self->{last_error} = "Socket is not ready for writing";
372             }
373             elsif ($code == SOCK_ERROR_READ) {
374 0           $self->{last_error} = "Socket is not ready for reading";
375             }
376 0           return $code;
377             }
378              
379             =head1 AUTHOR
380             Leandr Khaliullov, C<< >>
381              
382             =head1 BUGS
383              
384             Please report any bugs or feature requests to C, or through
385             the web interface at L. I will be notified, and then you'll
386             automatically be notified of progress on your bug as I make changes.
387              
388              
389              
390              
391             =head1 SUPPORT
392              
393             You can find documentation for this module with the perldoc command.
394              
395             perldoc Device::Gembird
396              
397              
398             You can also look for information at:
399              
400             =over 4
401              
402             =item * RT: CPAN's request tracker (report bugs here)
403              
404             L
405              
406             =item * AnnoCPAN: Annotated CPAN documentation
407              
408             L
409              
410             =item * CPAN Ratings
411              
412             L
413              
414             =item * Search CPAN
415              
416             L
417              
418             =back
419              
420              
421             =head1 ACKNOWLEDGEMENTS
422              
423              
424             =head1 LICENSE AND COPYRIGHT
425              
426             Copyright 2014 Leandr Khaliullov.
427              
428             This program is free software; you can redistribute it and/or modify it
429             under the terms of the the Artistic License (2.0). You may obtain a
430             copy of the full license at:
431              
432             L
433              
434             Any use, modification, and distribution of the Standard or Modified
435             Versions is governed by this Artistic License. By using, modifying or
436             distributing the Package, you accept this license. Do not use, modify,
437             or distribute the Package, if you do not accept this license.
438              
439             If your Modified Version has been derived from a Modified Version made
440             by someone other than you, you are nevertheless required to ensure that
441             your Modified Version complies with the requirements of this license.
442              
443             This license does not grant you the right to use any trademark, service
444             mark, tradename, or logo of the Copyright Holder.
445              
446             This license includes the non-exclusive, worldwide, free-of-charge
447             patent license to make, have made, use, offer to sell, sell, import and
448             otherwise transfer the Package with respect to any patent claims
449             licensable by the Copyright Holder that are necessarily infringed by the
450             Package. If you institute patent litigation (including a cross-claim or
451             counterclaim) against any party alleging that the Package constitutes
452             direct or contributory patent infringement, then this Artistic License
453             to you shall terminate on the date that such litigation is filed.
454              
455             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
456             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
457             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
458             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
459             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
460             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
461             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
462             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
463              
464              
465             =cut
466              
467             1; # End of Device::Gembird