File Coverage

blib/lib/WebService/FritzBox.pm
Criterion Covered Total %
statement 25 75 33.3
branch 0 18 0.0
condition n/a
subroutine 9 18 50.0
pod 3 3 100.0
total 37 114 32.4


line stmt bran cond sub pod time code
1             package WebService::FritzBox;
2             # ABSTRACT: Interface to FritzBox devices
3 1     1   700 use Digest::MD5 qw/md5_hex/;
  1         1  
  1         52  
4 1     1   730 use JSON::MaybeXS;
  1         107883  
  1         104  
5 1     1   1728 use LWP::UserAgent;
  1         51988  
  1         35  
6 1     1   1294 use Log::Log4perl;
  1         90835  
  1         5  
7 1     1   1033 use Moose;
  1         479804  
  1         9  
8 1     1   8277 use MooseX::Params::Validate;
  1         81332  
  1         7  
9 1     1   444 use Try::Tiny;
  1         2  
  1         58  
10 1     1   727 use YAML;
  1         7013  
  1         60  
11 1     1   10 BEGIN { Log::Log4perl->easy_init() };
12             our $VERSION = 0.008;
13              
14             with "MooseX::Log::Log4perl";
15              
16             =head1 NAME
17              
18             WebService::FritzBox
19              
20             =head1 DESCRIPTION
21              
22             Interact with FritzBox devices
23              
24             =head1 ATTRIBUTES
25              
26             =cut
27              
28             with "MooseX::Log::Log4perl";
29              
30             =over 4
31              
32             =item password
33              
34             Required.
35              
36             =cut
37             has 'password' => (
38             is => 'ro',
39             isa => 'Str',
40             required => 1,
41             );
42              
43             =item host
44              
45             Optional. Default: fritz.box
46              
47             =cut
48             has 'host' => (
49             is => 'ro',
50             isa => 'Str',
51             required => 1,
52             default => 'fritz.box',
53             );
54              
55             =item use_https
56              
57             Optional. Default: 0
58              
59             =cut
60              
61             has 'use_https' => (
62             is => 'ro',
63             isa => 'Bool',
64             );
65              
66             =item user_agent
67              
68             Optional. A new LWP::UserAgent will be created for you if you don't already have one you'd like to reuse.
69              
70             =cut
71              
72             has 'user_agent' => (
73             is => 'ro',
74             isa => 'LWP::UserAgent',
75             required => 1,
76             lazy => 1,
77             builder => '_build_user_agent',
78              
79             );
80              
81             =item loglevel
82              
83             Optional.
84              
85             =cut
86              
87             has 'loglevel' => (
88             is => 'rw',
89             isa => 'Str',
90             trigger => \&_set_loglevel,
91             );
92              
93             has 'base_url' => (
94             is => 'ro',
95             isa => 'Str',
96             required => 1,
97             lazy => 1,
98             builder => '_build_base_url',
99             );
100              
101             has 'sid' => (
102             is => 'ro',
103             isa => 'Str',
104             required => 1,
105             lazy => 1,
106             builder => '_build_sid',
107             );
108              
109             sub _build_user_agent {
110 0     0     my $self = shift;
111 0           $self->log->debug( "Building useragent" );
112 0           my $ua = LWP::UserAgent->new(
113             keep_alive => 1
114             );
115             # $ua->default_headers( $self->default_headers );
116 0           return $ua;
117             }
118              
119             sub _build_base_url {
120 0     0     my $self = shift;
121 0 0         my $base_url = 'http' . ( $self->use_https ? 's' : '' ) . '://' . $self->host;
122 0           $self->log->debug( "Base url: $base_url" );
123 0           return $base_url;
124             }
125              
126             sub _build_sid {
127 0     0     my $self = shift;
128              
129 0           my $response = $self->user_agent->get( $self->base_url . '/login_sid.lua' );
130 0 0         $self->log->trace( "Login (get challenge) http response:\n" . Dump( $response ) ) if $self->log->is_trace;
131 0           my( $challenge_str ) = ( $response->decoded_content =~ /<Challenge>(\w+)/i );
132             # generate a response to the challenge
133 0           my $ch_pw = $challenge_str . '-' . $self->password;
134 0           $ch_pw =~ s/(.)/$1 . chr(0)/eg;
  0            
135 0           my $md5 = lc(md5_hex($ch_pw));
136 0           my $challenge_response = $challenge_str . '-' . $md5;
137             # Get session id
138 0           $response = $self->user_agent->get( $self->base_url . '/login_sid.lua?user=&response=' . $challenge_response );
139 0 0         $self->log->trace( "Login (challenge sent) http response :\n" . Dump( $response ) ) if $self->log->is_trace;
140              
141             # Read session id from XMl
142 0           my( $sid ) = ( $response->content =~ /<SID>(\w+)/i );
143 0           $self->log->debug( "SID: $sid" );
144 0           return $sid;
145             }
146              
147             sub _set_loglevel {
148 0     0     my( $self, $new, $old ) = @_;
149 0           $self->log->level( $new );
150             }
151              
152              
153             =back
154              
155             =head1 METHODS
156              
157             =over 4
158              
159             =item init
160              
161             Create the user agent log in (get a sid).
162              
163             =cut
164              
165             sub init {
166 0     0 1   my $self = shift;
167 0           my $ua = $self->user_agent;
168 0           my $sid = $self->sid;
169             }
170              
171             =item get
172              
173             Get some path from the FritzBox. e.g.
174            
175             my $response = $fb->get( path => '/internet/inetstat_monitor.lua?useajax=1&xhr=1&action=get_graphic' );
176              
177             Returns the HTTP::Response object
178              
179             =cut
180              
181             sub get {
182 0     0 1   my ( $self, %params ) = validated_hash(
183             \@_,
184             path => { isa => 'Str' },
185             );
186              
187             my $response = $self->user_agent->get(
188             $self->base_url .
189             $params{path} .
190 0 0         ( $params{path} =~ m/\?/ ? '&' : '?' ) .
191             'sid=' . $self->sid );
192 0 0         $self->log->trace( Dump( $response ) ) if $self->log->is_trace;
193 0           return $response;
194             }
195              
196             =item bandwidth
197              
198             A wrapper around the /inetstat_monitor endpoint which responds with a normalised hash. The monitor web page
199             on the fritz.box refreshes every 5 seconds, and it seems there is a new value every 5 seconds... 5 seconds is
200             probably a reasonable lowest request interval for this method.
201              
202             Example response:
203              
204             ---
205             available:
206             downstream: 11404000
207             upstream: 2593000
208             current:
209             downstream:
210             internet: 303752
211             media: 0
212             total: 303752
213             upstream:
214             default: 33832
215             high: 22640
216             low: 0
217             realtime: 1600
218             total: 58072
219             max:
220             downstream: 342241935
221             upstream: 655811
222              
223             The section C<current> represents the current (last 5 seconds) bandwith consumption.
224             The value C<current.downstream.total> is the sum of the C<media> and C<internet> fields
225             The value C<current.upstream.total> is the sum of the respective C<default>, C<high>, C<low> and C<realtime> fields
226             The section C<available> is the available bandwidth as reported by the DSL modem.
227             The section C<max> represents
228              
229             =cut
230             sub bandwidth {
231 0     0 1   my $self = shift;
232              
233 0           my $response = $self->get( path => '/internet/inetstat_monitor.lua?useajax=1&xhr=1&action=get_graphic' );
234 0 0         $self->log->trace( Dump( $response ) ) if $self->log->is_trace();
235 0 0         if( not $response->is_success ){
236 0           $self->log->logdie( "Request failed: ($response->code): $response->decoded_content" );
237             }
238 0           my $data;
239             try{
240 0     0     $data = decode_json( $response->decoded_content );
241             # It's just an array with one element...
242 0           $data = $data->[0];
243             }catch{
244 0     0     $self->log->logdie( "Could not decode json: $_" );
245 0           };
246            
247             # There is an array of values for every key, but we just want to capture the latest one
248 0           my %latest;
249 0           foreach( qw/prio_default_bps prio_high_bps prio_low_bps prio_realtime_bps mc_current_bps ds_current_bps/ ){
250             # all the '_bps' entries are bytes per second... multiply by 8 to normalise to bits per second
251 0           $latest{$_} = ( split( ',', $data->{$_} ) )[0] * 8;
252             }
253             my $document = {
254             "available" => {
255             "upstream" => int( $data->{upstream} ),
256             "downstream" => int( $data->{downstream} ),
257             },
258             "max" => {
259             "upstream" => int( $data->{max_us} ),
260             "downstream" => int( $data->{max_ds} ),
261             },
262             "current" => {
263             "upstream" => {
264             "low" => int( $latest{prio_low_bps} ),
265             "default" => int( $latest{prio_default_bps} ),
266             "high" => int( $latest{prio_high_bps} ),
267             "realtime" => int( $latest{prio_realtime_bps} ),
268             "total" => $latest{prio_low_bps} + $latest{prio_default_bps} + $latest{prio_high_bps} + $latest{prio_realtime_bps},
269             },
270             "downstream" => {
271             "internet" => int( $latest{ds_current_bps} ),
272             "media" => int( $latest{mc_current_bps} ),
273             "total" => $latest{ds_current_bps} + $latest{mc_current_bps},
274             },
275             }
276 0           };
277              
278             # Info if the current bandwidth is higher than what we expect to have available (this is not a problem, but
279             # it is odd...)
280             # Occasionally (when DSL reconnects) there can be massive spikes... maybe these should be cut out?
281 0 0         if( $document->{current}{upstream}{total} > $document->{available}{upstream} ){
282             $self->log->info( sprintf( "Upstream total (%u) is greater than the available bandwidth (%u)",
283 0           $document->{current}{upstream}{total}, $document->{available}{upstream} ) );
284             }
285 0 0         if( $document->{current}{downstream}{total} > $document->{available}{downstream} ){
286             $self->log->info( sprintf( "Downstream total (%u) is greater than the available bandwidth (%u)",
287 0           $document->{current}{downstream}{total}, $document->{available}{downstream} ) );
288             }
289              
290 0           return $document;
291             }
292              
293             1;
294              
295             =back
296              
297             =head1 COPYRIGHT
298              
299             Copyright 2015, Robin Clarke
300              
301             =head1 AUTHOR
302              
303             Robin Clarke <robin@robinclarke.net>