package WebService::FritzBox;
# ABSTRACT: Interface to FritzBox devices
use Digest::MD5 qw/md5_hex/;
use JSON::MaybeXS;
use LWP::UserAgent;
use Log::Log4perl;
use Moose;
use MooseX::Params::Validate;
use Try::Tiny;
use YAML;
BEGIN { Log::Log4perl->easy_init() };
our $VERSION = 0.010;
with "MooseX::Log::Log4perl";
=head1 NAME
WebService::FritzBox
=head1 DESCRIPTION
Interact with FritzBox devices
=head1 ATTRIBUTES
=cut
with "MooseX::Log::Log4perl";
=over 4
=item password
Required.
=cut
has 'password' => (
is => 'ro',
isa => 'Str',
required => 1,
);
=item host
Optional. Default: fritz.box
=cut
has 'host' => (
is => 'ro',
isa => 'Str',
required => 1,
default => 'fritz.box',
);
=item use_https
Optional. Default: 0
=cut
has 'use_https' => (
is => 'ro',
isa => 'Bool',
);
=item user_agent
Optional. A new LWP::UserAgent will be created for you if you don't already have one you'd like to reuse.
=cut
has 'user_agent' => (
is => 'ro',
isa => 'LWP::UserAgent',
required => 1,
lazy => 1,
builder => '_build_user_agent',
);
=item loglevel
Optional.
=cut
has 'loglevel' => (
is => 'rw',
isa => 'Str',
trigger => \&_set_loglevel,
);
has 'base_url' => (
is => 'ro',
isa => 'Str',
required => 1,
lazy => 1,
builder => '_build_base_url',
);
has 'sid' => (
is => 'ro',
isa => 'Str',
required => 1,
lazy => 1,
builder => '_build_sid',
);
sub _build_user_agent {
my $self = shift;
$self->log->debug( "Building useragent" );
my $ua = LWP::UserAgent->new(
keep_alive => 1
);
# $ua->default_headers( $self->default_headers );
return $ua;
}
sub _build_base_url {
my $self = shift;
my $base_url = 'http' . ( $self->use_https ? 's' : '' ) . '://' . $self->host;
$self->log->debug( "Base url: $base_url" );
return $base_url;
}
sub _build_sid {
my $self = shift;
my $response = $self->user_agent->get( $self->base_url . '/login_sid.lua' );
$self->log->trace( "Login (get challenge) http response:\n" . Dump( $response ) ) if $self->log->is_trace;
my( $challenge_str ) = ( $response->decoded_content =~ /<Challenge>(\w+)/i );
# generate a response to the challenge
my $ch_pw = $challenge_str . '-' . $self->password;
$ch_pw =~ s/(.)/$1 . chr(0)/eg;
my $md5 = lc(md5_hex($ch_pw));
my $challenge_response = $challenge_str . '-' . $md5;
# Get session id
$response = $self->user_agent->get( $self->base_url . '/login_sid.lua?user=&response=' . $challenge_response );
$self->log->trace( "Login (challenge sent) http response :\n" . Dump( $response ) ) if $self->log->is_trace;
# Read session id from XMl
my( $sid ) = ( $response->content =~ /<SID>(\w+)/i );
$self->log->debug( "SID: $sid" );
return $sid;
}
sub _set_loglevel {
my( $self, $new, $old ) = @_;
$self->log->level( $new );
}
=back
=head1 METHODS
=over 4
=item init
Create the user agent log in (get a sid).
=cut
sub init {
my $self = shift;
my $ua = $self->user_agent;
my $sid = $self->sid;
}
=item get
Get some path from the FritzBox. e.g.
my $response = $fb->get( path => '/internet/inetstat_monitor.lua?useajax=1&xhr=1&action=get_graphic' );
Returns the HTTP::Response object
=cut
sub get {
my ( $self, %params ) = validated_hash(
\@_,
path => { isa => 'Str' },
);
my $response = $self->user_agent->get(
$self->base_url .
$params{path} .
( $params{path} =~ m/\?/ ? '&' : '?' ) .
'sid=' . $self->sid );
$self->log->trace( Dump( $response ) ) if $self->log->is_trace;
return $response;
}
=item post
POST some path from the FritzBox. e.g.
my $response = $fb->post( path => '/system/syslog.lua?delete=1' );
Returns the HTTP::Response object
=cut
sub post {
my ( $self, %params ) = validated_hash(
\@_,
path => { isa => 'Str' },
content => { isa => 'Str', optional => 1 }
);
$params{content} .= ( $params{content} ? '&' : '' ) . 'sid=' . $self->sid;
my $response = $self->user_agent->post(
$self->base_url .
$params{path},
Content => $params{content}
);
$self->log->trace( Dump( $response ) ) if $self->log->is_trace;
return $response;
}
=item bandwidth
A wrapper around the /inetstat_monitor endpoint which responds with a normalised hash. The monitor web page
on the fritz.box refreshes every 5 seconds, and it seems there is a new value every 5 seconds... 5 seconds is
probably a reasonable lowest request interval for this method.
Example response:
---
available:
downstream: 11404000
upstream: 2593000
current:
downstream:
internet: 303752
media: 0
total: 303752
upstream:
default: 33832
high: 22640
low: 0
realtime: 1600
total: 58072
max:
downstream: 342241935
upstream: 655811
The section C<current> represents the current (last 5 seconds) bandwith consumption.
The value C<current.downstream.total> is the sum of the C<media> and C<internet> fields
The value C<current.upstream.total> is the sum of the respective C<default>, C<high>, C<low> and C<realtime> fields
The section C<available> is the available bandwidth as reported by the DSL modem.
The section C<max> represents
=cut
sub bandwidth {
my $self = shift;
my $response = $self->get( path => '/internet/inetstat_monitor.lua?useajax=1&xhr=1&action=get_graphic' );
$self->log->trace( Dump( $response ) ) if $self->log->is_trace();
if( not $response->is_success ){
$self->log->logdie( "Request failed: ($response->code): $response->decoded_content" );
}
my $data;
try{
$data = decode_json( $response->decoded_content );
# It's just an array with one element...
$data = $data->[0];
}catch{
$self->log->logdie( "Could not decode json: $_" );
};
# There is an array of values for every key, but we just want to capture the latest one
my %latest;
foreach( qw/prio_default_bps prio_high_bps prio_low_bps prio_realtime_bps mc_current_bps ds_current_bps/ ){
# all the '_bps' entries are bytes per second... multiply by 8 to normalise to bits per second
$latest{$_} = ( split( ',', $data->{$_} ) )[0] * 8;
}
my $document = {
"available" => {
"upstream" => int( $data->{upstream} ),
"downstream" => int( $data->{downstream} ),
},
"max" => {
"upstream" => int( $data->{max_us} ),
"downstream" => int( $data->{max_ds} ),
},
"current" => {
"upstream" => {
"low" => int( $latest{prio_low_bps} ),
"default" => int( $latest{prio_default_bps} ),
"high" => int( $latest{prio_high_bps} ),
"realtime" => int( $latest{prio_realtime_bps} ),
"total" => $latest{prio_low_bps} + $latest{prio_default_bps} + $latest{prio_high_bps} + $latest{prio_realtime_bps},
},
"downstream" => {
"internet" => int( $latest{ds_current_bps} ),
"media" => int( $latest{mc_current_bps} ),
"total" => $latest{ds_current_bps} + $latest{mc_current_bps},
},
}
};
# Info if the current bandwidth is higher than what we expect to have available (this is not a problem, but
# it is odd...)
# Occasionally (when DSL reconnects) there can be massive spikes... maybe these should be cut out?
if( $document->{current}{upstream}{total} > $document->{available}{upstream} ){
$self->log->info( sprintf( "Upstream total (%u) is greater than the available bandwidth (%u)",
$document->{current}{upstream}{total}, $document->{available}{upstream} ) );
}
if( $document->{current}{downstream}{total} > $document->{available}{downstream} ){
$self->log->info( sprintf( "Downstream total (%u) is greater than the available bandwidth (%u)",
$document->{current}{downstream}{total}, $document->{available}{downstream} ) );
}
return $document;
}
1;
=back
=head1 COPYRIGHT
Copyright 2015, Robin Clarke
=head1 AUTHOR
Robin Clarke <robin@robinclarke.net>