File Coverage

blib/lib/App/AquariumHive.pm
Criterion Covered Total %
statement 70 216 32.4
branch 0 38 0.0
condition 0 3 0.0
subroutine 24 49 48.9
pod 0 12 0.0
total 94 318 29.5


line stmt bran cond sub pod time code
1             package App::AquariumHive;
2             BEGIN {
3 1     1   497 $App::AquariumHive::AUTHORITY = 'cpan:GETTY';
4             }
5             # ABSTRACT: Temporary Daemon - will later be replaced by HiveHub
6             $App::AquariumHive::VERSION = '0.003';
7             our $VERSION ||= '0.000';
8              
9 1         4 use MooX qw(
10             Options
11 1     1   401 );
  1         8005  
12              
13 1     1   218869 use Path::Tiny;
  1         10349  
  1         66  
14 1     1   467 use PocketIO;
  1         57725  
  1         32  
15 1     1   465 use Plack::Builder;
  1         7120  
  1         79  
16 1     1   538 use Twiggy::Server;
  1         23738  
  1         37  
17 1     1   7 use AnyEvent;
  1         1  
  1         17  
18 1     1   413 use AnyEvent::SerialPort;
  1         18817  
  1         33  
19 1     1   646 use AnyEvent::HTTP;
  1         6935  
  1         83  
20 1     1   546 use File::ShareDir::ProjectDistDir;
  1         9835  
  1         6  
21 1     1   210866 use File::HomeDir;
  1         5791  
  1         71  
22 1     1   544 use JSON::MaybeXS;
  1         526  
  1         50  
23 1     1   920 use DateTime;
  1         100827  
  1         54  
24 1     1   570 use Config::INI::Reader;
  1         23226  
  1         52  
25 1     1   496 use Config::INI::Writer;
  1         2975  
  1         28  
26 1     1   6 use Carp qw( croak );
  1         1  
  1         48  
27 1     1   593 use DDP;
  1         20248  
  1         8  
28 1     1   44 use Module::Runtime qw( use_module );
  1         1  
  1         8  
29             use Module::Pluggable
30 1         8 sub_name => 'plugin_classes',
31             search_path => ['App::AquariumHive::Plugin'],
32             max_depth => 4,
33 1     1   510 require => 1;
  1         7852  
34              
35 1     1   510 use HiveJSO;
  1         17273  
  1         40  
36 1     1   515 use AnyEvent::HiveJSO;
  1         400  
  1         24  
37 1     1   499 use AquariumHive::Simulator;
  1         2  
  1         31  
38 1     1   380 use App::AquariumHive::DB;
  1         3  
  1         39  
39              
40 1     1   518 use Log::Any::Adapter ('Stdout');
  1         10857  
  1         5  
41              
42             with 'App::AquariumHive::LogRole';
43              
44             sub BUILD {
45 0     0 0   my ( $self ) = @_;
46 0 0         path($self->cfg)->mkpath unless -d $self->cfg;
47             }
48              
49             option 'cfg' => (
50             is => 'ro',
51             format => 'i',
52             default => sub {
53             return path(File::HomeDir->my_home,'.aqhive')->absolute->stringify;
54             },
55             doc => 'directory for config',
56             );
57              
58             option 'port' => (
59             is => 'ro',
60             format => 'i',
61             default => '8888',
62             doc => 'port for the webserver',
63             );
64              
65             option 'simulation' => (
66             is => 'ro',
67             default => 0,
68             doc => 'Simulate Aquarium Hive hardware',
69             );
70              
71             option 'name' => (
72             is => 'ro',
73             format => 's',
74             default => 'AQHIVE',
75             doc => 'Name on top of interface',
76             );
77              
78             option 'sensor_rows' => (
79             is => 'ro',
80             format => 's',
81             default => 2,
82             doc => 'Number of sensor rows in use',
83             );
84              
85             option 'no_pwm' => (
86             is => 'ro',
87             default => 0,
88             doc => 'No pwm controls',
89             );
90              
91             option 'no_power' => (
92             is => 'ro',
93             default => 0,
94             doc => 'No power controls',
95             );
96              
97             option 'serial' => (
98             is => 'ro',
99             format => 's',
100             default => '/dev/ttyAMA0',
101             doc => 'serial port for the HiveJSO stream',
102             );
103              
104             option 'baud' => (
105             is => 'ro',
106             format => 's',
107             default => '19200',
108             doc => 'baud rate for the serial port',
109             );
110              
111             option 'agent' => (
112             is => 'ro',
113             format => 's',
114             default => 'App::AquariumHive/'.$VERSION,
115             doc => 'user agent for the web requests',
116             );
117              
118             has config_ini => (
119             is => 'lazy',
120             init_arg => undef,
121             );
122              
123             sub _build_config_ini {
124 0     0     my ( $self ) = @_;
125 0           return path($self->cfg,'aqhive.ini')->stringify;
126             }
127              
128             has config => (
129             is => 'lazy',
130             init_arg => undef,
131             );
132              
133             sub _build_config {
134 0     0     my ( $self ) = @_;
135 0 0         return {} unless -f $self->config_ini;
136 0           return Config::INI::Reader->read_file($self->config_ini);
137             }
138              
139             has db => (
140             is => 'lazy',
141             init_arg => undef,
142             );
143              
144             sub _build_db {
145 0     0     my ( $self ) = @_;
146 0           return App::AquariumHive::DB->connect($self);
147             }
148              
149             sub save_config {
150 0     0 0   my ( $self ) = @_;
151 0           Config::INI::Writer->write_file($self->config, $self->config_ini);
152             }
153              
154             has plugins => (
155             is => 'lazy',
156             init_arg => undef,
157             );
158              
159             sub _build_plugins {
160 0     0     my ( $self ) = @_;
161 0           $self->debug('Building plugins...');
162 0           my @plugins;
163 0           for my $class ($self->plugin_classes) {
164 0           $self->debug('Loading '.$class);
165 0           push @plugins, use_module($class)->new( app => $self );
166             }
167 0           for (@plugins) {
168 0 0         if ($_->can('configure')) {
169 0           $_->configure;
170             }
171             }
172 0           return \@plugins;
173             }
174              
175             has pocketio => (
176             is => 'lazy',
177             init_arg => undef,
178             );
179              
180             sub _build_pocketio {
181 0     0     my ( $self ) = @_;
182             return PocketIO->new( handler => sub {
183 0     0     $self->debug('PocketIO connect');
184 0           my $pio = shift;
185 0           for my $key (keys %{$self->pocketio_handler}) {
  0            
186 0           my @ons = @{$self->pocketio_handler->{$key}};
  0            
187             $pio->on($key, sub {
188 0           my ( $pio, $message ) = @_;
189 0           $self->debug('PocketIO incoming '.$key);
190 0           for my $sub (@ons) {
191 0           $sub->($self, $message);
192             }
193 0           });
194             }
195 0           });
196             }
197              
198             has pocketio_handler => (
199             is => 'rw',
200             init_arg => undef,
201             default => sub {{}},
202             );
203              
204             sub on_socketio {
205 0     0 0   my ( $self, $key, $sub ) = @_;
206 0 0         $self->pocketio_handler->{$key} = [] unless defined $self->pocketio_handler->{$key};
207 0           push @{$self->pocketio_handler->{$key}}, $sub;
  0            
208             }
209              
210             has data_handler => (
211             is => 'rw',
212             init_arg => undef,
213             default => sub {[]},
214             );
215              
216             sub on_data {
217 0     0 0   my ( $self, $sub ) = @_;
218 0           push @{$self->data_handler}, $sub;
  0            
219             }
220              
221             has simulator => (
222             is => 'lazy',
223             init_arg => undef,
224             );
225              
226             sub _build_simulator {
227 0     0     my ( $self ) = @_;
228 0           return AquariumHive::Simulator->new(
229             sensor_rows => $self->sensor_rows,
230             );
231             }
232              
233             has uart => (
234             is => 'lazy',
235             init_arg => undef,
236             );
237              
238             sub _build_uart {
239 0     0     my ( $self ) = @_;
240 0           my $uart;
241 0 0         if ($self->simulation) {
242 0           $uart = AnyEvent::Handle->new(
243             fh => $self->simulator->fh,
244             );
245             } else {
246 0           $uart = AnyEvent::SerialPort->new(
247             serial_port => [
248             $self->serial,
249             [ baudrate => $self->baud ],
250             ],
251             read_size => 1,
252             );
253             }
254             $uart->on_read(sub {
255             $_[0]->push_read(hivejso => sub {
256 0           my ( $uart, $data ) = @_;
257 0 0         if (ref $data eq 'HiveJSO::Error') {
258 0           p($data->error); p($data->garbage);
  0            
259 0           return;
260             }
261 0           my $hivejso = $data->hivejso_short;
262 0           $self->debug('HiveJSO IN '.$hivejso);
263 0 0         if ($data->has_data) {
264 0           $self->send( data => $data->data );
265 0           for my $sub (@{$self->data_handler}) {
  0            
266 0           $sub->($self, $data->data);
267             }
268             }
269 0     0     });
270 0           });
271 0           return $uart;
272             }
273              
274             sub http {
275 0     0 0   my ( $self, $method, $url, @args ) = @_;
276 0           my $cb = pop @args;
277 0           my ( %arg ) = @args;
278 0 0         $arg{headers} = {} unless defined $arg{headers};
279 0 0         $arg{headers}->{'user-agent'} = $self->agent unless defined $arg{headers}->{'user-agent'};
280 0 0         $arg{timeout} = 30 unless defined $arg{timeout};
281 0           return http_request($method, $url, %arg, $cb);
282             }
283              
284             sub send {
285 0     0 0   my ( $self, $key, $data ) = @_;
286 0 0 0       if ($self->pocketio->pool->{connections} && %{$self->pocketio->pool->{connections}}) {
  0            
287 0           my @keys = keys %{$self->pocketio->pool->{connections}};
  0            
288 0           return $self->pocketio->pool->{connections}->{$keys[0]}->sockets->emit($key,$data);
289             }
290 0           return;
291             }
292              
293             has web_root => (
294             is => 'lazy',
295             init_arg => undef,
296             );
297              
298             sub _build_web_root {
299 0     0     my ( $self ) = @_;
300 0           return path(dist_dir('AquariumHive'),'root')->absolute->realpath->stringify;
301             }
302              
303             has web_mounts => (
304             is => 'rw',
305             init_arg => undef,
306             default => sub {{}},
307             );
308              
309             sub web_mount {
310 0     0 0   my ( $self, $mount, $psgi ) = @_;
311 0           $self->web_mounts->{$mount} = $psgi;
312             }
313              
314             has tiles => (
315             is => 'rw',
316             init_arg => undef,
317             default => sub {{}},
318             );
319              
320             sub tile {
321 0     0 0   my ( $self, $key ) = @_;
322 0           return $self->tiles->{$key};
323             }
324              
325             sub add_tile {
326 0     0 0   my ( $self, $key, $tile ) = @_;
327 0           $self->tiles->{$key} = $tile;
328             }
329              
330             has web => (
331             is => 'lazy',
332             init_arg => undef,
333             );
334              
335             sub _build_web {
336 0     0     my ( $self ) = @_;
337 0           my $server = Twiggy::Server->new(
338             port => $self->port,
339             );
340             $server->register_service(builder {
341             enable sub {
342 0           my $app = shift;
343             sub {
344 0           $self->debug('Web Request '.$_[0]->{PATH_INFO});
345 0           my $res = $app->($_[0]);
346 0           return $res;
347 0           };
348 0     0     };
349 0           mount '/shutdown' => sub { exit 0 };
  0            
350 0           mount '/socket.io' => $self->pocketio;
351 0 0         for my $mount (sort { length($a) <=> length($b) || $a cmp $b } keys %{$self->web_mounts}) {
  0            
  0            
352 0           mount '/'.$mount, $self->web_mounts->{$mount};
353             }
354             mount '/tile' => sub {
355 0           my ( $tile ) = $_[0]->{PATH_INFO} =~ m!/(.*)!;
356 0 0         if ($self->tile($tile)) {
357 0 0         return [ 200, [ "Content-Type" => "application/json" ], [encode_json({
358             html => $self->tile($tile)->html,
359             $self->tile($tile)->has_js ? ( js => $self->tile($tile)->js ) : (),
360             })] ];
361             } else {
362 0           return [ 404, [ "Content-Type" => "application/json" ], [encode_json({
363             not => 'found',
364             })] ];
365             }
366 0           };
367             mount '/name' => sub {
368 0           return [ 200, [ "Content-Type" => "application/json" ], ['"'.$self->name.'"'] ];
369 0           };
370             mount '/tiles' => sub {
371 0           return [ 200, [ "Content-Type" => "application/json" ], [encode_json([sort { $a cmp $b } keys %{$self->tiles}])] ];
  0            
  0            
372 0           };
373             mount '/' => builder {
374             enable 'Rewrite',
375 0           rules => sub { s{^/$}{/index.html}; };
  0            
376 0           enable "Plack::Middleware::Static",
377             path => qr{^/},
378             root => $self->web_root;
379 0           };
380 0           });
381 0           return $server;
382             }
383              
384             sub command_aqhive {
385 0     0 0   my ( $self, $command, @args ) = @_;
386 0 0         return unless defined $command;
387 0           my $hivejso;
388 0 0         if ($self->simulation) {
389 0 0         $hivejso = HiveJSO->new(
390             unit => 'rasputin',
391             command => scalar @args ? ([ $command, @args ]) : ($command),
392             )->hivejso_short;
393             } else {
394 0 0         $hivejso = encode_json({
395             o => scalar @args ? ([ $command, @args ]) : ($command),
396             });
397             }
398 0           $self->debug('HiveJSO OUT '.$hivejso);
399 0           $self->uart->push_write($hivejso);
400             }
401              
402             sub run {
403 0     0 0   my ( $self ) = @_;
404              
405 0           $self->plugins;
406 0           $self->web;
407 0           $self->uart;
408 0           $self->db;
409              
410 0           $self->info("Starting App::AquariumHive (port ".$self->port.")...");
411              
412 0     0     my $t = AE::timer 0, 15, sub { $self->command_aqhive('data') };
  0            
413              
414 0           AE::cv->recv;
415             }
416              
417             sub run_cmd {
418 0     0 0   my ( $self, $command ) = @_;
419 0           my @lines;
420 0 0         if ($command) {
421 0           require IPC::Open3; # core
422             # autoflush STDOUT so we can see command output right away
423 0           local $| = 1;
424             # combine stdout and stderr for ease of proxying through the logger
425 0           my $pid = IPC::Open3::open3(my ($in, $out), undef, $command);
426 0           while(defined(my $line = <$out>)){
427 0           chomp($line);
428 0           push @lines, $line;
429             }
430             # zombie repellent
431 0           waitpid($pid, 0);
432 0           my $status = ($? >> 8);
433             }
434 0           return @lines;
435             }
436              
437             1;
438              
439             __END__
440              
441             =pod
442              
443             =head1 NAME
444              
445             App::AquariumHive - Temporary Daemon - will later be replaced by HiveHub
446              
447             =head1 VERSION
448              
449             version 0.003
450              
451             =head1 DESCRIPTION
452              
453             B<IN DEVELOPMENT, DO NOT USE YET>
454              
455             See L<http://aquariumhive.com/> for now.
456              
457             =head1 SUPPORT
458              
459             IRC
460              
461             Join #AquariumHive on irc.freenode.net. Highlight Getty for fast reaction :).
462              
463             Repository
464              
465             https://github.com/homehivelab/aquariumhive
466             Pull request and additional contributors are welcome
467              
468             Issue Tracker
469              
470             https://github.com/homehivelab/aquariumhive/issues
471              
472             =head1 AUTHOR
473              
474             Torsten Raudssus <torsten@raudss.us>
475              
476             =head1 COPYRIGHT AND LICENSE
477              
478             This software is copyright (c) 2014 by Torsten Raudssus.
479              
480             This is free software; you can redistribute it and/or modify it under
481             the same terms as the Perl 5 programming language system itself.
482              
483             =cut