diff --git a/project/settings/base.py b/project/settings/base.py index e5976c5..d337904 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -157,3 +157,6 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +TELEGRAM_BOT_TOKEN = os.environ['TELEGRAM_BOT_TOKEN'] +TELEGRAM_CHAT_ID = os.environ['TELEGRAM_CHAT_ID'] \ No newline at end of file diff --git a/project/urls.py b/project/urls.py index 3c0e18b..0d2cd4c 100644 --- a/project/urls.py +++ b/project/urls.py @@ -24,6 +24,6 @@ urlpatterns = [ path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), path('api/', include([ - path('', include('weather.urls')), + path('weather/', include('weather.urls')), ])), ] diff --git a/weather/admin.py b/weather/admin.py index 5550a6a..d36e651 100644 --- a/weather/admin.py +++ b/weather/admin.py @@ -3,7 +3,7 @@ from .models import WeatherStats # Register your models here. class WeatherStatsAdmin(admin.ModelAdmin): - list_display = ('temperature', 'humidityAir', 'humidityGround', 'light', 'created_at') + list_display = ('temperature', 'humidity_air', 'humidity_ground', 'light', 'created_at') readonly_fields = ('created_at',) admin.site.register(WeatherStats, WeatherStatsAdmin) \ No newline at end of file diff --git a/weather/migrations/0002_rename_humidityair_weatherstats_humidity_air_and_more.py b/weather/migrations/0002_rename_humidityair_weatherstats_humidity_air_and_more.py new file mode 100644 index 0000000..15da764 --- /dev/null +++ b/weather/migrations/0002_rename_humidityair_weatherstats_humidity_air_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.8 on 2025-11-28 09:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('weather', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='weatherstats', + old_name='humidityAir', + new_name='humidity_air', + ), + migrations.RenameField( + model_name='weatherstats', + old_name='humidityGround', + new_name='humidity_ground', + ), + ] diff --git a/weather/models.py b/weather/models.py index f181b27..593e13e 100644 --- a/weather/models.py +++ b/weather/models.py @@ -2,8 +2,8 @@ from django.db import models # Create your models here. class WeatherStats(models.Model): - humidityAir = models.FloatField(default=0) - humidityGround = models.FloatField(default=0) + humidity_air = models.FloatField(default=0) + humidity_ground = models.FloatField(default=0) temperature = models.FloatField(default=0) light = models.FloatField(default=0) created_at = models.DateTimeField(auto_now_add=True) diff --git a/weather/serializers.py b/weather/serializers.py index e1102d6..a9caf96 100644 --- a/weather/serializers.py +++ b/weather/serializers.py @@ -6,4 +6,4 @@ class WeatherStatSerializer(serializers.ModelSerializer): class Meta: model = WeatherStats - fields = ('humidityAir', 'humidityGround', 'temperature', 'light', 'created_at') \ No newline at end of file + fields = ('humidity_air', 'humidity_ground', 'temperature', 'light', 'created_at') \ No newline at end of file diff --git a/weather/urls.py b/weather/urls.py index f06d9b0..a5afb5a 100644 --- a/weather/urls.py +++ b/weather/urls.py @@ -1,6 +1,8 @@ from django.urls import path -from .views import CreateStatAPI +from .views import * urlpatterns = [ - path('stats/', CreateStatAPI.as_view()) + path('create/', CreateStatAPI.as_view()), + path('last/', LastStatAPI.as_view()), + path('history/', StatsHistoryAPI.as_view()), ] diff --git a/weather/utils.py b/weather/utils.py index 1f13436..0e17ef6 100644 --- a/weather/utils.py +++ b/weather/utils.py @@ -1,11 +1,63 @@ import os +import requests +from django.db.models import Avg +from django.utils import timezone +from datetime import timedelta + +from rest_framework.pagination import PageNumberPagination from rest_framework.request import Request from rest_framework import permissions +from .models import WeatherStats +from project.settings_context import TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID + class IsMikrokontroller(permissions.BasePermission): def has_permission(self, request: Request, view): mikro_key = os.environ['MIKRO_SECRET_KEY'] request_key = request.headers.get('X-Mikro-Key') - return request_key == mikro_key \ No newline at end of file + return request_key == mikro_key + +class SmallPageNumberPagination(PageNumberPagination): + page_size = 200 + page_size_query_param = 'page_size' + max_page_size = 500 + +def format_value(val, unit="%"): + return f"{val:.1f}{unit}" if val is not None else "—" + +def send_telegram_stats(): + now = timezone.now() + thirty_minutes_ago = now - timedelta(minutes=30) + now_formatted = now.strftime("%d.%m.%Y %H:%M") + + # Collect data + averages = WeatherStats.objects.filter( + created_at__gte=thirty_minutes_ago + ).aggregate( + avg_humidity_air = Avg('humidity_air'), + avg_humidity_ground = Avg('humidity_ground'), + avg_temperature = Avg('temperature'), + avg_light = Avg('light') + ) + + message = f""" + Weather average stats for 30 mins ({now_formatted}) + +Humidity Air: {format_value(averages['avg_humidity_air'])} +Humidity Ground: {format_value(averages['avg_humidity_ground'])} +Humidity Temperature: {format_value(averages['avg_temperature', '°C'])} +Light: {format_value(averages['avg_light'])} +""" + + url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage" + params = { + "chat_id": TELEGRAM_CHAT_ID, + "text": message, + "parse_mode": "HTML" + } + + requests.get(url, params) + + \ No newline at end of file diff --git a/weather/views.py b/weather/views.py index 784036c..fd6dd63 100644 --- a/weather/views.py +++ b/weather/views.py @@ -1,13 +1,21 @@ -from rest_framework.generics import CreateAPIView -from drf_spectacular.utils import extend_schema, OpenApiParameter +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 .models import WeatherStats from .serializers import WeatherStatSerializer -from .utils import IsMikrokontroller +from .utils import IsMikrokontroller, PageNumberPagination + +CACHE_KEY_WEATHER = 'weather_latest_data' +CACHE_TIMEOUT = 30 # 30 seconds # 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)", + 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', @@ -21,4 +29,45 @@ from .utils import IsMikrokontroller class CreateStatAPI(CreateAPIView): serializer_class = WeatherStatSerializer permission_classes = [ IsMikrokontroller ] - queryset = WeatherStats.objects.all() \ No newline at end of file + queryset = WeatherStats.objects.all() + + def perform_create(self, serializer: WeatherStatSerializer): + new_data = serializer.validated_data + cached_data = cache.get(CACHE_KEY_WEATHER) + + if cached_data is None: + serializer.save() + + cache.set(CACHE_KEY_WEATHER, 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: OpenApiResponse( + description="No latest data available from microcontroller", + response={ + "message": "We don't have latest data from microcontroller" + } + ) + } + ) +class LastStatAPI(APIView): + def get(self, request: Request, format=None): + cached_data = cache.get(CACHE_KEY_WEATHER) + + 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 +