From 9e1087f875e3effa599face85079ee762dd4bf68 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Apr 2026 18:40:27 +0000 Subject: [PATCH] Tag ambient audio chunks with current OpenWeather conditions Adds weather-tag.sh, a small failure-safe helper that queries OpenWeather's free "Current weather" endpoint and prints one tag from a fixed vocabulary (thunderstorm, rain, rainshower, snow, snowstorm, fog, windy, cloudy, clear) or nothing on any failure. ambient-record.sh calls it just before each 30-minute chunk so the filename reflects conditions at the moment of capture, e.g. east-08-30-00-thunderstorm.m4a east-12-00-00-clear.m4a A foggy dawn no longer mislabels the sunny afternoon files. Off by default. Enable with WEATHER_ENABLED=true and an OPENWEATHER_API_KEY entry in .env; missing key / API error / disabled toggle all fall through to the un-tagged filename, never blocking the recorder. --- ambient-record.sh | 12 ++++++- sky-cam.conf | 32 +++++++++++++++++ weather-tag.sh | 87 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100755 weather-tag.sh diff --git a/ambient-record.sh b/ambient-record.sh index 6e296b6..31c5626 100755 --- a/ambient-record.sh +++ b/ambient-record.sh @@ -50,7 +50,17 @@ while true; do mkdir -p "$dir" timestamp=$(date +%H-%M-%S) - outfile="$dir/${CAM}-${timestamp}.m4a" + + # Weather tag for THIS chunk — queried right before recording so the + # filename reflects conditions at the time of capture, not earlier in the + # day. weather-tag.sh prints empty on disabled / missing key / API error, + # so the un-tagged filename is the safe fallback. + weather_tag=$("$SCRIPT_DIR/weather-tag.sh" 2>/dev/null || true) + if [ -n "$weather_tag" ]; then + outfile="$dir/${CAM}-${timestamp}-${weather_tag}.m4a" + else + outfile="$dir/${CAM}-${timestamp}.m4a" + fi echo "[$(date '+%Y-%m-%d %H:%M:%S')] Recording → $outfile (${CHUNK}s)" diff --git a/sky-cam.conf b/sky-cam.conf index f6b0819..550b21c 100644 --- a/sky-cam.conf +++ b/sky-cam.conf @@ -174,6 +174,38 @@ LATITUDE="${LATITUDE:-0.0000}" LONGITUDE="${LONGITUDE:-0.0000}" TIMEZONE="${TIMEZONE:-America/New_York}" +# ── Weather tagging (OpenWeather) ───────────────────────────────────────────── +# When enabled, ambient-record.sh queries OpenWeather's "Current weather" +# endpoint just before each 30-minute audio chunk and appends a one-word +# weather tag to the filename, e.g.: +# east-08-30-00-thunderstorm.m4a +# east-09-00-00-rain.m4a +# east-09-30-00-cloudy.m4a +# That way each chunk is labelled with what was happening DURING that chunk — +# a foggy dawn won't mislabel the sunny afternoon files. +# +# Tag vocabulary (fixed set — anything else → no tag): +# thunderstorm rain rainshower snow snowstorm +# fog windy cloudy clear +# +# Setup: +# 1. Get a free API key: https://openweathermap.org/api +# (the free "Current weather" tier — 60 calls/min — is plenty) +# 2. Add to .env: OPENWEATHER_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxx +# 3. Confirm LATITUDE / LONGITUDE are set in .env (already needed by sunrise.py) +# 4. Flip WEATHER_ENABLED=true below +# +# The query uses the same LATITUDE / LONGITUDE configured above — one location +# for the whole installation; per-camera coordinates are not supported. +# +# Failure-safe: if the toggle is off, the key is missing, the network is down, +# or the API errors out, weather-tag.sh prints nothing and the recorder falls +# back to the un-tagged filename. ambient-record.sh never blocks on this. +# +# Manual test: ./weather-tag.sh (prints e.g. "cloudy" or nothing) +WEATHER_ENABLED=false +OPENWEATHER_API_KEY="${OPENWEATHER_API_KEY:-}" + # ── Cameras ─────────────────────────────────────────────────────────────────── # List every camera name here (space-separated inside the parentheses). # Each name becomes a subfolder under BASE_DIR (images) and BASE_DIR/movies. diff --git a/weather-tag.sh b/weather-tag.sh new file mode 100755 index 0000000..e113459 --- /dev/null +++ b/weather-tag.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# weather-tag.sh — print a one-word weather tag for the current moment at +# (LATITUDE, LONGITUDE), using the OpenWeather "Current weather" endpoint. +# +# Output: a single lowercase word from this fixed vocabulary, or empty: +# thunderstorm rain rainshower snow snowstorm +# fog windy cloudy clear +# +# Used by ambient-record.sh to suffix each 30-minute chunk's filename so the +# file collection self-documents what the audio actually captured. +# +# Failure-safe: if WEATHER_ENABLED≠true, the API key is missing, the network +# is down, the API returns an error, or the response can't be parsed, this +# script prints NOTHING and exits 0. Callers MUST treat empty output as +# "no tag — fall back to the un-tagged filename". Never blocks the recorder. +# +# Required setup: +# 1. Sign up at https://openweathermap.org/api → free "Current weather" plan +# 2. Put the key in .env: OPENWEATHER_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxx +# 3. Confirm LATITUDE / LONGITUDE are set in .env (already needed by sunrise.py) +# 4. Flip WEATHER_ENABLED=true in sky-cam.conf +# +# Manual test: ./weather-tag.sh (prints e.g. "cloudy" or nothing) + +set -uo pipefail # NOT -e: we want to swallow every failure into "no tag" + +SCRIPT_DIR="$(dirname "$(realpath "$0")")" +source "$SCRIPT_DIR/sky-cam.conf" 2>/dev/null || exit 0 +source "$SCRIPT_DIR/.env" 2>/dev/null || true + +[ "${WEATHER_ENABLED:-false}" = "true" ] || exit 0 +[ -n "${OPENWEATHER_API_KEY:-}" ] || exit 0 +[ -n "${LATITUDE:-}" ] || exit 0 +[ -n "${LONGITUDE:-}" ] || exit 0 + +# ── Fetch current conditions ───────────────────────────────────────────────── +# 5s connect / 8s total — must never delay the recorder noticeably. +url="https://api.openweathermap.org/data/2.5/weather?lat=${LATITUDE}&lon=${LONGITUDE}&appid=${OPENWEATHER_API_KEY}&units=metric" +resp=$(curl -s --connect-timeout 5 --max-time 8 "$url" 2>/dev/null) || exit 0 +[ -n "$resp" ] || exit 0 + +# ── Parse condition ID + wind speed ────────────────────────────────────────── +# Condition codes: https://openweathermap.org/weather-conditions +# OpenWeather may return multiple entries in weather[]; the first is primary. +if command -v jq >/dev/null 2>&1; then + code=$(echo "$resp" | jq -r '.weather[0].id // empty' 2>/dev/null) + wind=$(echo "$resp" | jq -r '.wind.speed // 0' 2>/dev/null) +else + # Fallback parser if jq is missing — grabs the first "id":NNN it sees. + code=$(echo "$resp" | grep -oE '"id":[0-9]+' | head -n1 | grep -oE '[0-9]+') + wind=$(echo "$resp" | grep -oE '"speed":[0-9.]+' | head -n1 | grep -oE '[0-9.]+') +fi + +[ -n "${code:-}" ] || exit 0 +wind="${wind:-0}" + +# ── Map condition code → tag (vocabulary documented at top of file) ────────── +# Group ranges follow OpenWeather's published condition-code table: +# 2xx thunder / 3xx drizzle / 5xx rain / 6xx snow / 7xx atmosphere / 800 clear / 80x clouds +tag="" +case "$code" in + 2*) tag="thunderstorm" ;; + 3*) tag="rainshower" ;; # drizzle = light shower + 500|520|521|522|531) tag="rainshower" ;; # light / shower variants + 501|502|503|504|511) tag="rain" ;; # moderate → extreme + freezing + 600|601|612|613|615|616|620|621) tag="snow" ;; + 602|622) tag="snowstorm" ;; # heavy snow / heavy shower snow + 611) tag="snow" ;; # sleet + 701|711|721|741|762) tag="fog" ;; # mist / smoke / haze / fog / ash + 731|751|761|771) tag="windy" ;; # dust whirls / sand / dust / squall + 781) tag="thunderstorm" ;; # tornado → reuse strongest tag + 800) tag="clear" ;; + 801) tag="clear" ;; # few clouds — still mostly clear + 802|803|804) tag="cloudy" ;; + *) tag="" ;; # unknown code → no tag +esac + +# Wind override: gusty conditions drown out other audio cues. If the base +# tag is clear/cloudy and wind ≥ 8 m/s (~18 mph), promote to "windy". +if [ "$tag" = "clear" ] || [ "$tag" = "cloudy" ]; then + # bash can't compare floats — strip the decimal for an integer check. + wind_int="${wind%.*}" + [ "${wind_int:-0}" -ge 8 ] 2>/dev/null && tag="windy" +fi + +[ -n "$tag" ] && printf '%s' "$tag" +exit 0