line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Power::Outlet::SonoffDiy; |
2
|
2
|
|
|
2
|
|
105146
|
use strict; |
|
2
|
|
|
|
|
12
|
|
|
2
|
|
|
|
|
59
|
|
3
|
2
|
|
|
2
|
|
9
|
use warnings; |
|
2
|
|
|
|
|
10
|
|
|
2
|
|
|
|
|
52
|
|
4
|
2
|
|
|
2
|
|
10
|
use base qw{Power::Outlet::Common::IP::HTTP::JSON}; |
|
2
|
|
|
|
|
9
|
|
|
2
|
|
|
|
|
564
|
|
5
|
2
|
|
|
2
|
|
13
|
use JSON qw{decode_json}; |
|
2
|
|
|
|
|
3
|
|
|
2
|
|
|
|
|
14
|
|
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
our $VERSION = '0.50'; |
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; |