diff --git a/weather/serializers.py b/weather/serializers.py index 38b082d..57a527d 100644 --- a/weather/serializers.py +++ b/weather/serializers.py @@ -1,4 +1,5 @@ from rest_framework import serializers +from django.db import models from .models import WeatherStats class WeatherStatSerializer(serializers.ModelSerializer): @@ -8,13 +9,32 @@ class WeatherStatSerializer(serializers.ModelSerializer): model = WeatherStats fields = ('humidity_air', 'humidity_ground', 'temperature', 'light', 'created_at') - - - class OpenWeatherAPISerializer(serializers.Serializer): date = serializers.DateField() temp_night = serializers.FloatField() temp_day = serializers.FloatField() humidity = serializers.FloatField() wind_speed = serializers.FloatField() - precip_probability = serializers.FloatField() \ No newline at end of file + precip_probability = serializers.FloatField() + +class WeatherPeriods(models.TextChoices): + min_10 = 'min_10', '10 Minutes' + min_30 = 'min_30', '30 Minutes' + hour = 'hour', '1 Hour' + hour_6 = 'hour_6', '6 Hours' + hour_12 = 'hour_12', '12 Hours' + hour_24 = 'hour_24', '24 Hours' + + @classmethod + def only_values(cls): + return [value for value, label in cls.choices] + +class WeatherByPeriodRequestSerializer(serializers.Serializer): + period = serializers.ChoiceField(WeatherPeriods.choices, required=True) + +class WeatherByPeriodSerializer(serializers.Serializer): + date = serializers.DateTimeField(required=True) + humidity_air = serializers.FloatField(required=True) + humidity_ground = serializers.FloatField(required=True) + temperature = serializers.FloatField(required=True) + light = serializers.FloatField(required=True) \ No newline at end of file diff --git a/weather/urls.py b/weather/urls.py index 1ff7783..a35c4c0 100644 --- a/weather/urls.py +++ b/weather/urls.py @@ -5,5 +5,6 @@ urlpatterns = [ path('create/', CreateStatAPI.as_view()), path('last/', LastStatAPI.as_view()), path('history/', StatsHistoryAPI.as_view()), - path('open-meteo/', OpenMeteoWeatherAPI.as_view()), + path('open-meteo/', OpenMeteoWeatherAPI.as_view()), + path('stats-by-period/', StatsByPeriodAPI.as_view()) ] diff --git a/weather/views.py b/weather/views.py index fd83336..984ab3f 100644 --- a/weather/views.py +++ b/weather/views.py @@ -1,6 +1,10 @@ import time from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page +from django.utils import timezone +from django.db.models import Avg +from django.db.models.functions import TruncMinute, TruncHour +from datetime import timedelta from django.core.cache import cache from rest_framework.generics import CreateAPIView, ListAPIView from rest_framework.views import APIView @@ -11,7 +15,7 @@ from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiRespon from project.serializers import MessageSerializer from .models import WeatherStats -from .serializers import WeatherStatSerializer, OpenWeatherAPISerializer +from .serializers import * from .utils import IsMikrokontroller, PageNumberPagination, get_weather_api_data CACHE_SAVE_CONTROL = 'stats_latest_saved_control' @@ -76,6 +80,90 @@ class StatsHistoryAPI(ListAPIView): queryset = WeatherStats.objects.order_by('-created_at') pagination_class = PageNumberPagination +class StatsByPeriodAPI(APIView): + + @extend_schema(tags=['Weather'], + description="Weather data for graph by period", + parameters=[ + OpenApiParameter( + name='period', + type=str, + location=OpenApiParameter.QUERY, + description='Period', + required=True, + enum=WeatherPeriods.only_values() + ) + ], + responses={ + 200: WeatherByPeriodSerializer(many=True), + 400: MessageSerializer + } + ) + def get(self, request: Request, format=None): + serializer = WeatherByPeriodRequestSerializer(data=request.query_params) + serializer.is_valid(raise_exception=True) + period = serializer.validated_data['period'] + + now = timezone.now() + if period == WeatherPeriods.min_10: + start_time = now - timedelta(minutes=10) + trunc_func = TruncMinute + step = 1 + elif period == WeatherPeriods.min_30: + start_time = now - timedelta(minutes=30) + trunc_func = TruncMinute + step = 1 + elif period == WeatherPeriods.hour: + start_time = now - timedelta(hours=1) + trunc_func = TruncMinute + step = 10 + elif period == WeatherPeriods.hour_6: + start_time = now - timedelta(hours=6) + trunc_func = TruncHour + elif period == WeatherPeriods.hour_12: + start_time = now - timedelta(hours=12) + trunc_func = TruncHour + elif period == WeatherPeriods.hour_24: + start_time = now - timedelta(hours=24) + trunc_func = TruncHour + + qs = ( + WeatherStats.objects + .filter(created_at__gte=start_time) + .annotate(time_slot=trunc_func('created_at')) + .values('time_slot') + .annotate( + humidity_air=Avg('humidity_air'), + humidity_ground=Avg('humidity_ground'), + temperature=Avg('temperature'), + light=Avg('light') + ) + .order_by('-time_slot') + ) + + data_dict = {entry['date']: entry for entry in qs} + + slots = [] + current = start_time + while current <= now: + + if trunc_func == TruncMinute: + slot = current.replace(second=0, microsecond=0, minute=(current.minute // step) * step) + else: + slot = current.replace(minute=0, second=0, microsecond=0) + + entry = data_dict.get(slot, { + "date": slot, + "humidity_air": 0, + "humidity_ground": 0, + "temperature": 0, + "light": 0 + }) + slots.append(entry) + current += timedelta(minutes=step if trunc_func == TruncMinute else 60) + + serializer = WeatherByPeriodSerializer(slots, many=True) + return Response(serializer.data) @method_decorator(cache_page(CACHE_TIMEOUT_WEATHER), name='dispatch')