File Coverage

blib/lib/Power/Outlet/SonoffDiy.pm
Criterion Covered Total %
statement 12 48 25.0
branch 0 24 0.0
condition 0 5 0.0
subroutine 4 11 36.3
pod 3 3 100.0
total 19 91 20.8


line stmt bran cond sub pod time code
1             package Power::Outlet::SonoffDiy;
2 2     2   84218 use strict;
  2         11  
  2         47  
3 2     2   9 use warnings;
  2         2  
  2         44  
4 2     2   8 use base qw{Power::Outlet::Common::IP::HTTP::JSON};
  2         3  
  2         447  
5 2     2   11 use JSON qw{decode_json};
  2         3  
  2         10  
6              
7             our $VERSION = '0.48';
8              
9             =head1 NAME
10              
11             Power::Outlet::SonoffDiy - Control and query a Sonoff DIY device
12              
13             =head1 SYNOPSIS
14              
15             my $outlet = Power::Outlet::SonoffDiy->new(host => "SonoffDiy");
16             print $outlet->query, "\n";
17             print $outlet->on, "\n";
18             print $outlet->off, "\n";
19              
20             =head1 DESCRIPTION
21              
22             Power::Outlet::SonoffDiy is a package for controlling and querying Sonoff ESP8266 hardware running Sonoff firmware in DIY mode. This package supports and has been tested on both the version 1.4 (firmware 3.3.0) and version 2.0 (firmware 3.6.0) of the API.
23              
24             From: L
25              
26             Commands can be executed via HTTP POST requests, for example:
27              
28             curl -i -XPOST -d '{"deviceid":"","data":{}}' http://10.10.7.1:8081/zeroconf/info
29              
30             1.4 Return where data is a string
31              
32             {
33             "seq" : 21,
34             "error" : 0,
35             "data" : "{\"switch\":\"off\",\"startup\":\"stay\",\"pulse\":\"off\",\"pulseWidth\":500,\"ssid\":\"my_ssid\",\"otaUnlock\":false}"
36             }
37              
38             2.0 Return where data is an object
39              
40             {
41             "seq" : 12,
42             "error" : 0,
43             "data":{
44             "switch" : "on",
45             "startup" : "stay",
46             "pulse" : "off",
47             "pulseWidth" : 500,
48             "ssid" : "my_ssid",
49             "otaUnlock" : false,
50             "fwVersion" : "3.6.0",
51             "deviceid" : "1001262ec1",
52             "bssid" : "fc:ec:da:81:c:98",
53             "signalStrength" : -61
54             }
55             }
56              
57             curl -i -XPOST -d '{"deviceid":"","data":{"switch":"off"}}' http://10.10.7.1:8081/zeroconf/switch
58             {
59             "seq" : 22,
60             "error" : 0
61             }
62              
63             curl -i -XPOST -d '{"deviceid":"","data":{"switch":"on"}}' http://10.10.7.1:8081/zeroconf/switch
64             {
65             "seq" : 23,
66             "error" : 0
67             }
68              
69             =head1 USAGE
70              
71             use Power::Outlet::SonoffDiy;
72             my $outlet = Power::Outlet::SonoffDiy->new(host=>"SonoffDiy");
73             print $outlet->on, "\n";
74              
75             =head1 CONSTRUCTOR
76              
77             =head2 new
78              
79             my $outlet = Power::Outlet->new(type=>"SonoffDiy", host=>"SonoffDiy");
80             my $outlet = Power::Outlet::SonoffDiy->new(host=>"SonoffDiy");
81              
82             =head1 PROPERTIES
83              
84             =head2 host
85              
86             Sets and returns the hostname or IP address.
87              
88             Default: SonoffDiy
89              
90             =cut
91              
92 0     0     sub _host_default {'SonoffDiy'};
93              
94             =head2 port
95              
96             Sets and returns the port number.
97              
98             Default: 8081
99              
100             =cut
101              
102 0     0     sub _port_default {'8081'};
103              
104             =head2 http_path
105              
106             Sets and returns the http_path.
107              
108             Default: /
109              
110             =cut
111              
112 0     0     sub _http_path_default {'/'};
113              
114             =head1 METHODS
115              
116             =head2 name
117              
118             Returns the name as configured.
119              
120             Note: The Sonoff DIY firmware does not support setting a hostname or friendly name.
121              
122             =cut
123              
124             #see Power::Outlet::Common->name
125             #see Power::Outlet::Common::IP->_name_default
126              
127             =head2 query
128              
129             Sends an HTTP message to the device to query the current state
130              
131             =cut
132              
133             sub query {
134 0     0 1   my $self = shift;
135 0           return $self->_call();
136             }
137              
138             =head2 on
139              
140             Sends a message to the device to Turn Power ON
141              
142             =cut
143              
144             sub on {
145 0     0 1   my $self = shift;
146 0           return $self->_call('ON');
147             }
148              
149             =head2 off
150              
151             Sends a message to the device to Turn Power OFF
152              
153             =cut
154              
155             sub off {
156 0     0 1   my $self = shift;
157 0           return $self->_call('OFF');
158             }
159              
160             =head2 switch
161              
162             Sends a message to the device to toggle the power
163              
164             =cut
165              
166             #see Power::Outlet::Common->switch
167              
168             =head2 cycle
169              
170             Sends messages to the device to Cycle Power (ON-OFF-ON or OFF-ON-OFF).
171              
172             =cut
173              
174             #see Power::Outlet::Common->cycle
175              
176             #from https://github.com/itead/Sonoff_Devices_DIY_Tools/blob/master/SONOFF%20DIY%20MODE%20Protocol%20Doc%20v1.4.md
177              
178             our %ERROR_STRING = (
179             '0' => 'successfully',
180             '400' => 'The operation failed and the request was formatted incorrectly. The request body is not a valid JSON format.',
181             '401' => 'The operation failed and the request was unauthorized. Device information encryption is enabled on the device, but the request is not encrypted.',
182             '404' => 'The operation failed and the device does not exist. The device does not support the requested deviceid.',
183             '422' => 'The operation failed and the request parameters are invalid. For example, the device does not support setting specific device information.',
184             '403' => 'The operation failed and the OTA function was not unlocked. The interface "3.2.6OTA function unlocking" must be successfully called first.',
185             '408' => 'The operation failed and the pre-download firmware timed out. You can try to call this interface again after optimizing the network environment or increasing the network speed.',
186             '413' => 'The operation failed and the request body size is too large. The size of the new OTA firmware exceeds the firmware size limit allowed by the device.',
187             '424' => 'The operation failed and the firmware could not be downloaded. The URL address is unreachable (IP address is unreachable, HTTP protocol is unreachable, firmware does not exist, server does not support Range request header, etc.)',
188             '471' => "The operation failed and the firmware integrity check failed. The SHA256 checksum of the downloaded new firmware does not match the value of the request body's sha256sum field. Restarting the device will cause bricking issue.",
189             );
190              
191             sub _call {
192 0     0     my $self = shift;
193 0   0       my $switch = shift || ''; #'' or 'ON' or 'OFF'
194 0 0         die('Error: Method _call() syntax _call(""|ON|OFF)') unless $switch =~ m{\A(|ON|OFF)\Z};
195              
196 0 0         my $path = $switch ? 'zeroconf/switch' : 'zeroconf/info';
197              
198 0           my $payload = {};
199 0           $payload->{'deviceid'} = ''; #required to exist for 1.4
200 0 0         $payload->{'data'} = $switch ? {switch=>lc($switch)} : {}; #required to exist
201              
202             #http://:8081/zeroconf/switch
203 0           my $url = $self->url; #isa URI from Power::Outlet::Common::IP::HTTP
204 0           $url->path($path);
205             #print "$url\n";
206              
207 0           my $hash = $self->json_request(POST => $url, $payload); #isa HASH
208             #{"seq":16,"error":0}
209             #{"seq":17,"error":0,"data":"{\"switch\":\"on\",\"startup\":\"stay\",\"pulse\":\"off\",\"pulseWidth\":500,\"ssid\":\"davisnetworks.com\",\"otaUnlock\":false}"}
210 0 0         die('Error: Method _call() web service did not return JSON object') unless ref($hash) eq 'HASH';
211 0           my $error_code = $hash->{'error'};
212 0 0         if ($error_code) {
213 0   0       my $error_string = $ERROR_STRING{$error_code} || "Unknown Error Code $error_code";
214 0           die(sprintf('Error: Method _call(), Web Service Error: %s "%s"', $error_code, $error_string));
215             }
216              
217 0 0         unless ($switch) { #info
218 0 0         my $data = $hash->{'data'} or die('Error: JSON malformed missing data key');
219 0           local $@;
220 0 0         $data = eval{decode_json($data)} unless ref($data) eq 'HASH'; #bug in 1.4 API fixed in 2.0
  0            
221 0           my $error = $@;
222 0 0         die(qq{Error: JSON malformed converting data JSON}) if $error;
223 0 0         die(qq{Error: JSON malformed converting data JSON}) unless ref($data) eq 'HASH';
224 0 0         $switch = $data->{'switch'} or die('Error: JSON malformed extracting switch value');
225 0 0         die(qq{Error: JSON malformed switch value unexpected "$switch"}) unless $switch =~ m{\A(on|off)\Z}i;
226 0           $switch = uc($switch); #match API
227             }
228              
229 0           return $switch;
230             }
231              
232             =head1 BUGS
233              
234             Please log on RT and send an email to the author.
235              
236             =head1 SUPPORT
237              
238             DavisNetworks.com supports all Perl applications including this package.
239              
240             =head1 AUTHOR
241              
242             Michael R. Davis
243             CPAN ID: MRDVT
244             DavisNetworks.com
245              
246             =head1 COPYRIGHT
247              
248             Copyright (c) 2020 Michael R. Davis
249              
250             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
251              
252             The full text of the license can be found in the LICENSE file included with this module.
253              
254             =head1 SEE ALSO
255              
256             L
257              
258             =cut
259              
260             1;