Group
Extension

WebService-Whistle-Pet-Tracker-API/lib/WebService/Whistle/Pet/Tracker/API.pm

package WebService::Whistle::Pet::Tracker::API;
use strict;
use warnings;
use JSON::XS qw{};
use HTTP::Tiny qw{};

our $VERSION = '0.03';
our $PACKAGE = __PACKAGE__;
our $API_URL = 'https://app.whistle.com/api';

=head1 NAME

WebService::Whistle::Pet::Tracker::API - Perl interface to access the Whistle Pet Tracker Web Service

=head1 SYNOPSIS

  use WebService::Whistle::Pet::Tracker::API;
  my $ws   = WebService::Whistle::Pet::Tracker::API->new(email=>$email, password=>$password);
  my $pets = $ws->pets; #isa ARRAY of HASHes
  foreach my $pet (@$pets) {
    print JSON::XS->new->pretty->encode($pet);
  }

=head1 DESCRIPTION

Perl interface to access the Whistle Pet Tracker Web Service.  All methods return JSON payloads that are converted to Perl data structures.  Methods that require authentication will request a token and cache it for the duration of the object.

=head1 CONSTRUCTORS
 
=head2 new
 
  my $ws = WebService::Whistle::Pet::Tracker::API->new(email=>$email, password=>$password);
 
=cut
 
sub new {
  my $this  = shift;
  my $class = ref($this) ? ref($this) : $this;
  my $self  = {};
  bless $self, $class;
  %$self    = @_ if @_;
  return $self;
}

=head1 PROPERTIES

=head2 email

Sets and returns the registered Whistle account email

=cut

sub email {
  my $self         = shift;
  $self->{'email'} = shift if @_;
  die("Error: Whistle API: email required") unless $self->{'email'};
  return $self->{'email'};
}

=head2 password

Sets and returns the registered Whistle account password

=cut

sub password {
  my $self            = shift;
  $self->{'password'} = shift if @_;
  die("Error: Whistle API: password required") unless $self->{'password'};
  return $self->{'password'};
}

=head1 METHODS

=head2 pets

Returns a list of pets as an array reference

  my $pets = $ws->pets;

=cut

sub pets {
  my $self = shift;
  return $self->api('/pets')->{'pets'};
}

=head2 device

Returns device data for the given device id

  my $device        = $ws->device('WXX-ABC123');
  my $battery_level = $device->{'battery_level'}; #0-100 charge level

=cut

sub device {
  my $self          = shift;
  my $serial_number = shift or die("Error: Whistle API: Device serial number required.");
  return $self->api("/devices/$serial_number")->{'device'};
}

=head2 pet_dailies

Returns dailies for the given pet id

  my $pet_dailies = $ws->pet_dailies($pet_id);

=cut

sub pet_dailies {
  my $self   = shift;
  my $pet_id = shift or die("Error: Whistle API: pet id required");
  return $self->api("/pets/$pet_id/dailies")->{'dailies'}; #https://app.whistle.com/api/pets/123456789/dailies
}

=head2 pet_daily_items

Returns the daily items for the given pet id and day number

  my $pet_daily_items = $ws->pet_daily_items($pet_id, $day_number);

=cut

sub pet_daily_items {
  my $self       = shift;
  my $pet_id     = shift or die("Error: Whistle API: pet id required");
  my $day_number = shift or die("Error: Whistle API: day number required");
  return $self->api("/pets/$pet_id/dailies/$day_number/daily_items")->{'daily_items'};
}

=head2 pet_stats

Returns pet stats for the given pet id

  my $pet_stats = $ws->pet_stats(123456789);

=cut

sub pet_stats {
  my $self   = shift;
  my $pet_id = shift or die("Error: Whistle API: pet id required");
  return $self->api("/pets/$pet_id/stats")->{'stats'};
}

=head2 places

Returns registered places as an array reference

  my $places = $ws->places;

=cut

sub places {shift->api('/places')}; #this api call returns an array instead of a hash like other calls

=head1 METHODS (API)

=head2 api

Returns the decoded JSON data from the given web service end point

  my $data = $ws->api('/end_point');

=cut

sub api {
  my $self             = shift;
  my $api_destination  = shift or die("Error: Whistle API: api destination required");
  my $url              = $API_URL. $api_destination;
  my $response         = $api_destination eq '/login'
                       ? $self->ua->post_form($url, [email => $self->email, password => $self->password])
                       : $self->ua->get($url, {
                                               headers => {
                                                           Accept        => 'application/vnd.whistle.com.v6+json',
                                                           Authorization => 'Bearer '. $self->auth_token,
                                                          },
                                              }
                                       );
  print JSON::XS->new->pretty->encode({response => $response}) if $self->{'DEBUG'};
  my $status           = $response->{'status'};
  my $reason           = $response->{'reason'};
  if ($api_destination eq '/login') {
    die("Error: Whistle API: login failed\n") if $status eq 422;
    die("Error: Whistle API: request unsuccessful - request: $api_destination, status: $status $reason\n") unless $status eq 201;
  } else {
    die("Error: Whistle API: request unsuccessful - request: $api_destination, status: $status $reason\n") unless $status eq 200;
  }
  my $response_content = $response->{'content'};
  local $@;
  my $response_decoded = eval{JSON::XS::decode_json($response_content)};
  my $error            = $@;
  die("Error: Whistle API: invalid JSON - request: $api_destination, status: $status $reason, content: $response_content\n") if $error;
  print JSON::XS->new->pretty->encode({response_decoded => $response_decoded}) if $self->{'DEBUG'};
  return $response_decoded;
}

=head2 login

Calls the login service, caches, and returns the response.

=cut

sub login {
  my $self         = shift;
  $self->{'login'} = shift if @_;
  $self->{'login'} = $self->api('/login') unless defined $self->{'login'};
  return $self->{'login'};
}

=head2 auth_token

Retrieves the authentication token from the login end point

=cut

sub auth_token {shift->login->{'auth_token'}};

=head1 ACCESSORS
 
=head2 ua
 
Returns an L<HTTP::Tiny> web client user agent
 
=cut
 
sub ua {
  my $self = shift;
  unless ($self->{'ua'}) {
    my %settinges = (
                     keep_alive => 0,
                     agent      => "Mozilla/5.0 (compatible; $PACKAGE/$VERSION; See rt.cpan.org 35173)",
                    );
    $self->{'ua'} = HTTP::Tiny->new(%settinges);
  }
  return $self->{'ua'};
}

=head1 SEE ALSO

=over

=item Python - L<https://github.com/RobertD502/whistleaio>

=item NodeJS (old api) - L<https://github.com/martzcodes/node-whistle>

=back

=head1 AUTHOR

Michael R. Davis

=head1 COPYRIGHT AND LICENSE

MIT License

Copyright (c) 2023 Michael R. Davis

=cut

1;


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.