123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479 |
- #!/bin/bash
- usage() {
- cat <<EOF
- Get weather data from FMI's mobile data service.
- Last non-option argument:
- Most options can also be shell-sourced from $conf
- Usage: ./$me [options] [location_id]
- Dependencies: ${deps[@]}
- Options:
- -L int numerical location ID (e.g. from geonames.org; you can use the provided
- get_location_id script). If no location ID is given Helsinki is used.
- Default: geoid=$geoid
- -l str language, possible values: en,fi,sv
- Default: lang=$lang
- -i str comma-separated list of indices (positive whole numbers) to get
- forecasts for. The default is to get them for every hour for the first
- two hours, then every other hour twice, then every three hours. No index
- must be larger than $max_index.
- Default: indices=$indices
- -f str Formatted output templates specifier.
- Will use header."string", forecast."string" and footer."string"
- templates from storage folder (see below, -s option).
- Currently "lua" is the only already present set of templates
- Default: format='' (terminal output)
- -o str Comma-separated list of options to pass to formatting section, to
- override one or several defaults.
- Example: "offset=50,img_size_fc=32,padding_h=10,font=droid\ sans\ mono"
- width - - - - - (px) overall width of output, padding excluded (you
- will still need to adjust your conky.config manually -
- default: $width)
- padding_h - - - (px) horizontal padding (default: $padding_h)
- offset - - - - - (px) initial offset for forecasts - or: height of the
- header (default: $offset). This value is incremented
- with each forecast element.
- img_size_fc - - (px) width and height of forecast image (default: $img_size_fc)
- img_size_wind - (px) width and height of wind image (default: $img_size_wind)
- forecast_height (px) height of each forecast element
- (default: same as img_size_fc)
- tempcol_plus - - (hex) color for temperature >= 0 (default: $tempcol)
- tempcol_minus - (hex) color for temperature < 0 (default: $tempcol_minus)
- textcol - - - - (hex) color for other text (default: $textcol)
- linecol - - - - (hex) color for line separator and time
- (default: $linecol)
- popcol70 - - - - (hex) color for PoP (chance of precipitation)
- when percentage is >=70 (default: $popcol70)
- popbg30 - - - - (hex) background color for PoP when percentage is >=30
- (default: $popbg30)
- popbg70 - - - - (hex) background color for PoP when percentage is >=70
- (default: $popbg70)
- font - - - - - - (str) font to use for all text (default: $font)
- Default: options=''
- -g Include wind gusts in forecast
- (comes from a different source: $urlbase_gusts)
- Default: gusts=$gusts
- -p Convert images to PNG instead of using native SVG
- Default: usepng=$usepng
- -r Remove cached weather data first
- EOF
- exit 0
- }
- ex_err() {
- [ -n "$opt" ] && printf '%s' "-${opt}: "
- [ -n "$*" ] && printf '%s\n' "$*"
- printf 'Try %s -h\n%b' "$0"
- exit 1
- }
- dl_smartsymbol() {
- # Check if smartsymbol SVG exists, otherwise download it, then check again
- # $1 full path to desired output
- [ -r "$1" ] && return 0
- curl --output "$1" --user-agent "$useragent" "$urlbase_img_fc/${1##*/}" >&2
- [ -r "$1" ] || usage "Oof. Could not download required image $1 from $urlbase_img_fc/${1##*/}"
- }
- dl_interval() {
- # $1: file to download to
- # $2: URL
- # will download a file if its age starts with the previous full hour
- # meaning: the threshold to re-download is not a full hour, but the start of a new hour
- if ! [ -r "$1" ]; then
- echo "Downloading for the first time: $2" >&2
- curl --user-agent "$useragent" -o "$1" "$2" >&2 || usage "Something went wrong. Bailing."
- elif check_hour "$1"; then
- echo "Updating $1 - downloading: $2" >&2
- curl --user-agent "$useragent" -o "$1" "$2" >&2 || usage "Something went wrong. Bailing."
- else
- echo "No new hour has started & file $1 exists." >&2
- fi
- }
- get_ss_text() {
- # get forecast text according to smartsymbol, from symbol_$lang file
- # $1: smartsymbol number
- while read line; do
- [[ "$line" == "$1"* ]] && RETVAL="${line#*:}" && break
- done <"$symbol_text$lang"
- }
- make_wind_svg() {
- # replaces strings in SVG data string with calculated values
- wind_svg="${wind_svg_base/@wind_deg@/$((wind_deg-180))}"
- wind_svg="${wind_svg//@circle_color@/$circle_color}"
- wind_svg="${wind_svg//@arrow_color@/$arrow_color}"
- }
- check_hour() {
- # $1: file to check
- # just return whether the current hour is at least 1 more than the file's age
- # because forecasts are updated on the full hour
- (( "$HOUR" > "$(date '+%y%m%d%H' --date "$(stat -c '%y' "$1")")" ))
- }
- reset="\e[0;0m" # reset terminal to normal colours
- me="${0##*/}"
- HOUR="$(date '+%y%m%d%H')" # the current hour, to decide whether re-downloading is required
- urlbase="https://widget.weatherproof.fi/android/androidwidget.php"
- urlbase_gusts="https://opendata.fmi.fi"
- urlbase_img_fc="https://cdn.fmi.fi/symbol-images/smartsymbol/v31/p"
- RETVAL='' # global variable to store functions' return values
- deps=( "curl:" "required," "jshon:" "required," "stat:" "required," "date:" "required," "sed:" "for conky formatted output," "rsvg-convert:" "to convert SVGs to PNGs" )
- max_index=220 # That many forecasts are contained in the json
- # files, storage, static and changing data
- files="${0%/*}/files"
- storage="$files/${me}.d"
- conf="$storage/conf"
- mkdir -p "$storage"
- symbol_text="$files/symbol_text_" # 2-letter language code will be appended
- geoid=658225 # Helsinki
- lang=en
- gusts=0
- indices="0,1,3,6,10,15,21,28,36,45,55"
- format=''
- usepng=0
- ### options (see help text for -o option) ###
- options=''
- ### what options haven't been set by users will be set here
- width=330
- padding_h=10
- offset=30
- img_size_fc=64
- img_size_wind=30
- tempcol_plus="ee0000"
- tempcol_minus="1e90ff"
- textcol="303193"
- linecol="888888"
- font="Roboto Condensed"
- popcol70="ffffff"
- popbg30="A1C8E6"
- popbg70="3A66E3"
- # wind_svg_base must contain the invalid string "@wind_deg@" which will be replaced with the actual rotation, also
- # @arrow_color@ and @circle_color@
- #~ wind_svg_base='<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"><g fill="none" fill-rule="evenodd" transform="rotate(@wind_deg@ 15 15)"><circle cx="15" cy="15" r="10" fill="@circle_color@" stroke="@arrow_color@" stroke-width="2"/><path fill="@arrow_color@" fill-rule="nonzero" d="M14.999 5L20 12 16 12 16 25 14 25 14 11.999 10 11.999z"/></g></svg>'
- # without stroke
- wind_svg_base='<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"><g fill="none" fill-rule="evenodd" transform="rotate(@wind_deg@ 15 15)"><circle cx="15" cy="15" r="13" fill="@circle_color@" /><path fill="@arrow_color@" fill-rule="nonzero" d="M14.999 5L20 12 16 12 16 25 14 25 14 11.999 10 11.999z"/></g></svg>'
- # Mobile User Agent
- # curl "https://www.whatismybrowser.com/guides/the-latest-user-agent/android" | xmllint --html --nonet --xpath "//table//li//text()" - 2>/dev/null | head -1
- useragent="Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.54 Mobile Safari/537.36"
- printf '%b' "\e[33m" >&2 # change color to yellow until optionparsing & data aquisition is done
- # dependency check
- for ((i=0;i<${#deps[@]};i+=2)); do
- type -f ${deps[i]%:} >/dev/null 2>&1 || { [[ "${deps[i+1]%,}" == "required" ]] && usage "Missing dependency: ${deps[i]%:}" || echo "Missing optional dependency: ${deps[i]%:} (${deps[i+1]%,})" >&2; }
- done
- [ -r "$conf" ] && source "$conf"
- while getopts "L:l:i:f:o:gprh" opt; do
- case $opt in
- L) [[ "$OPTARG" =~ [0-9]+ ]] || ex_err "Invalid location ID: $OPTARG"
- geoid="$OPTARG"
- ;;
- l) [[ "$OPTARG" =~ ^(fi|en|sv)$ ]] || ex_err "Invalid language: $OPTARG"
- lang="$OPTARG"
- ;;
- i) { [[ "${OPTARG//,/}" =~ ^[0-9]+$ ]] && [[ "$OPTARG" =~ ^[0-9].*[0-9]$ ]]; } || ex_err "-${opt}: invalid option $OPTARG"
- indices="$OPTARG"
- ;;
- f) [[ "$OPTARG" =~ ^[0-9a-zA-Z]+$ ]] || ex_err "invalid option $OPTARG"
- format="$OPTARG"
- ;;
- o) options="$OPTARG"
- ;;
- g) gusts=1
- ;;
- p) usepng=1
- ;;
- r) rm -f "$storage/"*.{json,xml}
- ;;
- h) usage
- ;;
- *) unset opt; ex_err
- ;;
- esac
- done
- max="${indices##*,}"
- file_forecast="$storage/forecast-$geoid.json"
- file_gusts="$storage/gusts-$geoid.xml"
- if [ -n "$options" ]; then
- # convert -o "options" string to actual variables
- oldifs="$IFS"; IFS=, options=( $options ); IFS="$oldifs"
- for ((i=0;i<${#options[@]};i++));do
- var="${options[i]%%=*}"
- val="${options[i]##*=}"
- declare "$var"="$val" || {
- opt=o ex_err" Could not set variable \"$var\" to value \"$val\"."
- }
- done
- fi
- # variables that need to be set after parsing options
- forecast_height="${forecast_height-"$img_size_fc"}"
- # declare this array even if it isn't used in the end
- declare -A wg # windgusts indexed by local timestamps
- ###########################
- ####### DATA AQUISITION ###
- ### Load gusts XML and extract values ###
- if [[ "$gusts" == 1 ]]; then
- # type of forecast, get a list with ./wfs_describeStoredQueries
- #Harmonie Point Weather Forecast as multipointcoverage (fmi::forecast::harmonie::surface::point::multipointcoverage)
- #Harmonie Point Weather Forecast as simple features (fmi::forecast::harmonie::surface::point::simple)
- #Harmonie Point Weather Forecast as time value pairs (fmi::forecast::harmonie::surface::point::timevaluepair)
- query="fmi::forecast::harmonie::surface::point::simple"
- wfs_req="wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id"
- starttime="$(date -u '+%FT%R:%SZ')" # now, in UTC
- endtime="$(date -u '+%FT%R:%SZ' --date="$((max + 1)) hours")" # maximum results. Have to indcrease by 1 becasue forecasts are always on the full hour.
- timestep=60 # always 60 minutes, because that's what the mobile service offers
- URL="$urlbase_gusts/$wfs_req=${query}&geoid=${geoid}&starttime=${starttime}&endtime=${endtime}×tep=${timestep}¶meters=windgust"
- #~ curl -s -o "$file_xml" "$URL"
- #~ curl -s -o "$file_forecast" --user-agent "$useragent" "${urlbase}?l=${lang}&locations=${geoid}"
- #~ exit
- dl_interval "$file_gusts" "$URL"
- xml="$(<"$file_gusts")"
- while read line; do
- # avoid dependency for XML parsing, quick'n'dirty line by line instead:
- [[ "$line" == *'<BsWfs:Time>'* ]] && {
- index="${line#*>}"
- index="${index%%<*}"
- index="$(date '+%Y%m%dT%H%M%S' --date "$index")"
- continue
- }
- [[ "$line" == *'<BsWfs:ParameterValue>'* ]] && {
- line="${line#*>}"
- line="${line%%<*}"
- { [[ "${line#*.}" == "0"* ]] || (( ${line#*.} < 50 )); } && line=${line%.*} || line=$(( ${line%.*} + 1 ))
- wg[$index]="$line"
- }
- done <<<"$xml"
- fi
- ### Load weather forecast JSON ###
- URL="${urlbase}?l=${lang}&locations=${geoid}"
- dl_interval "$file_forecast" "$URL"
- json="$(<"$file_forecast")"
- ### Associative array to house all forecasts' key/value pairs
- declare -A output
- # This we need only once, the name of the place, and its region:
- name="$(jshon -e forecasts -e 0 -e forecast -e 0 -e name -u -p -e region -u <<<"$json")"
- region="${name##*$'\n'}"
- name="${name%%$'\n'*}"
- # This we need for every hourly forecast index:
- keys=(localtime Temperature FeelsLike SmartSymbol PoP WindSpeedMS WindDirection WindCompass8 Precipitation1h)
- units=("" "°C" "°C" "" "%" "m/s" "°" "" "mm") # applied later
- for ((i=0;i<${#keys[@]};i++)); do
- j=0
- while read line; do
- output[${keys[i]} $((j++))]="$line"
- done <<<"$(jshon -Q -e forecasts -e 0 -e forecast -a -e "${keys[i]}" -u <<<"$json")"
- done
- printf '%b' "$reset" >&2 # reset colors
- ################################################################################
- ####### DATA OUTPUT & FORMATTING #############
- indices=( ${indices//,/ } ) # transform comma-separated list to array
- declare -A weekdays=(
- [en1]=Mon [en2]=Tue [en3]=Wed [en4]=Thu [en5]=Fri [en6]=Sat [en7]=Sun
- [en1long]=Monday [en2long]=Tuesday [en3long]=Wednesday [en4long]=Thursday [en5long]=Friday [en6long]=Saturday [en7long]=Sunday
- [fi1]=ma [fi2]=ti [fi3]=ke [fi4]=to [fi5]=pe [fi6]=la [fi7]=su
- [fi1long]=maanantai [fi2long]=tiistai [fi3long]=keskiviikko [fi4long]=torstai [fi5long]=perjantai [fi6long]=lauantai [fi7long]=sunnuntai
- [sv1]=mån [sv2]=tis [sv3]=ons [sv4]=tor [sv5]=fre [sv6]=lör [sv7]=sön
- [sv1long]=måndag [sv2long]=tisdag [sv3long]=onsdag [sv4long]=torsdag [sv5long]=fredag [sv6long]=lördag [sv7long]=söndag
- )
- long="" # set to "long" to use long names
- upper=0 # set to 0 to use days as they are, without capitalizing them
- if [ -z "$format" ]; then
- ### Simple CLI output ###
- sep="⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯"
- ### HEADING ###
- printf '%s, %s\n%s\n' "$name" "$region" "$sep"
- ### FORECAST ###
- for i in ${indices[@]}; do
- for ((j=0;j<${#keys[@]};j++)); do
- [[ "${keys[j],,}" == smartsymbol ]] && {
- # get weather icon description from symbol_ file
- get_ss_text "${output[${keys[j]} $i]}"
- echo "${keys[j]}: $RETVAL"
- continue
- }
- echo "${keys[j]}: ${output[${keys[j]} $i]}${units[j]}"
- done
- [[ "$gusts" == 1 ]] && echo "WindGusts: ${wg[${output[localtime $i]}]}"
- echo "$sep"
- done
- else
- ### Output with images etc. (conky) ###
- ### check for or create different subfolders for SVG and PNG images
- smartsymbols_png="$storage/smartsymbol_png_$img_size_fc"
- windsymbols_png="$storage/windsymbol_png_$img_size_wind"
- smartsymbols_svg="$storage/smartsymbol_svg"
- windsymbols_svg="$storage/windsymbol_svg"
- mkdir -p "$smartsymbols_svg" "$windsymbols_svg" || ex_err "Could not find or create subfolders in \"$storage\""
- [[ "$usepng" == 1 ]] && { mkdir -p "$smartsymbols_png" "$windsymbols_png" || ex_err "Could not find or create subfolders in \"$storage\""; }
- template_header="$files/header.$format"
- template_forecast="$files/forecast.$format"
- template_footer="$files/footer.$format"
- # /^[ \t]*#/d; /^[ \t]*--/d = remove comments
- sed_script_base="/^[ \t]*--/d; /^[ \t]*#/d
- s|%textcol%|$textcol|g
- s|%linecol%|$linecol|g
- s|%padding_h%|$padding_h|g
- s|%font%|$font|g
- s|%img_size_fc%|$img_size_fc|g
- s|%img_size_wind%|$img_size_wind|g
- s|%width%|$width|g
- s|%forecast_height%|$forecast_height|g"
- ### HEADER ###
- [ -r "$template_header" ] && sed "$sed_script_base
- s|%offset%|$offset|g
- s|%name%|$name|
- s|%region%|$region|" "$template_header"
- ### FORECASTS ###
- [ -r "$template_forecast" ] && { # all of this only happens if there's a template file to iterate this over
- oldday="${output[localtime ${indices[0]}]:4:4}"
- for i in ${indices[@]}; do
- time="${output[localtime $i]:9:2}:${output[localtime $i]:11:2}"
- day="${output[localtime $i]:4:4}"
- # if a new day begins, show it as a weekday, not a numerical date.
- if [[ "$day" != "$oldday" ]]; then
- oldday="$day"
- day="$(date +%u --date="${output[localtime $i]:0:8}")"
- day="${weekdays[$lang$day$long]}"
- [[ "$upper" == 1 ]] && day="${day^}"
- else
- day=""
- fi
- # smartsymbol (an integer)
- ss="${output[SmartSymbol $i]}"
- # get forecast text corresponding to smartsymbol
- get_ss_text "$ss"
- ss_text="$RETVAL"
-
- # adjust temperature color depending on temperature plus or minus
- (( ${output[Temperature $i]} < 0 )) && tempcol="$tempcol_minus" || tempcol="$tempcol_plus"
-
- ### now prepare images & colors ###
- # rotate wind svg (data)
- wind_deg="${output[WindDirection $i]}"
- wind_speed="${output[WindSpeedMS $i]}"
- # rounding up/down to end on 0, so that we have only 36 directions instead of 360
- (( ${wind_deg: -1} >= 5 )) && wind_deg="$(( ${wind_deg%?} + 1 ))0" || wind_deg="${wind_deg%?}0"
- # colorize SVG acccording to wind strength
- if (( wind_speed >= 21 )); then
- circle_color="#000000"; arrow_color="#ffffff"; level="level-04"
- elif (( wind_speed >= 14 )); then
- circle_color="#303193"; arrow_color="#ffffff"; level="level-03"
- elif (( wind_speed >= 8 )); then
- circle_color="#3A66E3"; arrow_color="#ffffff"; level="level-02"
- elif (( wind_speed >= 1 )); then
- circle_color="#E7F0FA"; arrow_color="#303193"; level="level-01"
- else
- circle_color="#E7F0FA"; arrow_color="#E7F0FA"; level="level-00"
- fi
- # colors for PoP background box & text
- #~ pop="${output[PoP $i]}"
- #~ if (( pop >= 70 )); then
- #~ popbg="$popbg70"
- #~ popcol="$popcol70"
- #~ elif (( pop >= 30 )); then
- #~ popbg="$popbg30"
- #~ popcol="$textcol"
- #~ else
- #~ popbg="x"
- #~ popcol="$textcol"
- #~ fi
- # colors for PoP text (no background box)
- pop="${output[PoP $i]}"
- if (( pop >= 70 )); then
- popbg="x"
- popcol="$textcol"
- elif (( pop >= 30 )); then
- popbg="x"
- popcol="$popbg70"
- else
- popbg="x"
- popcol="$popbg30"
- fi
- si_svg="$smartsymbols_svg/$ss.svg"
- if [[ "$usepng" == 1 ]]; then
- # convert things to PNG
- wind_img="$windsymbols_png/deg${wind_deg}-$level.png"
- [ -r "$wind_img" ] || {
- make_wind_svg
- rsvg-convert -o "$wind_img" --page-width="$img_size_wind" --page-height="$img_size_wind" --top=$(((img_size-30)/2)) --left=$(((img_size-30)/2)) <<<"$wind_svg"
- }
- smart_img="$smartsymbols_png/$ss.png"
- [ -r "$smart_img" ] || {
- dl_smartsymbol "$si_svg"
- rsvg-convert -o "$smart_img" --width="$img_size_fc" --height="$img_size_fc" "$si_svg"
- }
- else
- # use SVGs
- dl_smartsymbol "$si_svg"
- smart_img="$si_svg"
- wind_img="$windsymbols_svg/deg${wind_deg}-$level.svg"
- [ -r "$wind_img" ] || {
- make_wind_svg
- echo "$wind_svg" > "$wind_img"
- }
- fi
- # If gust is the same as windspped, it is not required
- gust="${wg[${output[localtime $i]}]}"
- [[ "$gust" == "$wind_speed" ]] || wind_speed="$wind_speed-$gust"
- ### now translate $format templates
- sed "$sed_script_base
- s|%localtime%|$time|
- s|%day%|$day|
- s|%Temperature%|${output[Temperature $i]}${units[1]}|
- s|%FeelsLike%|${output[FeelsLike $i]}${units[2]}|
- s|%PoP%|$pop${units[4]}|
- s|%Precipitation1h%|${output[Precipitation1h $i]}|
- s|%WindSpeedMS%|$wind_speed|
- s|%WindSpeedUnit%|${units[5]}|
- s|%WindCompass8%|${output[WindCompass8 $i]}|
- s|%wind_img%|$wind_img|; s|%offset%|$offset|g
- s|%smart_img%|$smart_img|
- s|%tempcol%|$tempcol|g
- s|%popbg%|$popbg|g
- s|%popcol%|$popcol|g
- s|%ss_text%|$ss_text|; s|%offset%|$offset|g" "$template_forecast"
-
- ### increase offset
- offset=$(( offset + forecast_height ))
- done
- } # END [ -r "$template_forecast" ] &&
- ### FOOTER ###
- [ -r "$template_footer" ] && sed "$sed_script_base; s|%offset%|$offset|g; s|%offset_file%|$storage/offset|g" "$template_footer"
- fi
|