summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-01-13 22:16:54 +0100
committertv <tv@krebsco.de>2026-01-13 22:16:54 +0100
commite064bc363416051ef0f9fe53c6f602509a571309 (patch)
tree82f94bcc8e9051298b8c8d0e5472cb4dad02159f
boom
-rw-r--r--default.nix19
-rwxr-xr-xwetter379
2 files changed, 398 insertions, 0 deletions
diff --git a/default.nix b/default.nix
new file mode 100644
index 0000000..b1a89f0
--- /dev/null
+++ b/default.nix
@@ -0,0 +1,19 @@
+{ lib, pkgs }:
+
+pkgs.runCommand "wetter-1.0" {
+} /* sh */ ''
+ ${pkgs.coreutils}/bin/mkdir -p $out/bin
+ ${pkgs.gnused}/bin/sed \
+ 's@^#!buildShellBin .*@export PATH=${lib.makeBinPath [
+ pkgs.attr
+ pkgs.coreutils
+ pkgs.curl
+ pkgs.gawk
+ pkgs.gnuplot
+ pkgs.gnused
+ pkgs.jq
+ ]}@' \
+ < ${./wetter} \
+ > $out/bin/wetter
+ ${pkgs.coreutils}/bin/chmod +x $out/bin/wetter
+''
diff --git a/wetter b/wetter
new file mode 100755
index 0000000..0fb3407
--- /dev/null
+++ b/wetter
@@ -0,0 +1,379 @@
+#! /bin/sh
+#!buildShellBin path=attr:coreutils:curl:gawk:gnuplot:gnused:jq
+#
+# NAME
+# wetter - client for api.weather.mg
+#
+# SYNOPSIS
+# wetter [LONGITUDE,LATITUDE]
+#
+# ENVIRONMENT
+# WETTER_CACHE_DIR
+# If set, the specified location will be used to cache HTTP
+# responses.
+#
+# WETTER_API_ROOT_URL
+# WETTER_TOKEN_URL
+#
+# WETTER_LOCATION
+#
+# SEE ALSO
+# API documentation: https://api.weather.mg/
+#
+
+set -efu
+
+# main [LONGITUDE,LATITUDE]
+main() {
+ locatedAt=${WETTER_LOCATION-13.42013,52.51297} # default: c-base
+
+ if test $# = 1; then
+ locatedAt=$1
+ fi
+
+ token_url=${WETTER_TOKEN_URL-https://api.weatherpro.com/v1/token/weather}
+ api_root_url=${WETTER_API_ROOT_URL-https://point-forecast-weatherpro.meteogroup.com}
+
+ cache_dir=${WETTER_CACHE_DIR-}
+ if test -z "$cache_dir"; then
+ debug "no cache directory set: caching disabled"
+ token_cache=
+ elif ! test -d "$cache_dir"; then
+ debug "cache directory $cache_dir does not exist: caching disabled"
+ token_cache=
+ else
+ token_cache=$WETTER_CACHE_DIR/api.weatherpro.com%1Fv1%1Ftoken%1Fweather
+ fi
+
+ token=$(get_token "$token_url" "$token_cache")
+
+ fields=$(jq -nr '
+ [
+ #"meteoGroupStationId",
+ "maxAirTemperatureInCelsius",
+ "minAirTemperatureInCelsius",
+ "precipitationAmountInMillimeter",
+ "sunshineDurationInMinutes",
+ ""
+ ] |
+ join(",")
+ ')
+
+ validFrom=$(date -d 'TZ="Europe/Berlin" - 48 hours' --utc +'%Y-%m-%dT%H:%M:%S.%3NZ')
+ validUntil=$(date -d "TZ=\"Europe/Berlin\" + $(( 5 * 24 )) hours" --utc +'%Y-%m-%dT%H:%M:%S.%3NZ')
+
+ t0=$(date -d 'TZ="Europe/Berlin" - 0 days 00:00' +'%Y-%m-%dT%H:%M:%S.%3NZ')
+ t1=$(date -d 'TZ="Europe/Berlin" + 4 days 00:00' +'%Y-%m-%dT%H:%M:%S.%3NZ')
+
+ curl -fsS \
+ -H "authorization: Bearer $token" \
+ -o /tmp/forecast.json \
+ "https://point-forecast-weatherpro.meteogroup.com/search?fields=$fields&locatedAt=$locatedAt&validPeriod=PT0S,PT1H,PT3H,PT6H,PT12H,PT24H&validFrom=$validFrom&validUntil=$validUntil&timezone=UTC"
+
+ #curl -fsS \
+ # -H "authorization: Bearer $token" \
+ # -o /tmp/forecastPT3H.json \
+ # "https://point-forecast-weatherpro.meteogroup.com/search?fields=$fieldsPT3H&locatedAt=$locatedAt&validPeriod=PT3H&validFrom=$validFrom&validUntil=$validUntil&timezone=UTC"
+
+ #curl -fsS \
+ # -H "authorization: Bearer $token" \
+ # -o /tmp/forecastPT6H.json \
+ # "https://point-forecast-weatherpro.meteogroup.com/search?fields=$fieldsPT6H&locatedAt=$locatedAt&validPeriod=PT6H&validFrom=$validFrom&validUntil=$validUntil&timezone=UTC"
+
+
+ < /tmp/forecast.json > /tmp/forecast-temperatures.tsv \
+ jq -r '
+ .forecasts[] |
+ select((.maxAirTemperatureInCelsius != null) and (.minAirTemperatureInCelsius != null) and (.validPeriod == "PT3H")) |
+ ( (.maxAirTemperatureInCelsius + .minAirTemperatureInCelsius) / 2 ) as $avg |
+ [.validFrom, .minAirTemperatureInCelsius, .maxAirTemperatureInCelsius, $avg, $avg ] |
+ @tsv
+ '
+
+ #jq -r '.forecasts[] | [.validFrom, .validUntil, .precipitationAmountInMillimeter, .sunshineDurationInMinutes] | @tsv' /tmp/forecastPT6H.json > /tmp/forecastPT6H.tsv
+
+ jq -r '
+ .forecasts |
+ map(select((.precipitationAmountInMillimeter != null) and (.validPeriod == "PT1H")))[] |
+ [.validFrom, .validUntil, .precipitationAmountInMillimeter] |
+ @tsv
+ ' /tmp/forecast.json > /tmp/forecast-precipitation.tsv
+
+ jq -r '
+ .forecasts |
+ map(select((.sunshineDurationInMinutes != null) and (.validPeriod == "PT1H")))[] |
+ [.validFrom, .validUntil, .sunshineDurationInMinutes] |
+ @tsv
+ ' /tmp/forecast.json > /tmp/forecast-sunshine.tsv
+
+ now=$(date -Is)
+
+ lines=$(
+ lines=$((LINES - 2))
+ max_lines=40
+ min_lines=20
+ if test $lines -gt $max_lines; then
+ echo $max_lines
+ elif test $lines -lt $min_lines; then
+ echo $min_lines
+ else
+ echo $lines
+ fi
+ )
+
+ # Ideally we would like 1 column per hour
+ # 48+5*24 = 168
+ # + 2*2 + 2*4 # y markings
+ # + 13 # colorbox
+ columns=$(
+ columns=$COLUMNS
+ max_columns=200
+ min_columns=80
+ if test $columns -gt $max_columns; then
+ echo $max_columns
+ elif test $columns -lt $min_columns; then
+ echo $min_columns
+ else
+ echo $columns
+ fi
+ )
+
+ #| sed 's/-/─/g; s/|/│/g; s/+/┼/g'
+ gnuplot <<EOF | sed '1d'
+ set terminal dumb size $columns,$lines enhanced aspect 2,1 ansirgb
+
+ #set terminal sixelgd enhanced truecolor butt size $((COLUMNS * 3)),$(( (LINES-1) * 13))
+
+ #set terminal pngcairo size 1200,600 background rgb "#404040"
+ #set output "/tmp/check.png"
+
+ #set key right top
+ unset key
+
+ # Compute stats for column 2 (Min Temp)
+ ## Don't try to stats the time column, only the numeric ones
+ #stats '/tmp/temperatures.tsv' using 2 name 'MIN' nooutput
+ #stats '/tmp/temperatures.tsv' using 3 name 'MAX' nooutput
+
+ unset border
+ #unset ytics
+
+ #replot
+ #set output
+
+
+
+
+
+ set xdata time
+ set timefmt "%Y-%m-%dT%H:%M:%S+01:00"
+ #set format x "%m-%d %Hh"
+ set format x "%m-%d %a"
+ #set format x "%H"
+ #set xtics $t0, 60, $t1 # 24h spacing
+ #set xtics $t0, 86400
+ ##set mxtics 4
+
+ unset ydata
+ unset y2data # critical: ensures right axis is numeric
+ set y2range [0:10] # force correct numeric range
+
+ # Major tics every 24h (86400 seconds)
+ set xtics "$t0", 86400, "$t1"
+ #set xtics "$t0", 10800
+ #set xtics "$t0", 43200
+
+ set y2tics
+ set y2range [0:60] # minutes of sunshine per hour
+ #set format y2 "%g"
+
+ #set boxwidth 21600 absolute # 6 hours = 6*3600 = 21600 seconds
+ set boxwidth 3600 absolute # PT1H
+ set style fill solid
+
+
+
+ # Minor tics: 3h = 10800 seconds
+ set mxtics 4 # 86400 / 10800 = 8 minor divisions per day
+
+ #set title "Forecast Temperatures"
+ #set timefmt "%Y-%m-%dT%H:%M:%S+01:00"
+ #set format x "%m-%d\n%Hh"
+ #set xtics rotate
+ #set ylabel "°C"
+ #set xlabel "Time"
+
+ set style line 1 lc rgb "#FF6666"
+ set style line 2 lc rgb "#6666FF"
+
+
+ set style line 5 lc rgb "#707070" #lw 1 dt solid
+ set style line 6 lc rgb "#303030" #lw 1 dt 2
+ set style line 7 lc rgb "magenta" #lw 1 dt 2
+
+ set ytics nomirror
+
+ set grid ytics ls 7
+ set grid xtics mxtics ls 5, ls 6
+
+
+ # Light rain/drizzle:
+ # Often between 0.1–1.0 mm per hour.
+ # Common in autumn and winter frontal systems.
+ #
+ # Moderate rain:
+ # Around 1–5 mm per hour.
+ # Typical of steady showers or longer rain events.
+ #
+ # Heavy rain (convective storms):
+ # Can reach 10–20 mm per hour.
+ # Thunderstorms in summer can briefly exceed this.
+ #
+ # Extreme downpours:
+ # Localized convective cells may produce >30 mm per hour, though this is rare.
+ # Such events are usually short-lived but can cause flash flooding.
+ precipitation_scaling_factor = 20
+
+ scale_precipitation(x) = x * precipitation_scaling_factor
+
+ set y2tics ()
+ do for [i=0:60:10] {
+ set y2tics add (sprintf(" %1.1f", i * 100.0 / precipitation_scaling_factor / 100.0) i)
+ }
+
+ #set style fill solid 0.4 noborder
+
+
+ #set arrow from graph 0, first 0 to graph 1, first 0 nohead lc rgb "red"
+
+ set arrow from "$now", graph 0 \
+ to "$now", graph 1 \
+ nohead lc rgb "red"
+
+ set palette defined (-30 "black", -20 "purple", -10 "blue", 0 "cyan", 10 "green", 20 "yellow", 25 "orange", 35 "red", 45 "pink", 50 "white")
+ #set palette defined ( -20 "#8B00FF", 0 "#0000FF", 20 "#FFA500", 40 "#FF0000" ) # classic cold→hot
+ #set palette defined ( -20 "#00FFFF", 0 "#FFFFFF", 20 "#FFFF00", 40 "#B22222" ) # ice → neutral → heat
+ #set palette defined ( -20 "#00008B", -10 "#87CEFA", 0 "#00FF00", 20 "#FFD700", 40 "#FF4500" ) # mimics meteorological maps
+ set cbrange [-30:50]
+ #set cbextend # values < -20 stay violet, > 40 stay red
+
+ # hide palette
+ unset colorbox
+
+ # Enable pm3d for smooth fills (no border, 50% opacity for band effect)
+ #set pm3d map flush begin interpolate 2,2 # 'interpolate 2,2' smooths the band
+ #set style pm3d solid opacity 0.6 noborder
+
+ #set style fill solid 0.5 noborder
+
+ # 40E0D0 more luminous
+ # 008B8B darker, more subdued
+ # 00CED1 DarkTurquoise
+ plot \
+ '/tmp/forecast-sunshine.tsv' using 1:3:1:2:(0):3 axes x1y2 with boxxyerror lc rgb "yellow" title 'Sunshine (minutes)', \
+ \
+ '/tmp/forecast-temperatures.tsv' using 1:2:(column(2)) with linespoints lc palette title 'Min Temp', \
+ '' using 1:3:(column(3)) with linespoints lc palette title 'Max Temp', \
+ '/tmp/forecast-precipitation.tsv' using 1:(scale_precipitation(\$3)):1:2:(0):(scale_precipitation(\$3)) axes x1y2 with boxxyerror lc rgb "#A0C0D0" title 'Precipitation (mm)'
+EOF
+ return
+ cat<<\EOF
+ # sonne von oben
+ '/tmp/forecast-sunshine.tsv' using 1:(60 - \$3):1:2:(60):(60 - \$3) axes x1y2 with boxxyerror lc rgb "yellow" title 'Sunshine (minutes)', \
+
+
+ #'/tmp/forecast-precipitation.tsv' using 1:3 axes x1y2 with linespoints lc rgb "#A0C0D0" title 'Precipitation (mm)', \
+ #'/tmp/forecast-temperatures.tsv' using 1:2:3 with filledcurves fc '#202020' notitle, \
+ #'/tmp/forecastPT3H.tsv' using 1:2:3 with filledcurves notitle, \
+ #using 1:2:3 with filledcurve lc palette notitle, \
+ #using 1:2 with linespoints ls 2 title 'Min Temp', \
+ # '' using 1:3 with linespoints ls 1 title 'Max Temp'
+ #'' using 1:2 every ::MIN_index_min::MIN_index_min with points pt 7 lc rgb "#ccccff" title "Abs Min", \
+ #'' using 1:3 every ::MAX_index_max::MAX_index_max with points pt 7 lc rgb "#ffcccc" title "Abs Max", \
+ #
+
+EOF
+}
+
+
+
+debug() {
+ echo "$*" >&2
+}
+
+error() {
+ echo "$0: error: $1" >&2
+ return 1
+}
+
+
+# get_header_field FIELD_NAME < HEADER
+# Read headers as dumped by curl from stdin and print the selected header field
+# content to stdout.
+get_header_field() {
+ split_at ' *\r *' | awk -F ': ' -v key="$1" '$1==key {print $2}'
+}
+
+# get_field DELIMITER FIELD_NAME
+get_field() {
+ awk -F "$1" -v name="$2" '$1==name {print $2}'
+}
+
+# Split stdin into lines
+# TODO escape ERE magic and /
+split_at() {
+ sed -r "s/$1/\\n/g"
+}
+
+get_max_age() {
+ split_at ' *\r *' | get_field ' *: *' cache-control | split_at ' *, *' | get_field = max-age
+}
+
+# get_token URL CACHE_PATH
+get_token() {
+ if test -n "$2"; then
+ if is_fresh_token "$2"; then
+ debug "token is fresh"
+ cat "$2"
+ else
+ debug "refresh token"
+ if refresh_token "$1" "$2"; then
+ cat "$2"
+ else
+ error "failed to refresh token"
+ fi
+ fi
+ else
+ debug "fetching token" # caching disabled
+ fetch_token "$1"
+ fi
+}
+
+# is_fresh_token PATH
+is_fresh_token() {
+ test -e "$1" &&
+ (
+ max_age=$(attr -q -g max-age "$1")
+ mtime=$(stat -c %Y "$1")
+ now=$(date +%s)
+ test $((mtime + max_age)) -gt $now
+ )
+}
+
+# refresh_token URL CACHE_PATH
+refresh_token() {
+ (
+ header=$(curl -fsS "$1" -o "$2" -D-)
+ #max_age=$(echo $header split_at ' *\r *' | get_field ' *: *' cache-control | split_at ' *, *' | get_field = max-age)
+
+ max_age=$(echo $header | split_at '\r ' | get_field ': ' cache-control | split_at ', ' | get_field = max-age)
+ attr -q -s max-age -V "$max_age" "$2"
+ )
+}
+
+# fetch_token URL
+fetch_token() {
+ curl -fsS "$1"
+}
+
+main "$@"