File Coverage

blib/lib/Power/Outlet/Dingtian.pm
Criterion Covered Total %
statement 9 74 12.1
branch 0 34 0.0
condition 0 2 0.0
subroutine 3 20 15.0
pod 6 6 100.0
total 18 136 13.2


line stmt bran cond sub pod time code
1             package Power::Outlet::Dingtian;
2 1     1   995 use strict;
  1         2  
  1         30  
3 1     1   5 use warnings;
  1         2  
  1         39  
4 1     1   6 use base qw{Power::Outlet::Common::IP::HTTP};
  1         2  
  1         1092  
5              
6             our $VERSION = '0.50';
7             our $PACKAGE = __PACKAGE__;
8              
9             =head1 NAME
10              
11             Power::Outlet::Dingtian - Control and query Dingtian Relay Boards via the HTTP API
12              
13             =head1 SYNOPSIS
14              
15             my $outlet = Power::Outlet::Dingtian->new(host => "my_host", relay => "1");
16             print $outlet->query, "\n";
17             print $outlet->on, "\n";
18             print $outlet->off, "\n";
19              
20             =head1 DESCRIPTION
21              
22             Power::Outlet::Dingtian is a package for controlling and querying a relay on Dingtian hardware via the HTTP API.
23              
24             Example commands can be executed via web (HTTP) GET requests, for example:
25              
26             Relay Status URL Example
27              
28             http://192.168.1.100/relay_cgi_load.cgi
29              
30             Relay 1 on example (relays are named one-based but the api uses a zero-based index)
31              
32             http://192.168.1.100/relay_cgi.cgi?type=0&relay=0&on=1&time=0&pwd=0&
33              
34             Relay 2 off example
35              
36             http://192.168.1.100/relay_cgi.cgi?type=0&relay=1&on=0&time=0&pwd=0&
37              
38             Relay 2 cycle off-on-off example (note: time in 100ms increments)
39              
40             http://192.168.1.100/relay_cgi.cgi?type=1&relay=1&on=1&time=100&pwd=0&
41              
42             I have tested this package against the Dingtian DT-R002 V3.6A with V3.1.276A firmware configured for both HTTP and HTTPS.
43              
44             =head1 USAGE
45              
46             use Power::Outlet::Dingtian;
47             my $relay = Power::Outlet::Dingtian->new(host=>"my_host", relay=>"1");
48             print $relay->on, "\n";
49              
50             =head1 CONSTRUCTOR
51              
52             =head2 new
53              
54             my $outlet = Power::Outlet->new(type=>"Dingtian", host=>"my_host", relay=>"1");
55             my $outlet = Power::Outlet::Dingtian->new(host=>"my_host", relay=>"1");
56              
57             =head1 PROPERTIES
58              
59             =head2 relay
60              
61             Dingtian API supports up to 32 relays numbered 1 to 32.
62              
63             Default: 1
64              
65             Note: The relays are numbered 1-32 but the api uses a zero based index.
66              
67             =cut
68              
69             sub relay {
70 0     0 1   my $self = shift;
71 0 0         $self->{'relay'} = shift if @_;
72 0 0         $self->{'relay'} = $self->_relay_default unless defined $self->{'relay'};
73 0 0         die("Error: $PACKAGE relay must be between 1 and 32") unless $self->{'relay'} =~ m/\A([1-9]|[12][0-9]|3[012])\Z/;
74 0           return $self->{'relay'};
75             }
76              
77 0     0     sub _relay_default {'1'};
78              
79             =head2 pwd
80              
81             Sets and returns the ID token used for authentication with the Dingtian hardware
82              
83             Default: "0"
84              
85             Can be set in the Relay Password property in the Other section on the Relay Connect screen.
86              
87             =cut
88              
89             sub pwd {
90 0     0 1   my $self = shift;
91 0 0         $self->{'pwd'} = shift if @_;
92 0 0         $self->{'pwd'} = $self->_pwd_default unless defined $self->{'pwd'};
93 0           return $self->{'pwd'};
94             }
95              
96 0     0     sub _pwd_default {'0'};
97              
98             =head2 host
99              
100             Sets and returns the hostname or IP address.
101              
102             Default: 192.168.1.100
103              
104             =cut
105              
106 0     0     sub _host_default {'192.168.1.100'};
107              
108             =head2 port
109              
110             Sets and returns the port number.
111              
112             Default: 80
113              
114             Can be set in the HTTP Server Port property on the Setting screen.
115              
116             =cut
117              
118 0     0     sub _port_default {'80'};
119              
120             =head2 http_scheme
121              
122             Sets and returns the http scheme (i.e. protocol) (e.g. http or https).
123              
124             Default: http
125              
126             Can be set in the HTTP or HTTPS property on the Setting screen
127              
128             =cut
129              
130 0     0     sub _http_scheme_default {'http'}; #see Power::Outlet::Common::IP::HTTP
131 0     0     sub _http_path_default {'/'}; #see Power::Outlet::Common::IP::HTTP
132 0     0     sub _http_path_script_name_set {'relay_cgi.cgi'}; #custom
133 0     0     sub _http_path_script_name_status {'relay_cgi_load.cgi'}; #custom
134              
135             =head1 METHODS
136              
137             =head2 name
138              
139             Sets and returns the friendly name for this relay.
140              
141             =cut
142              
143             #see Power::Outlet::Common
144              
145 0     0     sub _name_default {sprintf("Relay %s", shift->relay)};
146              
147             =head2 query
148              
149             Sends an HTTP message to the device to query the current state
150              
151             =cut
152              
153             sub query {
154 0     0 1   my $self = shift;
155 0           return $self->_call(); #zero params is query but content is different format
156             }
157              
158             =head2 on
159              
160             Sends a message to the device to Turn Power ON
161              
162             =cut
163              
164             sub on {
165 0     0 1   my $self = shift;
166 0           return $self->_call(1);
167             }
168              
169             =head2 off
170              
171             Sends a message to the device to Turn Power OFF
172              
173             =cut
174              
175             sub off {
176 0     0 1   my $self = shift;
177 0           return $self->_call(0);
178             }
179              
180             =head2 switch
181              
182             Sends a message to the device to toggle the power
183              
184             =cut
185              
186             #see Power::Outlet::Common
187              
188             =head2 cycle
189              
190             Sends messages to the device to Cycle Power (ON-OFF-ON or OFF-ON-OFF).
191              
192             =cut
193              
194             sub cycle {
195 0     0 1   my $self = shift;
196 0           my $query = $self->query;
197 0 0         $self->_call(($query eq 'OFF' ? 1 : 0) => $self->cycle_duration);
198 0           return 'CYCLE';
199             }
200              
201             #head2 _call
202             #
203             # Returns "ON" or "OFF" for both query and set http calls
204             #
205             # $self->_call(); #query
206             # $self->_call(1); #on
207             # $self->_call(0); #off
208             # $self->_call(1, 10); #when off then does on 10 seconds wait then off
209             # $self->_call(0, 10); #when on then does off 10 seconds wait then on
210             #
211             # When time is 0 or undef the type is set to 0 which is a simple on/off capability
212             # When time is greater than 0 the the type is set to 1 which is cycle (jogging) capability
213             # This package does not support type 2 which is a relay delay switching capability
214             # The api does not support a toggle capability natively so toggle is implemented as a query/set.
215             #
216             #cut
217              
218 0           sub _call {
219 0     0     my $self = shift;
220 0 0         my $set = scalar(@_) ? 1 : 0;
221 0           my $url = $self->url; #isa URI from Power::Outlet::Common::IP::HTTP
222 0           my $relay = $self->relay; #e.g. 1 .. 32
223 0           my $relay0 = $relay - 1; #e.g. 0 .. 31
224 0 0         if ($set) {
225 0           my $on = shift; #e.g. "1" | "0"
226 0   0       my $time = shift || 0; #time seconds
227 0           my $time_ds = int($time * 10); #time in 100ms increments (deciseconds) for the api
228 0 0         my $type = $time > 0 ? 1 : 0; #0:relay on/off, 1:relay jogging, 2:relay delay
229 0           my $pwd = $self->pwd; #password id token 0 .. 9999
230 0           $url->path($self->http_path . $self->_http_path_script_name_set);
231 0           $url->query_form(type => $type, relay => $relay0, on => $on, time => $time_ds, pwd => $pwd);
232             } else {
233 0           $url->path($self->http_path . $self->_http_path_script_name_status);
234             }
235             #print "$url\n";
236 0           my $response = $self->http_client->request(GET => $url);
237 0 0         if ($response->{"status"} eq "599") {
    0          
238 0           die(sprintf(qq{HTTP Error: "%s %s", URL: "$url", Content: %s}, $response->{"status"}, $response->{"reason"}, $response->{"content"}));
239             } elsif ($response->{"status"} ne "200") {
240 0           die(sprintf(qq{HTTP Error: "%s %s", URL: "$url"}, $response->{"status"}, $response->{"reason"}));
241             }
242              
243 0           my $return = '';
244 0           my $content = $response->{"content"};
245             #print "$content\n";
246 0 0         die(qq{Error: content malformed, url: "$url", content: "$content"}) unless $content =~ m/\A\&[0-9].*\&\Z/;
247 0           my @values = split(/\&/, $content, -1); #LIMIT=-1 since split filters trailing values by default
248 0           shift @values; #API has empty string as first array element
249 0           pop @values; #API has empty string as last array element
250 0           my $ok = shift @values; #0 => OK, 302 => NAK
251 0 0         die(qq{Error: API returned error code "$ok". url: "$url", content: "$content"}) unless $ok eq '0';
252 0 0         if ($set) {
253             #&0&0&0&1&0& #$ok, $type, $relay, $on, $time
254 0           my ($type, $relay, $on, $time) = @values;
255 0           $return = _state($on);
256             } else {
257             #&0&2&0&0& #$ok, $count, $relay[0], $relay[1]
258 0           my $count = shift @values;
259 0           my $on = $values[$relay0]; #relay is zero-based index
260 0           $return = _state($on);
261             }
262 0           return $return;
263              
264             sub _state {
265 0     0     my $state = shift;
266 0 0         die("Error: API returned undefined relay state.") unless defined $state;
267 0 0         return $state eq '1' ? 'ON'
    0          
268             : $state eq '0' ? 'OFF'
269             : die(qq{Error: API returned invalid relay state. state: "$state"});
270             }
271             }
272              
273             =head1 BUGS
274              
275             Please open an issue on GitHub.
276              
277             =head1 AUTHOR
278              
279             Michael R. Davis
280             CPAN ID: MRDVT
281              
282             =head1 COPYRIGHT
283              
284             Copyright (c) 2020 Michael R. Davis
285              
286             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
287              
288             The full text of the license can be found in the LICENSE file included with this module.
289              
290             =head1 SEE ALSO
291              
292             L => programming_manual_en.pdf page 12 "Protocol: HTTP GET CGI"
293              
294             =cut
295              
296             1;