myansible/sonoff-ota-flash.sh

277 lines
7.0 KiB
Bash
Executable File

#!/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='{"deviceid":"","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 "{\"deviceid\":\"\",\"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