[locations] Add weather fetching to birds and moods
All checks were successful
build & deploy / test (push) Successful in 1m49s
build & deploy / build-and-deploy (push) Successful in 30s

This commit is contained in:
2026-05-20 13:19:54 -04:00
parent c7850878fe
commit 3014a30616
7 changed files with 105 additions and 86 deletions

View File

@ -16,6 +16,7 @@ class BirdingLocationAdmin(admin.ModelAdmin):
date_hierarchy = "created" date_hierarchy = "created"
list_display = ("uuid", "title") list_display = ("uuid", "title")
ordering = ("-created",) ordering = ("-created",)
raw_id_fields = ("geo_location",)
search_fields = ("title",) search_fields = ("title",)
inlines = [ inlines = [
ScrobbleInline, ScrobbleInline,

View File

@ -136,6 +136,25 @@ def import_birding_csv(file_path, user_id):
log_dict = logdata.asdict log_dict = logdata.asdict
weather_loc = location.geo_location
if not weather_loc:
last_loc = (
Scrobble.objects.filter(
user=user,
media_type=Scrobble.MediaType.GEO_LOCATION,
geo_location__isnull=False,
)
.order_by("-timestamp")
.first()
)
if last_loc:
weather_loc = last_loc.geo_location
if weather_loc:
weather = weather_loc.current_weather
if weather:
log_dict["weather"] = weather["description"]
log_dict["temperature"] = weather["temp"]
stop_timestamp = timestamp + timedelta(minutes=duration_minutes) if duration_minutes else None stop_timestamp = timestamp + timedelta(minutes=duration_minutes) if duration_minutes else None
scrobble = Scrobble( scrobble = Scrobble(

View File

@ -16,6 +16,7 @@ class GeoLocationAdmin(admin.ModelAdmin):
"altitude", "altitude",
) )
ordering = ("-created",) ordering = ("-created",)
search_fields = ("title",)
inlines = [ inlines = [
ScrobbleInline, ScrobbleInline,
] ]

View File

@ -1,7 +1,7 @@
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from decimal import Decimal from decimal import Decimal
from typing import Dict from typing import Dict, Optional
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -107,6 +107,12 @@ class GeoLocation(ScrobblableMixin):
def strings(self) -> ScrobblableConstants: def strings(self) -> ScrobblableConstants:
return ScrobblableConstants(verb="Going", tags="world_map", priority="low") return ScrobblableConstants(verb="Going", tags="world_map", priority="low")
@property
def current_weather(self) -> Optional[dict]:
from locations.utils import fetch_current_weather
return fetch_current_weather(self.lat, self.lon)
def loc_diff(self, old_lat_lon: tuple) -> tuple: def loc_diff(self, old_lat_lon: tuple) -> tuple:
return ( return (
abs(Decimal(old_lat_lon[0]) - Decimal(self.lat)), abs(Decimal(old_lat_lon[0]) - Decimal(self.lat)),

View File

@ -1,6 +1,9 @@
import logging import logging
import xml.etree.ElementTree as ET
from typing import Optional from typing import Optional
from urllib.parse import urlencode
import requests
from django.utils import timezone from django.utils import timezone
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
@ -196,3 +199,76 @@ def detect_movement(
return result return result
return result return result
NWS_URL = "https://forecast.weather.gov/MapClick.php"
def fetch_current_weather(lat: float, lon: float) -> Optional[dict]:
"""Fetch current weather from NWS DWML feed for the given lat/lon.
Returns dict with 'temp' and 'description' keys, or None on failure.
"""
params = {
"lat": lat,
"lon": lon,
"unit": 0,
"lg": "english",
"FcstType": "dwml",
}
url = f"{NWS_URL}?{urlencode(params)}"
try:
resp = requests.get(url, timeout=10)
resp.raise_for_status()
except requests.RequestException as e:
logger.warning("Failed to fetch NWS weather data: %s", e)
return None
try:
root = ET.fromstring(resp.content)
except ET.ParseError as e:
logger.warning("Failed to parse NWS XML: %s", e)
return None
# Find current observations data block
data_blocks = root.findall("data")
obs_block = None
for block in data_blocks:
if block.get("type") == "current observations":
obs_block = block
break
if obs_block is None:
logger.warning("No current observations block in NWS response")
return None
params_el = obs_block.find("parameters")
if params_el is None:
return None
# Temperature (apparent / feels-like)
temp = None
for temp_el in params_el.findall("temperature"):
if temp_el.get("type") == "apparent":
val_el = temp_el.find("value")
if val_el is not None and val_el.text:
try:
temp = int(round(float(val_el.text)))
except (ValueError, TypeError):
pass
break
# Weather description
description = None
weather_el = params_el.find("weather")
if weather_el is not None:
cond_el = weather_el.find("weather-conditions")
if cond_el is not None:
description = cond_el.get("weather-summary")
if temp is None and description is None:
logger.warning("No weather data found in NWS response")
return None
return {"temp": temp, "description": description}

View File

@ -1,80 +0,0 @@
import logging
import xml.etree.ElementTree as ET
from typing import Optional
from urllib.parse import urlencode
import requests
logger = logging.getLogger(__name__)
NWS_URL = "https://forecast.weather.gov/MapClick.php"
def fetch_current_weather(lat: float, lon: float) -> Optional[dict]:
"""Fetch current weather from NWS DWML feed for the given lat/lon.
Returns dict with 'temp' and 'description' keys, or None on failure.
"""
params = {
"lat": lat,
"lon": lon,
"unit": 0,
"lg": "english",
"FcstType": "dwml",
}
url = f"{NWS_URL}?{urlencode(params)}"
try:
resp = requests.get(url, timeout=10)
resp.raise_for_status()
except requests.RequestException as e:
logger.warning("Failed to fetch NWS weather data: %s", e)
return None
try:
root = ET.fromstring(resp.content)
except ET.ParseError as e:
logger.warning("Failed to parse NWS XML: %s", e)
return None
# Find current observations data block
data_blocks = root.findall("data")
obs_block = None
for block in data_blocks:
if block.get("type") == "current observations":
obs_block = block
break
if obs_block is None:
logger.warning("No current observations block in NWS response")
return None
params_el = obs_block.find("parameters")
if params_el is None:
return None
# Temperature (apparent / feels-like)
temp = None
for temp_el in params_el.findall("temperature"):
if temp_el.get("type") == "apparent":
val_el = temp_el.find("value")
if val_el is not None and val_el.text:
try:
temp = int(round(float(val_el.text)))
except (ValueError, TypeError):
pass
break
# Weather description
description = None
weather_el = params_el.find("weather")
if weather_el is not None:
cond_el = weather_el.find("weather-conditions")
if cond_el is not None:
description = cond_el.get("weather-summary")
if temp is None and description is None:
logger.warning("No weather data found in NWS response")
return None
return {"temp": temp, "description": description}

View File

@ -5,7 +5,6 @@ from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from moods.models import Mood, MoodLogData, MoodQuality, MoodReason from moods.models import Mood, MoodLogData, MoodQuality, MoodReason
from moods.utils import fetch_current_weather
from scrobbles.models import Scrobble from scrobbles.models import Scrobble
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -78,10 +77,7 @@ def checkin(request):
.first() .first()
) )
if last_loc and last_loc.geo_location: if last_loc and last_loc.geo_location:
weather = fetch_current_weather( weather = last_loc.geo_location.current_weather
last_loc.geo_location.lat,
last_loc.geo_location.lon,
)
if weather: if weather:
log_data.weather_temp = weather["temp"] log_data.weather_temp = weather["temp"]
log_data.weather_description = weather["description"] log_data.weather_description = weather["description"]