first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import WeatherForecast, WeatherParameter
|
||||
|
||||
|
||||
@admin.register(WeatherParameter)
|
||||
class WeatherParameterAdmin(admin.ModelAdmin):
|
||||
list_display = ("code", "name_fa", "unit", "created_at")
|
||||
search_fields = ("code", "name_fa")
|
||||
|
||||
|
||||
@admin.register(WeatherForecast)
|
||||
class WeatherForecastAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"location",
|
||||
"forecast_date",
|
||||
"temperature_min",
|
||||
"temperature_max",
|
||||
"precipitation",
|
||||
"et0",
|
||||
"fetched_at",
|
||||
)
|
||||
list_filter = ("forecast_date",)
|
||||
search_fields = ("location__latitude", "location__longitude")
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class WeatherConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "weather"
|
||||
verbose_name = "Weather Forecast"
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
Management command to seed weather parameters.
|
||||
Run: python manage.py seed_weather_parameters
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from weather.models import WeatherParameter
|
||||
|
||||
|
||||
INITIAL_PARAMETERS = [
|
||||
("temperature_min", "حداقل دمای هوا", "°C"),
|
||||
("temperature_max", "حداکثر دمای هوا", "°C"),
|
||||
("temperature_mean", "میانگین دمای هوا", "°C"),
|
||||
("precipitation", "مجموع بارش", "mm"),
|
||||
("precipitation_probability", "احتمال بارش", "%"),
|
||||
("humidity_mean", "میانگین رطوبت نسبی", "%"),
|
||||
("wind_speed_max", "حداکثر سرعت باد", "km/h"),
|
||||
("et0", "تبخیر-تعرق مرجع (ET₀)", "mm/day"),
|
||||
("weather_code", "کد وضعیت آبوهوا (WMO)", ""),
|
||||
]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Seed weather parameters (temperature, precipitation, ET0, etc.)"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
created_count = 0
|
||||
for code, name_fa, unit in INITIAL_PARAMETERS:
|
||||
_, created = WeatherParameter.objects.get_or_create(
|
||||
code=code,
|
||||
defaults={"name_fa": name_fa, "unit": unit},
|
||||
)
|
||||
if created:
|
||||
created_count += 1
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f" Created: {code} ({name_fa})")
|
||||
)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"\nDone. Created {created_count} new weather parameters."
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,184 @@
|
||||
# Generated manually for weather
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("location_data", "0002_soildepthdata_refactor"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# ── WeatherParameter ──
|
||||
migrations.CreateModel(
|
||||
name="WeatherParameter",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"code",
|
||||
models.CharField(
|
||||
db_index=True,
|
||||
help_text="کد یکتا (مثلاً temperature_max)",
|
||||
max_length=64,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"name_fa",
|
||||
models.CharField(
|
||||
help_text="نام فارسی",
|
||||
max_length=128,
|
||||
),
|
||||
),
|
||||
(
|
||||
"unit",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="واحد اندازهگیری",
|
||||
max_length=32,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
"ordering": ["code"],
|
||||
"verbose_name": "پارامتر هواشناسی",
|
||||
"verbose_name_plural": "پارامترهای هواشناسی",
|
||||
},
|
||||
),
|
||||
# ── WeatherForecast ──
|
||||
migrations.CreateModel(
|
||||
name="WeatherForecast",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"location",
|
||||
models.ForeignKey(
|
||||
help_text="موقعیت مکانی مرتبط از جدول SoilLocation",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="weather_forecasts",
|
||||
to="location_data.soillocation",
|
||||
),
|
||||
),
|
||||
(
|
||||
"forecast_date",
|
||||
models.DateField(
|
||||
db_index=True,
|
||||
help_text="تاریخ پیشبینی",
|
||||
),
|
||||
),
|
||||
(
|
||||
"temperature_min",
|
||||
models.FloatField(
|
||||
blank=True,
|
||||
help_text="حداقل دمای هوا (°C)",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"temperature_max",
|
||||
models.FloatField(
|
||||
blank=True,
|
||||
help_text="حداکثر دمای هوا (°C)",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"temperature_mean",
|
||||
models.FloatField(
|
||||
blank=True,
|
||||
help_text="میانگین دمای هوا (°C)",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"precipitation",
|
||||
models.FloatField(
|
||||
blank=True,
|
||||
help_text="مجموع بارش (mm)",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"precipitation_probability",
|
||||
models.FloatField(
|
||||
blank=True,
|
||||
help_text="احتمال بارش (%)",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"humidity_mean",
|
||||
models.FloatField(
|
||||
blank=True,
|
||||
help_text="میانگین رطوبت نسبی (%)",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"wind_speed_max",
|
||||
models.FloatField(
|
||||
blank=True,
|
||||
help_text="حداکثر سرعت باد (km/h)",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"et0",
|
||||
models.FloatField(
|
||||
blank=True,
|
||||
help_text="تبخیر-تعرق مرجع (ET₀) — mm/day",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"weather_code",
|
||||
models.IntegerField(
|
||||
blank=True,
|
||||
help_text="کد وضعیت آبوهوا (WMO code)",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"fetched_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="آخرین زمان واکشی از API",
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
"ordering": ["location", "forecast_date"],
|
||||
"verbose_name": "پیشبینی هواشناسی",
|
||||
"verbose_name_plural": "پیشبینیهای هواشناسی",
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="weatherforecast",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("location", "forecast_date"),
|
||||
name="weather_unique_location_date",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
# Seed migration: populate initial weather parameters.
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
INITIAL_PARAMETERS = [
|
||||
("temperature_min", "حداقل دمای هوا", "°C"),
|
||||
("temperature_max", "حداکثر دمای هوا", "°C"),
|
||||
("temperature_mean", "میانگین دمای هوا", "°C"),
|
||||
("precipitation", "مجموع بارش", "mm"),
|
||||
("precipitation_probability", "احتمال بارش", "%"),
|
||||
("humidity_mean", "میانگین رطوبت نسبی", "%"),
|
||||
("wind_speed_max", "حداکثر سرعت باد", "km/h"),
|
||||
("et0", "تبخیر-تعرق مرجع (ET₀)", "mm/day"),
|
||||
("weather_code", "کد وضعیت آبوهوا (WMO)", ""),
|
||||
]
|
||||
|
||||
|
||||
def seed_parameters(apps, schema_editor):
|
||||
WeatherParameter = apps.get_model("weather", "WeatherParameter")
|
||||
for code, name_fa, unit in INITIAL_PARAMETERS:
|
||||
WeatherParameter.objects.get_or_create(
|
||||
code=code,
|
||||
defaults={"name_fa": name_fa, "unit": unit},
|
||||
)
|
||||
|
||||
|
||||
def unseed_parameters(apps, schema_editor):
|
||||
WeatherParameter = apps.get_model("weather", "WeatherParameter")
|
||||
codes = [p[0] for p in INITIAL_PARAMETERS]
|
||||
WeatherParameter.objects.filter(code__in=codes).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("weather", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(seed_parameters, unseed_parameters),
|
||||
]
|
||||
@@ -0,0 +1,137 @@
|
||||
# Seed migration: populate sample 7-day weather forecasts for existing SoilLocations.
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import migrations
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
SAMPLE_DAILY_DATA = [
|
||||
{
|
||||
"day_offset": 0,
|
||||
"temperature_min": 18.5,
|
||||
"temperature_max": 33.2,
|
||||
"temperature_mean": 25.8,
|
||||
"precipitation": 0.0,
|
||||
"precipitation_probability": 5.0,
|
||||
"humidity_mean": 28.0,
|
||||
"wind_speed_max": 12.0,
|
||||
"et0": 6.8,
|
||||
"weather_code": 0,
|
||||
},
|
||||
{
|
||||
"day_offset": 1,
|
||||
"temperature_min": 19.0,
|
||||
"temperature_max": 34.5,
|
||||
"temperature_mean": 26.7,
|
||||
"precipitation": 0.0,
|
||||
"precipitation_probability": 10.0,
|
||||
"humidity_mean": 30.0,
|
||||
"wind_speed_max": 14.0,
|
||||
"et0": 7.1,
|
||||
"weather_code": 1,
|
||||
},
|
||||
{
|
||||
"day_offset": 2,
|
||||
"temperature_min": 20.2,
|
||||
"temperature_max": 32.0,
|
||||
"temperature_mean": 26.1,
|
||||
"precipitation": 3.5,
|
||||
"precipitation_probability": 65.0,
|
||||
"humidity_mean": 52.0,
|
||||
"wind_speed_max": 18.0,
|
||||
"et0": 5.2,
|
||||
"weather_code": 61,
|
||||
},
|
||||
{
|
||||
"day_offset": 3,
|
||||
"temperature_min": 17.8,
|
||||
"temperature_max": 28.5,
|
||||
"temperature_mean": 23.1,
|
||||
"precipitation": 12.0,
|
||||
"precipitation_probability": 85.0,
|
||||
"humidity_mean": 70.0,
|
||||
"wind_speed_max": 22.0,
|
||||
"et0": 3.8,
|
||||
"weather_code": 63,
|
||||
},
|
||||
{
|
||||
"day_offset": 4,
|
||||
"temperature_min": 16.5,
|
||||
"temperature_max": 27.0,
|
||||
"temperature_mean": 21.7,
|
||||
"precipitation": 5.0,
|
||||
"precipitation_probability": 55.0,
|
||||
"humidity_mean": 60.0,
|
||||
"wind_speed_max": 16.0,
|
||||
"et0": 4.5,
|
||||
"weather_code": 61,
|
||||
},
|
||||
{
|
||||
"day_offset": 5,
|
||||
"temperature_min": 18.0,
|
||||
"temperature_max": 31.0,
|
||||
"temperature_mean": 24.5,
|
||||
"precipitation": 0.0,
|
||||
"precipitation_probability": 8.0,
|
||||
"humidity_mean": 35.0,
|
||||
"wind_speed_max": 10.0,
|
||||
"et0": 6.2,
|
||||
"weather_code": 2,
|
||||
},
|
||||
{
|
||||
"day_offset": 6,
|
||||
"temperature_min": 19.5,
|
||||
"temperature_max": 34.0,
|
||||
"temperature_mean": 26.7,
|
||||
"precipitation": 0.0,
|
||||
"precipitation_probability": 3.0,
|
||||
"humidity_mean": 25.0,
|
||||
"wind_speed_max": 8.0,
|
||||
"et0": 7.0,
|
||||
"weather_code": 0,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def seed_forecasts(apps, schema_editor):
|
||||
SoilLocation = apps.get_model("location_data", "SoilLocation")
|
||||
WeatherForecast = apps.get_model("weather", "WeatherForecast")
|
||||
|
||||
today = timezone.now().date()
|
||||
|
||||
for location in SoilLocation.objects.all():
|
||||
for daily in SAMPLE_DAILY_DATA:
|
||||
forecast_date = today + timedelta(days=daily["day_offset"])
|
||||
WeatherForecast.objects.get_or_create(
|
||||
location=location,
|
||||
forecast_date=forecast_date,
|
||||
defaults={
|
||||
"temperature_min": daily["temperature_min"],
|
||||
"temperature_max": daily["temperature_max"],
|
||||
"temperature_mean": daily["temperature_mean"],
|
||||
"precipitation": daily["precipitation"],
|
||||
"precipitation_probability": daily["precipitation_probability"],
|
||||
"humidity_mean": daily["humidity_mean"],
|
||||
"wind_speed_max": daily["wind_speed_max"],
|
||||
"et0": daily["et0"],
|
||||
"weather_code": daily["weather_code"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def unseed_forecasts(apps, schema_editor):
|
||||
WeatherForecast = apps.get_model("weather", "WeatherForecast")
|
||||
WeatherForecast.objects.all().delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("weather", "0002_seed_weather_parameters"),
|
||||
("location_data", "0002_soildepthdata_refactor"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(seed_forecasts, unseed_forecasts),
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class WeatherParameter(models.Model):
|
||||
"""
|
||||
تعریف پارامترهای هواشناسی (مثلاً دما، بارش، تبخیر-تعرق، ...).
|
||||
"""
|
||||
|
||||
code = models.CharField(
|
||||
max_length=64,
|
||||
unique=True,
|
||||
db_index=True,
|
||||
help_text="کد یکتا (مثلاً temperature_max)",
|
||||
)
|
||||
name_fa = models.CharField(max_length=128, help_text="نام فارسی")
|
||||
unit = models.CharField(max_length=32, blank=True, help_text="واحد اندازهگیری")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["code"]
|
||||
verbose_name = "پارامتر هواشناسی"
|
||||
verbose_name_plural = "پارامترهای هواشناسی"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code} ({self.name_fa})"
|
||||
|
||||
|
||||
class WeatherForecast(models.Model):
|
||||
"""
|
||||
پیشبینی هواشناسی روزانه (تا ۷ روز آینده) برای یک SoilLocation.
|
||||
دادهها شامل دما، بارش، رطوبت، باد و تبخیر-تعرق مرجع (ET0) هستند.
|
||||
"""
|
||||
|
||||
location = models.ForeignKey(
|
||||
"location_data.SoilLocation",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="weather_forecasts",
|
||||
help_text="موقعیت مکانی مرتبط از جدول SoilLocation",
|
||||
)
|
||||
forecast_date = models.DateField(
|
||||
db_index=True,
|
||||
help_text="تاریخ پیشبینی",
|
||||
)
|
||||
|
||||
temperature_min = models.FloatField(
|
||||
null=True, blank=True, help_text="حداقل دمای هوا (°C)"
|
||||
)
|
||||
temperature_max = models.FloatField(
|
||||
null=True, blank=True, help_text="حداکثر دمای هوا (°C)"
|
||||
)
|
||||
temperature_mean = models.FloatField(
|
||||
null=True, blank=True, help_text="میانگین دمای هوا (°C)"
|
||||
)
|
||||
|
||||
precipitation = models.FloatField(
|
||||
null=True, blank=True, help_text="مجموع بارش (mm)"
|
||||
)
|
||||
precipitation_probability = models.FloatField(
|
||||
null=True, blank=True, help_text="احتمال بارش (%)"
|
||||
)
|
||||
|
||||
humidity_mean = models.FloatField(
|
||||
null=True, blank=True, help_text="میانگین رطوبت نسبی (%)"
|
||||
)
|
||||
|
||||
wind_speed_max = models.FloatField(
|
||||
null=True, blank=True, help_text="حداکثر سرعت باد (km/h)"
|
||||
)
|
||||
|
||||
et0 = models.FloatField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="تبخیر-تعرق مرجع (ET₀) — mm/day",
|
||||
)
|
||||
|
||||
weather_code = models.IntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="کد وضعیت آبوهوا (WMO code)",
|
||||
)
|
||||
|
||||
fetched_at = models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="آخرین زمان واکشی از API",
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["location", "forecast_date"],
|
||||
name="weather_unique_location_date",
|
||||
)
|
||||
]
|
||||
ordering = ["location", "forecast_date"]
|
||||
verbose_name = "پیشبینی هواشناسی"
|
||||
verbose_name_plural = "پیشبینیهای هواشناسی"
|
||||
|
||||
def __str__(self):
|
||||
return f"WeatherForecast({self.location_id}, {self.forecast_date})"
|
||||
|
||||
@property
|
||||
def will_rain(self):
|
||||
"""آیا بارندگی پیشبینی شده است؟"""
|
||||
if self.precipitation is not None:
|
||||
return self.precipitation > 0.0
|
||||
return None
|
||||
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
سرویسهای هواشناسی — واکشی پیشبینی ۷ روزه و ذخیره در دیتابیس.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import date, timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
|
||||
from location_data.models import SoilLocation
|
||||
|
||||
from .models import WeatherForecast
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def fetch_weather_from_api(latitude: float, longitude: float) -> dict | None:
|
||||
"""
|
||||
اتصال به API هواشناسی و دریافت پیشبینی ۷ روزه.
|
||||
|
||||
TODO: پیادهسازی اتصال واقعی به API (مثلاً Open-Meteo).
|
||||
در حال حاضر این تابع خالی است و None برمیگرداند.
|
||||
|
||||
پارامترها:
|
||||
latitude: عرض جغرافیایی
|
||||
longitude: طول جغرافیایی
|
||||
|
||||
خروجی مورد انتظار (وقتی پیادهسازی شود):
|
||||
{
|
||||
"daily": {
|
||||
"time": ["2025-07-01", "2025-07-02", ...],
|
||||
"temperature_2m_max": [35.2, 36.1, ...],
|
||||
"temperature_2m_min": [22.1, 23.0, ...],
|
||||
"temperature_2m_mean": [28.6, 29.5, ...],
|
||||
"precipitation_sum": [0.0, 2.5, ...],
|
||||
"precipitation_probability_max": [0, 60, ...],
|
||||
"relative_humidity_2m_mean": [30.0, 45.0, ...],
|
||||
"wind_speed_10m_max": [15.0, 20.0, ...],
|
||||
"et0_fao_evapotranspiration": [6.5, 5.8, ...],
|
||||
"weather_code": [0, 61, ...],
|
||||
}
|
||||
}
|
||||
"""
|
||||
# TODO: اتصال واقعی به API هواشناسی
|
||||
# api_url = settings.WEATHER_API_BASE_URL
|
||||
# api_key = settings.WEATHER_API_KEY
|
||||
return None
|
||||
|
||||
|
||||
def parse_weather_response(data: dict) -> list[dict]:
|
||||
"""
|
||||
تبدیل پاسخ API به لیست dict برای ذخیره در WeatherForecast.
|
||||
فرمت ورودی: Open-Meteo daily format.
|
||||
"""
|
||||
daily = data.get("daily", {})
|
||||
times = daily.get("time", [])
|
||||
forecasts = []
|
||||
|
||||
for i, date_str in enumerate(times):
|
||||
forecasts.append(
|
||||
{
|
||||
"forecast_date": date_str,
|
||||
"temperature_max": _safe_index(
|
||||
daily.get("temperature_2m_max"), i
|
||||
),
|
||||
"temperature_min": _safe_index(
|
||||
daily.get("temperature_2m_min"), i
|
||||
),
|
||||
"temperature_mean": _safe_index(
|
||||
daily.get("temperature_2m_mean"), i
|
||||
),
|
||||
"precipitation": _safe_index(
|
||||
daily.get("precipitation_sum"), i
|
||||
),
|
||||
"precipitation_probability": _safe_index(
|
||||
daily.get("precipitation_probability_max"), i
|
||||
),
|
||||
"humidity_mean": _safe_index(
|
||||
daily.get("relative_humidity_2m_mean"), i
|
||||
),
|
||||
"wind_speed_max": _safe_index(
|
||||
daily.get("wind_speed_10m_max"), i
|
||||
),
|
||||
"et0": _safe_index(
|
||||
daily.get("et0_fao_evapotranspiration"), i
|
||||
),
|
||||
"weather_code": _safe_index(
|
||||
daily.get("weather_code"), i
|
||||
),
|
||||
}
|
||||
)
|
||||
return forecasts
|
||||
|
||||
|
||||
def _safe_index(lst: list | None, index: int):
|
||||
"""مقدار index را از لیست برمیگرداند یا None."""
|
||||
if lst is None or index >= len(lst):
|
||||
return None
|
||||
return lst[index]
|
||||
|
||||
|
||||
def update_weather_for_location(location: SoilLocation) -> dict:
|
||||
"""
|
||||
واکشی و ذخیره پیشبینی هواشناسی ۷ روزه برای یک SoilLocation.
|
||||
|
||||
خروجی:
|
||||
{"status": "success"|"no_data"|"error", "location_id": int, ...}
|
||||
"""
|
||||
lat = float(location.latitude)
|
||||
lon = float(location.longitude)
|
||||
|
||||
try:
|
||||
data = fetch_weather_from_api(lat, lon)
|
||||
except Exception as exc:
|
||||
logger.error("Weather API error for location %s: %s", location.id, exc)
|
||||
return {
|
||||
"status": "error",
|
||||
"location_id": location.id,
|
||||
"error": str(exc),
|
||||
}
|
||||
|
||||
if data is None:
|
||||
logger.info(
|
||||
"Weather API returned no data for location %s (stub mode).",
|
||||
location.id,
|
||||
)
|
||||
return {
|
||||
"status": "no_data",
|
||||
"location_id": location.id,
|
||||
"message": "API connection not implemented yet.",
|
||||
}
|
||||
|
||||
forecasts = parse_weather_response(data)
|
||||
|
||||
with transaction.atomic():
|
||||
for fc in forecasts:
|
||||
WeatherForecast.objects.update_or_create(
|
||||
location=location,
|
||||
forecast_date=fc.pop("forecast_date"),
|
||||
defaults=fc,
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"location_id": location.id,
|
||||
"days_updated": len(forecasts),
|
||||
}
|
||||
|
||||
|
||||
def update_weather_for_all_locations() -> list[dict]:
|
||||
"""
|
||||
واکشی پیشبینی هواشناسی برای تمام SoilLocationهای موجود.
|
||||
"""
|
||||
results = []
|
||||
for location in SoilLocation.objects.all():
|
||||
result = update_weather_for_location(location)
|
||||
results.append(result)
|
||||
return results
|
||||
|
||||
|
||||
def get_forecast_for_location(
|
||||
location: SoilLocation,
|
||||
days: int = 7,
|
||||
) -> list[WeatherForecast]:
|
||||
"""
|
||||
دریافت پیشبینیهای ذخیرهشده برای یک location (تا N روز آینده).
|
||||
"""
|
||||
today = date.today()
|
||||
end_date = today + timedelta(days=days)
|
||||
return list(
|
||||
WeatherForecast.objects.filter(
|
||||
location=location,
|
||||
forecast_date__gte=today,
|
||||
forecast_date__lte=end_date,
|
||||
).order_by("forecast_date")
|
||||
)
|
||||
|
||||
|
||||
def should_irrigate_today(location: SoilLocation) -> dict:
|
||||
"""
|
||||
بررسی ساده: آیا فردا باران میبارد؟
|
||||
اگر بارش فردا بیشتر از آستانه باشد → آبیاری لازم نیست.
|
||||
|
||||
خروجی:
|
||||
{
|
||||
"needs_irrigation": bool | None,
|
||||
"tomorrow_precipitation": float | None,
|
||||
"tomorrow_date": str,
|
||||
"reason": str,
|
||||
}
|
||||
"""
|
||||
tomorrow = date.today() + timedelta(days=1)
|
||||
forecast = WeatherForecast.objects.filter(
|
||||
location=location,
|
||||
forecast_date=tomorrow,
|
||||
).first()
|
||||
|
||||
if forecast is None:
|
||||
return {
|
||||
"needs_irrigation": None,
|
||||
"tomorrow_precipitation": None,
|
||||
"tomorrow_date": str(tomorrow),
|
||||
"reason": "داده پیشبینی فردا موجود نیست.",
|
||||
}
|
||||
|
||||
rain_threshold_mm = 2.0
|
||||
if forecast.precipitation is not None and forecast.precipitation >= rain_threshold_mm:
|
||||
return {
|
||||
"needs_irrigation": False,
|
||||
"tomorrow_precipitation": forecast.precipitation,
|
||||
"tomorrow_date": str(tomorrow),
|
||||
"reason": (
|
||||
f"فردا {forecast.precipitation} mm بارش پیشبینی شده — "
|
||||
"نیاز به آبیاری نیست."
|
||||
),
|
||||
}
|
||||
|
||||
return {
|
||||
"needs_irrigation": True,
|
||||
"tomorrow_precipitation": forecast.precipitation,
|
||||
"tomorrow_date": str(tomorrow),
|
||||
"reason": "بارش فردا ناچیز یا صفر — آبیاری توصیه میشود.",
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
تسکهای Celery برای واکشی دادههای هواشناسی.
|
||||
"""
|
||||
|
||||
from config.celery import app
|
||||
|
||||
from location_data.models import SoilLocation
|
||||
|
||||
from .services import update_weather_for_location, update_weather_for_all_locations
|
||||
|
||||
|
||||
@app.task(bind=True)
|
||||
def fetch_weather_task(self, location_id: int):
|
||||
"""
|
||||
واکشی پیشبینی هواشناسی ۷ روزه برای یک location مشخص.
|
||||
"""
|
||||
try:
|
||||
location = SoilLocation.objects.get(pk=location_id)
|
||||
except SoilLocation.DoesNotExist:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": f"SoilLocation with id={location_id} not found.",
|
||||
}
|
||||
|
||||
return update_weather_for_location(location)
|
||||
|
||||
|
||||
@app.task(bind=True)
|
||||
def fetch_weather_all_locations_task(self):
|
||||
"""
|
||||
واکشی پیشبینی هواشناسی برای تمام locationها.
|
||||
مناسب برای Celery Beat (مثلاً هر ۶ ساعت).
|
||||
"""
|
||||
return update_weather_for_all_locations()
|
||||
Reference in New Issue
Block a user