forked from njh/sonoff-ota-flash-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
/
sonoff-ota-flash.sh
executable file
·276 lines (241 loc) · 7.02 KB
/
sonoff-ota-flash.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#!/bin/bash
#
# BASH Shell script to flash a Sonoff DIY module Over The Air.
#
FIRMWARE_URL_BASE="http://sonoff-ota.aelius.com/"
DEFAULT_FILENAME="tasmota-latest-lite.bin"
IPADDRESS=
SHASUM=
# JSON Pretty Print by Evgeny Karpov
# https://stackoverflow.com/a/38607019/1156096
json_pretty_print() {
grep -Eo '"[^"]*" *(: *([0-9]*|"[^"]*")[^{}\["]*|,)?|[^"\]\[\}\{]*|\{|\},?|\[|\],?|[0-9 ]*,?' | \
awk '{if ($0 ~ /^[}\]]/ ) offset-=4; printf "%*c%s\n", offset, " ", $0; if ($0 ~ /^[{\[]/) offset+=4}'
}
sonoff_http_request() {
local hostname="${1}"
local path="${2}"
local body="${3:-}"
local url="http://${hostname}:8081/zeroconf/${path}"
if [ -z "${body}" ]; then
body='{"data":{}}'
fi
# Build up the curl command arguments as an array
cmd=('curl' '--silent' '--show-error')
cmd+=('-XPOST')
cmd+=('--header' "Content-Type: application/json")
cmd+=('--data-raw' "${body}")
cmd+=("${url}")
output=$("${cmd[@]}")
exit_code=$?
if [ "$exit_code" -ne 0 ]; then
echo "Error posting to: ${url}"
echo "${output}"
exit $exit_code
fi
echo "${output}" | json_pretty_print
sleep 1
}
lookup_ip_address() {
local hostname="${1}"
# We use dscacheutil / getent because it can do multicast dns lookups
if command -v dscacheutil &> /dev/null; then
dscacheutil -q host -a name "${hostname}" | grep -m 1 'ip_address:' | awk '{print $2}'
elif command -v getent &> /dev/null; then
getent ahostsv4 "${hostname}" | grep -m 1 -oE "^([0-9]{1,3}\.){3}[0-9]{1,3}"
else
echo "Unable to resolve hostname to ip address: didn't find dscacheutil or getent." >&2
exit 1
fi
}
mdns_browse() {
local service="${1}"
local domain="${2:local.}"
if command -v dns-sd &> /dev/null; then
# Use the 'dns-sd' command on Mac OS
# expect is used because dns-sd doesn't timeout
output=$(expect <<-EOD
set timeout 10
spawn -noecho dns-sd -B ${service} ${domain}
expect {
" Add " {exit 0}
timeout {exit 1}
eof {exit 2}
default {exp_continue}
}
EOD
)
# Find the first 'Add' line from the output
echo "${output}" | grep -m 1 ' Add ' | awk '{sub("\r", "", $NF); print $NF}'
elif command -v avahi-browse &> /dev/null; then
# Use the 'avahi-browse' command on Linux
avahi-browse -pt -d "${domain}" "${service}" | awk 'BEGIN {FS=";"} {if ($1=="+" && $3=="IPv4") print $4}'
else
echo "Unable to perform multicast DNS discovery : didn't find dns-sd or avahi-browse." >&2
exit 1
fi
}
discover_module() {
echo "Searching for Sonoff module on network..."
hostname=$(mdns_browse '_ewelink._tcp')
if [ -z "${hostname}" ]; then
echo "Failed to find a Sonoff module on the local network." >&2
exit 2
else
echo "Found module on network."
echo "Hostname: ${hostname}"
fi
# Now get the IP address for the hostname
IPADDRESS=$(lookup_ip_address "${hostname}.local.")
if [ -z "${IPADDRESS}" ]; then
echo "Failed to resolve IP address for ${hostname}" >&2
exit 3
fi
echo "IPv4 Address: ${IPADDRESS}"
echo
}
display_info() {
echo "Getting Module Info..."
sonoff_http_request "${IPADDRESS}" info
echo
}
ota_unlock() {
echo "Unlocking for OTA flashing..."
# FIXME: skip this if already unlocked
sonoff_http_request "${IPADDRESS}" ota_unlock
echo
}
ota_flash() {
read -p "Proceed with flashing? [N/y] " -n 1 -r
echo # (optional) move to a new line
if [[ $REPLY =~ ^[Yy]$ ]]
then
echo "Requesting OTA flashing..."
sonoff_http_request "${IPADDRESS}" ota_flash "{\"data\":{\"downloadUrl\":\"${FIRMWARE_URL}\",\"sha256sum\":\"${SHASUM}\"}}"
echo
else
echo "Aborting"
exit 1
fi
}
check_firmware_exists() {
echo "Checking new firmware file exists"
output=$(curl '--fail' '--silent' '--show-error' '--head' "${FIRMWARE_URL}")
exit_code=$?
if [ "$exit_code" -ne 0 ]; then
if [ "$exit_code" -eq 22 ]; then
echo "The firmware file does not exist: ${FIRMWARE_URL}" >&2
else
echo "There was an error checking if firmware exists: ${FIRMWARE_URL}" >&2
fi
exit $exit_code
else
echo "OK"
echo
fi
}
lookup_shasum() {
echo "Looking up sha256sum for firmware"
output=$(curl '--fail' '--silent' '--show-error' "${FIRMWARE_URL}.sha256")
exit_code=$?
if [ "$exit_code" -ne 0 ]; then
echo "Failed to get .sha256 file for: ${FIRMWARE_URL}" >&2
echo "Please add a .sha256 file to server, or pass the SHA256 on the command line" >&2
exit $exit_code
fi
# Check it looks like it is in the right format
if [[ $output =~ ^([0-9a-f]{64})\ \*(.+)$ ]]; then
SHASUM="${BASH_REMATCH[1]}"
echo "OK"
echo
else
echo "Failed to parse SHA256 sum from file: ${output}" >&2
exit 2
fi
}
display_help() {
printf "Usage: %s [options] [<filename or url>]\n" "${0}"
printf "If just a filename is given, it is relative to %s\n\n" "${FIRMWARE_URL_BASE}"
printf -- "Options:\n"
grep -E -e '^[[:space:]]*# PARAM_Usage:' -e '^[[:space:]]*# PARAM_Description:' "${0}" | while read -r usage; read -r description; do
if [[ ! "${usage}" =~ Usage ]] || [[ ! "${description}" =~ Description ]]; then
_exiterr "Error generating help text."
fi
printf " %-32s %s\n" "${usage##"# PARAM_Usage: "}" "${description##"# PARAM_Description: "}"
done
exit 1
}
parse_options() {
check_parameters() {
if [[ -z "${1:-}" ]]; then
echo "The specified command requires additional parameters. See help:" >&2
display_help >&2
exit 1
elif [[ "${1:0:1}" = "-" ]]; then
_exiterr "Invalid argument: ${1}"
fi
}
local params=()
while (( "$#" )); do
case "$1" in
# PARAM_Usage: -i, --ipaddress <ipaddress>
# PARAM_Description: Specify the IP address of the Sonoff module
-i|--ipaddress)
shift 1
check_parameters "${1:-}"
IPADDRESS="${1}"
;;
# PARAM_Usage: -s, --sha256 <sha256>
# PARAM_Description: Specify the SHA256 sum of the firmware
-s|--sha256)
shift 1
check_parameters "${1:-}"
SHASUM="${1}"
;;
# PARAM_Usage: -h, --help
# PARAM_Description: Display this message
-h|--help)
display_help
;;
-*)
echo "Error: Unsupported flag $1" >&2
display_help
;;
*) # preserve positional arguments
if [ -n "$1" ]; then
params+=("$1")
fi
;;
esac
shift
done
if [ "${#params[@]}" -eq 0 ]; then
FIRMWARE_URL="${DEFAULT_FILENAME}"
elif [ "${#params[@]}" -eq 1 ]; then
FIRMWARE_URL="${params[0]}"
else
display_help
fi
# If the filename doesn't start http: then prepend the base URL
if [[ ! ${FIRMWARE_URL} =~ ^https?: ]]; then
FIRMWARE_URL="${FIRMWARE_URL_BASE}${FIRMWARE_URL}"
fi
}
main() {
parse_options "${@:-}"
check_firmware_exists
if [ -z "${SHASUM:-}" ]; then
lookup_shasum
fi
if [ -z "${IPADDRESS:-}" ]; then
discover_module
fi
display_info
ota_unlock
ota_flash
echo "Please wait for your device to finish flashing."
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]
then
main "${@:-}"
fi