277 lines
7.0 KiB
Bash
Executable File
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
|