File Coverage

blib/lib/WebService/Whistle/Pet/Tracker/API.pm
Criterion Covered Total %
statement 31 71 43.6
branch 9 46 19.5
condition n/a
subroutine 8 17 47.0
pod 13 13 100.0
total 61 147 41.5


line stmt bran cond sub pod time code
1             package WebService::Whistle::Pet::Tracker::API;
2 5     5   390268 use strict;
  5         57  
  5         142  
3 5     5   34 use warnings;
  5         19  
  5         145  
4 5     5   3851 use JSON::XS qw{};
  5         38718  
  5         132  
5 5     5   3763 use HTTP::Tiny qw{};
  5         258453  
  5         5059  
6              
7             our $VERSION = '0.03';
8             our $PACKAGE = __PACKAGE__;
9             our $API_URL = 'https://app.whistle.com/api';
10              
11             =head1 NAME
12              
13             WebService::Whistle::Pet::Tracker::API - Perl interface to access the Whistle Pet Tracker Web Service
14              
15             =head1 SYNOPSIS
16              
17             use WebService::Whistle::Pet::Tracker::API;
18             my $ws = WebService::Whistle::Pet::Tracker::API->new(email=>$email, password=>$password);
19             my $pets = $ws->pets; #isa ARRAY of HASHes
20             foreach my $pet (@$pets) {
21             print JSON::XS->new->pretty->encode($pet);
22             }
23              
24             =head1 DESCRIPTION
25              
26             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.
27              
28             =head1 CONSTRUCTORS
29            
30             =head2 new
31            
32             my $ws = WebService::Whistle::Pet::Tracker::API->new(email=>$email, password=>$password);
33            
34             =cut
35            
36             sub new {
37 1     1 1 85 my $this = shift;
38 1 50       8 my $class = ref($this) ? ref($this) : $this;
39 1         3 my $self = {};
40 1         3 bless $self, $class;
41 1 50       12 %$self = @_ if @_;
42 1         3 return $self;
43             }
44              
45             =head1 PROPERTIES
46              
47             =head2 email
48              
49             Sets and returns the registered Whistle account email
50              
51             =cut
52              
53             sub email {
54 2     2 1 3108 my $self = shift;
55 2 100       9 $self->{'email'} = shift if @_;
56 2 50       7 die("Error: Whistle API: email required") unless $self->{'email'};
57 2         8 return $self->{'email'};
58             }
59              
60             =head2 password
61              
62             Sets and returns the registered Whistle account password
63              
64             =cut
65              
66             sub password {
67 2     2 1 8 my $self = shift;
68 2 100       8 $self->{'password'} = shift if @_;
69 2 50       7 die("Error: Whistle API: password required") unless $self->{'password'};
70 2         7 return $self->{'password'};
71             }
72              
73             =head1 METHODS
74              
75             =head2 pets
76              
77             Returns a list of pets as an array reference
78              
79             my $pets = $ws->pets;
80              
81             =cut
82              
83             sub pets {
84 0     0 1 0 my $self = shift;
85 0         0 return $self->api('/pets')->{'pets'};
86             }
87              
88             =head2 device
89              
90             Returns device data for the given device id
91              
92             my $device = $ws->device('WXX-ABC123');
93             my $battery_level = $device->{'battery_level'}; #0-100 charge level
94              
95             =cut
96              
97             sub device {
98 0     0 1 0 my $self = shift;
99 0 0       0 my $serial_number = shift or die("Error: Whistle API: Device serial number required.");
100 0         0 return $self->api("/devices/$serial_number")->{'device'};
101             }
102              
103             =head2 pet_dailies
104              
105             Returns dailies for the given pet id
106              
107             my $pet_dailies = $ws->pet_dailies($pet_id);
108              
109             =cut
110              
111             sub pet_dailies {
112 0     0 1 0 my $self = shift;
113 0 0       0 my $pet_id = shift or die("Error: Whistle API: pet id required");
114 0         0 return $self->api("/pets/$pet_id/dailies")->{'dailies'}; #https://app.whistle.com/api/pets/123456789/dailies
115             }
116              
117             =head2 pet_daily_items
118              
119             Returns the daily items for the given pet id and day number
120              
121             my $pet_daily_items = $ws->pet_daily_items($pet_id, $day_number);
122              
123             =cut
124              
125             sub pet_daily_items {
126 0     0 1 0 my $self = shift;
127 0 0       0 my $pet_id = shift or die("Error: Whistle API: pet id required");
128 0 0       0 my $day_number = shift or die("Error: Whistle API: day number required");
129 0         0 return $self->api("/pets/$pet_id/dailies/$day_number/daily_items")->{'daily_items'};
130             }
131              
132             =head2 pet_stats
133              
134             Returns pet stats for the given pet id
135              
136             my $pet_stats = $ws->pet_stats(123456789);
137              
138             =cut
139              
140             sub pet_stats {
141 0     0 1 0 my $self = shift;
142 0 0       0 my $pet_id = shift or die("Error: Whistle API: pet id required");
143 0         0 return $self->api("/pets/$pet_id/stats")->{'stats'};
144             }
145              
146             =head2 places
147              
148             Returns registered places as an array reference
149              
150             my $places = $ws->places;
151              
152             =cut
153              
154 0     0 1 0 sub places {shift->api('/places')}; #this api call returns an array instead of a hash like other calls
155              
156             =head1 METHODS (API)
157              
158             =head2 api
159              
160             Returns the decoded JSON data from the given web service end point
161              
162             my $data = $ws->api('/end_point');
163              
164             =cut
165              
166             sub api {
167 0     0 1 0 my $self = shift;
168 0 0       0 my $api_destination = shift or die("Error: Whistle API: api destination required");
169 0         0 my $url = $API_URL. $api_destination;
170 0 0       0 my $response = $api_destination eq '/login'
171             ? $self->ua->post_form($url, [email => $self->email, password => $self->password])
172             : $self->ua->get($url, {
173             headers => {
174             Accept => 'application/vnd.whistle.com.v6+json',
175             Authorization => 'Bearer '. $self->auth_token,
176             },
177             }
178             );
179 0 0       0 print JSON::XS->new->pretty->encode({response => $response}) if $self->{'DEBUG'};
180 0         0 my $status = $response->{'status'};
181 0         0 my $reason = $response->{'reason'};
182 0 0       0 if ($api_destination eq '/login') {
183 0 0       0 die("Error: Whistle API: login failed\n") if $status eq 422;
184 0 0       0 die("Error: Whistle API: request unsuccessful - request: $api_destination, status: $status $reason\n") unless $status eq 201;
185             } else {
186 0 0       0 die("Error: Whistle API: request unsuccessful - request: $api_destination, status: $status $reason\n") unless $status eq 200;
187             }
188 0         0 my $response_content = $response->{'content'};
189 0         0 local $@;
190 0         0 my $response_decoded = eval{JSON::XS::decode_json($response_content)};
  0         0  
191 0         0 my $error = $@;
192 0 0       0 die("Error: Whistle API: invalid JSON - request: $api_destination, status: $status $reason, content: $response_content\n") if $error;
193 0 0       0 print JSON::XS->new->pretty->encode({response_decoded => $response_decoded}) if $self->{'DEBUG'};
194 0         0 return $response_decoded;
195             }
196              
197             =head2 login
198              
199             Calls the login service, caches, and returns the response.
200              
201             =cut
202              
203             sub login {
204 0     0 1 0 my $self = shift;
205 0 0       0 $self->{'login'} = shift if @_;
206 0 0       0 $self->{'login'} = $self->api('/login') unless defined $self->{'login'};
207 0         0 return $self->{'login'};
208             }
209              
210             =head2 auth_token
211              
212             Retrieves the authentication token from the login end point
213              
214             =cut
215              
216 0     0 1 0 sub auth_token {shift->login->{'auth_token'}};
217              
218             =head1 ACCESSORS
219            
220             =head2 ua
221            
222             Returns an L web client user agent
223            
224             =cut
225            
226             sub ua {
227 1     1 1 620 my $self = shift;
228 1 50       4 unless ($self->{'ua'}) {
229 1         8 my %settinges = (
230             keep_alive => 0,
231             agent => "Mozilla/5.0 (compatible; $PACKAGE/$VERSION; See rt.cpan.org 35173)",
232             );
233 1         7 $self->{'ua'} = HTTP::Tiny->new(%settinges);
234             }
235 1         92 return $self->{'ua'};
236             }
237              
238             =head1 SEE ALSO
239              
240             =over
241              
242             =item Python - L
243              
244             =item NodeJS (old api) - L
245              
246             =back
247              
248             =head1 AUTHOR
249              
250             Michael R. Davis
251              
252             =head1 COPYRIGHT AND LICENSE
253              
254             MIT License
255              
256             Copyright (c) 2023 Michael R. Davis
257              
258             =cut
259              
260             1;