Compare commits

..
Author SHA1 Message Date
Claude 5e1cb8857d Add daily timezone drift check
tz-drift-check.sh + sky-cam-tzcheck timer notify when TIMEZONE no longer
matches the machine's timezone (timers follow the machine, scripts follow TIMEZONE).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZRpfenCe2TRwT4JFMG
2026-11-02 02:54:07 +00:00
Claude 63bbe5c2ab Fix sunrise wait windows on DST change days
daily_sunrise_video.sh and sunrise-audio-capture.sh added wall-clock
seconds-since-midnight to a midnight epoch, which is 3600s off on the two
days a year the clocks change. Resolve targets with date -d "today HH:MM:SS"
instead. sunrise.py now builds local midnight with tz.localize().

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZRpfenCe2TRwT4JFMG
2026-11-02 01:21:57 +00:00
5 changed files with 84 additions and 11 deletions
+8 -6
View File
@@ -65,14 +65,16 @@ if ! $TEST_MODE; then
start_sec=$((sunrise_sec - SUNRISE_PRE_MIN * 60)) start_sec=$((sunrise_sec - SUNRISE_PRE_MIN * 60))
end_sec=$((sunrise_sec + SUNRISE_POST_MIN * 60)) end_sec=$((sunrise_sec + SUNRISE_POST_MIN * 60))
echo "Window: $((start_sec/3600)):$(printf '%02d' $(((start_sec%3600)/60))) → $((end_sec/3600)):$(printf '%02d' $(((end_sec%3600)/60)))" echo "Window: $((start_sec/3600)):$(printf '%02d' $(((start_sec%3600)/60))) → $((end_sec/3600)):$(printf '%02d' $(((end_sec%3600)/60)))"
midnight=$(date -d "today 00:00:00" +%s) # Resolve wall-clock targets through `date -d` instead of adding seconds-since-
now_day_sec=$(( $(date +%s) - midnight )) # midnight to a midnight epoch: on a DST day (23h/25h) the two differ by 3600s.
if [ "$now_day_sec" -lt "$end_sec" ]; then end_epoch=$(date -d "today $(printf '%02d:%02d:%02d' $((end_sec/3600)) $(((end_sec%3600)/60)) $((end_sec%60)))" +%s)
wait_sec=$(( end_sec - now_day_sec + 30 )) now_epoch=$(date +%s)
echo "Waiting ${wait_sec}s for capture window to finish (ends $(date -d "@$(( midnight + end_sec ))" '+%H:%M:%S'))..." if [ "$now_epoch" -lt "$end_epoch" ]; then
wait_sec=$(( end_epoch - now_epoch + 30 ))
echo "Waiting ${wait_sec}s for capture window to finish (ends $(date -d "@$end_epoch" '+%H:%M:%S'))..."
sleep "$wait_sec" sleep "$wait_sec"
else else
echo "Capture window already closed at $(date -d "@$(( midnight + end_sec ))" '+%H:%M:%S') — processing available images" echo "Capture window already closed at $(date -d "@$end_epoch" '+%H:%M:%S') — processing available images"
fi fi
fi fi
+19
View File
@@ -296,6 +296,25 @@ for cam in "${CAMERAS[@]}"; do
fi fi
done done
# ── Timezone drift check ─────────────────────────────────────────────────────
# Daily + shortly after boot: notify if TIMEZONE no longer matches the
# machine's timezone (systemd timers follow the machine, scripts follow TIMEZONE).
write_service "sky-cam-tzcheck" "timezone drift check" "tz-drift-check.sh"
cat > "$UNIT_DIR/sky-cam-tzcheck.timer" <<EOF2
[Unit]
Description=Sky-Cam — timezone drift check
[Timer]
OnBootSec=3min
OnCalendar=*-*-* 12:00:00
Persistent=true
[Install]
WantedBy=timers.target
EOF2
echo " wrote sky-cam-tzcheck.timer"
timers+=("sky-cam-tzcheck")
# ── Generate .env.example ──────────────────────────────────────────────────── # ── Generate .env.example ────────────────────────────────────────────────────
env_example="$HERE/.env.example" env_example="$HERE/.env.example"
{ {
+5 -4
View File
@@ -54,12 +54,13 @@ record_dur_sec=$SUNRISE_TARGET_SECS
echo "Audio window: ${half_pre}s before → ${half_post}s after sunrise (${record_dur_sec}s total)" echo "Audio window: ${half_pre}s before → ${half_post}s after sunrise (${record_dur_sec}s total)"
midnight=$(date -d "today 00:00:00" +%s) # Resolve the wall-clock start via `date -d` (not midnight epoch + seconds):
now_day_sec=$(( $(date +%s) - midnight )) # on a DST day (23h/25h) those differ by 3600s.
wait_sec=$(( record_start_sec - now_day_sec )) start_epoch=$(date -d "today $(printf '%02d:%02d:%02d' $((record_start_sec/3600)) $(((record_start_sec%3600)/60)) $((record_start_sec%60)))" +%s)
wait_sec=$(( start_epoch - $(date +%s) ))
if [ "$wait_sec" -gt 0 ]; then if [ "$wait_sec" -gt 0 ]; then
echo "Waiting ${wait_sec}s until audio window ($(date -d "@$(( midnight + record_start_sec ))" '+%H:%M:%S'))..." echo "Waiting ${wait_sec}s until audio window ($(date -d "@$start_epoch" '+%H:%M:%S'))..."
sleep "$wait_sec" sleep "$wait_sec"
fi fi
+3 -1
View File
@@ -58,7 +58,9 @@ tz = pytz.timezone(conf['TIMEZONE'])
# This bracket is timezone-correct and handles any UTC offset, including large # This bracket is timezone-correct and handles any UTC offset, including large
# positive offsets (UTC+12/+14) where local midnight falls on the previous UTC day. # positive offsets (UTC+12/+14) where local midnight falls on the previous UTC day.
now_local = datetime.now(tz) now_local = datetime.now(tz)
midnight_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0) # tz.localize (not .replace) so the UTC offset is re-resolved for midnight —
# on a DST-change day now_local's offset differs from midnight's.
midnight_local = tz.localize(datetime(now_local.year, now_local.month, now_local.day))
t0_utc = midnight_local.astimezone(timezone.utc) t0_utc = midnight_local.astimezone(timezone.utc)
t1_utc = t0_utc + timedelta(hours=25) # 25 h covers any timezone's full day t1_utc = t0_utc + timedelta(hours=25) # 25 h covers any timezone's full day
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# tz-drift-check.sh — warn if TIMEZONE (sky-cam.conf / .env) no longer matches
# the machine's own timezone.
#
# systemd timers fire on the machine's clock, but sunrise.py, the capture
# scripts and the per-day folder names use TIMEZONE. If the two drift apart
# (someone ran `timedatectl set-timezone`, a cloud-init reset, a restored
# image, a typo in .env...) every scheduled job runs at the wrong local time
# and the sunrise capture window is off by the zone difference.
#
# Run daily by sky-cam-tzcheck.timer (and shortly after boot). Sends one
# notify.sh message per run while the mismatch persists; silent when fine.
# Exit 0 either way — a mismatch is a notification, not a job failure.
set -uo pipefail
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
source "$SCRIPT_DIR/sky-cam.conf"
source "$SCRIPT_DIR/.env" 2>/dev/null || true
# Machine's zone — read WITHOUT any TZ override so we see the real system value.
sys_tz="$(env -u TZ timedatectl show -p Timezone --value 2>/dev/null)"
[ -z "$sys_tz" ] && sys_tz="$(cat /etc/timezone 2>/dev/null)"
[ -z "$sys_tz" ] && sys_tz="$(readlink /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')"
if [ -z "$sys_tz" ]; then
echo "tz-drift-check: could not determine the machine's timezone — skipping"
exit 0
fi
# Compare by real UTC offsets across the year so aliases (US/Eastern vs
# America/New_York) and identical-rule zones are not reported as drift.
same=true
if [ "$sys_tz" != "$TIMEZONE" ]; then
for m in 01 04 07 10; do
a="$(TZ="$sys_tz" date -d "2026-$m-15 12:00 UTC" +%z)"
b="$(TZ="$TIMEZONE" date -d "2026-$m-15 12:00 UTC" +%z)"
[ "$a" = "$b" ] || { same=false; break; }
done
fi
if $same; then
echo "Timezone OK: sky-cam TIMEZONE=$TIMEZONE, machine=$sys_tz"
exit 0
fi
msg="sky-cam TIMEZONE is $TIMEZONE but this machine's timezone is $sys_tz. Scheduled jobs run on the machine clock, so sunrise/seasons/moon jobs will fire at the wrong local time. Fix: set TIMEZONE=$sys_tz in $SCRIPT_DIR/.env and re-run ./install.sh, or run: sudo timedatectl set-timezone $TIMEZONE"
echo "WARNING: $msg"
"$SCRIPT_DIR/notify.sh" "WARNING: sky-cam timezone mismatch" "$msg" || true
exit 0