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 from rest_framework.request import Request from rest_framework.response import Response from rest_framework import status from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse from project.serializers import MessageSerializer from .models import WeatherStats from .serializers import * from .utils import IsMikrokontroller, PageNumberPagination, get_weather_api_data CACHE_SAVE_CONTROL = 'stats_latest_saved_control' CACHE_KEY_STATS = 'stats_latest_data' CACHE_TIMEOUT = 30 # 30 seconds CACHE_KEY_WEATHER = 'weather_latest_data' CACHE_TIMEOUT_WEATHER = 60 * 5 # 5 minutes # Create your views here. @extend_schema(tags=['Weather'], description="Call method by mikrocontroller to set new data (required MIKRO_SECRET_KEY with header X-Mikro-Key) with delay 30 seconds", parameters=[ OpenApiParameter( name='X-Mikro-Key', type=str, location=OpenApiParameter.HEADER, description='Secret Key for microcontroller', required=True ) ] ) class CreateStatAPI(CreateAPIView): serializer_class = WeatherStatSerializer permission_classes = [ IsMikrokontroller ] queryset = WeatherStats.objects.all() def perform_create(self, serializer: WeatherStatSerializer): new_data = serializer.validated_data last_saved = cache.get(CACHE_SAVE_CONTROL) current_time = time.time() if last_saved is None or (current_time - last_saved) >= CACHE_TIMEOUT: serializer.save() cache.set(CACHE_SAVE_CONTROL, current_time, CACHE_TIMEOUT) cache.set(CACHE_KEY_STATS, new_data, CACHE_TIMEOUT) @extend_schema(tags=['Weather'], description="Get latest stat by microcontroller. Can be used to get actual data now", responses={ 200: WeatherStatSerializer, 404: MessageSerializer } ) class LastStatAPI(APIView): def get(self, request: Request, format=None): cached_data = cache.get(CACHE_KEY_STATS) if cached_data is None: return Response( {"message": "We don't have latest data from microcontroller. Maybe microcontroller is not connected"}, status=status.HTTP_404_NOT_FOUND ) return Response(cached_data) @extend_schema(tags=['Weather'], description="Get full history for graph") class StatsHistoryAPI(ListAPIView): serializer_class = WeatherStatSerializer 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') class OpenMeteoWeatherAPI(APIView): @extend_schema(tags=['Weather'], description="Weather prediction using OpenMeteo API data based on latitude and longitude for 7 days", parameters=[ OpenApiParameter( name='lat', type=float, location=OpenApiParameter.QUERY, description='Latitude', required=True ), OpenApiParameter( name='long', type=float, location=OpenApiParameter.QUERY, description='Longitude', required=True ) ], responses={ 200: OpenWeatherAPISerializer(many=True), 400: MessageSerializer } ) def get(self, request: Request, format=None): latitude = request.query_params.get('lat') longitude = request.query_params.get('long') if not latitude or not longitude: return Response("No latitude or longitude data", status=status.HTTP_400_BAD_REQUEST) weather_stats = get_weather_api_data(latitude, longitude) return Response(weather_stats)