""" 気象データを Open-Meteo API から取得して DB に保存する。 使い方: # 差分取得(最終レコードの翌日〜昨日) python manage.py fetch_weather # 全件取得(初回インポート) python manage.py fetch_weather --full # 期間指定 python manage.py fetch_weather --start-date 2024-01-01 --end-date 2024-12-31 """ import datetime import requests from django.core.management.base import BaseCommand, CommandError from apps.weather.models import WeatherRecord LATITUDE = 33.213 LONGITUDE = 133.133 TIMEZONE = 'Asia/Tokyo' FULL_START = '2016-01-01' OPEN_METEO_URL = 'https://archive-api.open-meteo.com/v1/archive' DAILY_VARS = [ 'temperature_2m_mean', 'temperature_2m_max', 'temperature_2m_min', 'sunshine_duration', 'precipitation_sum', 'wind_speed_10m_max', 'surface_pressure_min', ] def fetch_from_api(start_date: str, end_date: str) -> list[dict]: """Open-Meteo から daily データを取得し、WeatherRecord 形式のリストで返す。""" params = { 'latitude': LATITUDE, 'longitude': LONGITUDE, 'start_date': start_date, 'end_date': end_date, 'daily': DAILY_VARS, 'timezone': TIMEZONE, } resp = requests.get(OPEN_METEO_URL, params=params, timeout=30) if resp.status_code != 200: raise CommandError(f'Open-Meteo API エラー: {resp.status_code} {resp.text[:200]}') data = resp.json().get('daily', {}) dates = data.get('time', []) if not dates: return [] sunshine_raw = data.get('sunshine_duration', []) results = [] for i, d in enumerate(dates): # 日照: 秒 → 時間 sun_sec = sunshine_raw[i] sunshine_h = round(sun_sec / 3600, 2) if sun_sec is not None else None results.append({ 'date': d, 'temp_mean': data['temperature_2m_mean'][i], 'temp_max': data['temperature_2m_max'][i], 'temp_min': data['temperature_2m_min'][i], 'sunshine_h': sunshine_h, 'precip_mm': data['precipitation_sum'][i], 'wind_max': data['wind_speed_10m_max'][i], 'pressure_min': data['surface_pressure_min'][i], }) return results def upsert_records(records: list[dict], stdout=None) -> int: """WeatherRecord に upsert し、保存件数を返す。""" saved = 0 for item in records: record, created = WeatherRecord.objects.get_or_create(date=item['date']) for field in ['temp_mean', 'temp_max', 'temp_min', 'sunshine_h', 'precip_mm', 'wind_max', 'pressure_min']: val = item.get(field) if val is not None: setattr(record, field, val) record.save() saved += 1 if stdout and created: stdout.write(f' new: {item["date"]}') return saved class Command(BaseCommand): help = '気象データを Open-Meteo から取得して DB に保存する' def add_arguments(self, parser): parser.add_argument( '--full', action='store_true', help=f'2016-01-01 から昨日まで全件取得(初回インポート用)', ) parser.add_argument( '--start-date', type=str, help='取得開始日 (YYYY-MM-DD)。省略時は最終レコードの翌日', ) parser.add_argument( '--end-date', type=str, help='取得終了日 (YYYY-MM-DD)。省略時は昨日', ) def handle(self, *args, **options): yesterday = (datetime.date.today() - datetime.timedelta(days=1)).isoformat() # 終了日 end_date = options.get('end_date') or yesterday # 開始日の決定 if options['full']: start_date = FULL_START elif options.get('start_date'): start_date = options['start_date'] else: # 最終レコードの翌日を自動算出 last = WeatherRecord.objects.order_by('-date').first() if last: start_date = (last.date + datetime.timedelta(days=1)).isoformat() else: start_date = FULL_START self.stdout.write( self.style.WARNING('DB にデータがないため 2016-01-01 から取得します。') ) if start_date > end_date: self.stdout.write(self.style.SUCCESS('すでに最新の状態です。取得をスキップします。')) return self.stdout.write(f'取得期間: {start_date} 〜 {end_date}') # Open-Meteo は 1回のリクエストで最大1年分程度が安定。 # 長期間の場合は年単位で分割して取得する。 start = datetime.date.fromisoformat(start_date) end = datetime.date.fromisoformat(end_date) total_saved = 0 current = start while current <= end: chunk_end = min( datetime.date(current.year, 12, 31), end ) self.stdout.write(f' → {current} 〜 {chunk_end} を取得中...') records = fetch_from_api(current.isoformat(), chunk_end.isoformat()) saved = upsert_records(records, stdout=None) total_saved += saved self.stdout.write(f' {saved} 件保存') current = datetime.date(chunk_end.year + 1, 1, 1) self.stdout.write(self.style.SUCCESS(f'完了: 合計 {total_saved} 件を保存しました。'))