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
+