Telegram bot and swagger
This commit is contained in:
parent
b3db1ca78f
commit
f563489328
@ -157,3 +157,6 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
|||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
TELEGRAM_BOT_TOKEN = os.environ['TELEGRAM_BOT_TOKEN']
|
||||||
|
TELEGRAM_CHAT_ID = os.environ['TELEGRAM_CHAT_ID']
|
||||||
@ -24,6 +24,6 @@ urlpatterns = [
|
|||||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||||
path('swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
path('swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||||
path('api/', include([
|
path('api/', include([
|
||||||
path('', include('weather.urls')),
|
path('weather/', include('weather.urls')),
|
||||||
])),
|
])),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from .models import WeatherStats
|
|||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
class WeatherStatsAdmin(admin.ModelAdmin):
|
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',)
|
readonly_fields = ('created_at',)
|
||||||
|
|
||||||
admin.site.register(WeatherStats, WeatherStatsAdmin)
|
admin.site.register(WeatherStats, WeatherStatsAdmin)
|
||||||
@ -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',
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -2,8 +2,8 @@ from django.db import models
|
|||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
class WeatherStats(models.Model):
|
class WeatherStats(models.Model):
|
||||||
humidityAir = models.FloatField(default=0)
|
humidity_air = models.FloatField(default=0)
|
||||||
humidityGround = models.FloatField(default=0)
|
humidity_ground = models.FloatField(default=0)
|
||||||
temperature = models.FloatField(default=0)
|
temperature = models.FloatField(default=0)
|
||||||
light = models.FloatField(default=0)
|
light = models.FloatField(default=0)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|||||||
@ -6,4 +6,4 @@ class WeatherStatSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WeatherStats
|
model = WeatherStats
|
||||||
fields = ('humidityAir', 'humidityGround', 'temperature', 'light', 'created_at')
|
fields = ('humidity_air', 'humidity_ground', 'temperature', 'light', 'created_at')
|
||||||
@ -1,6 +1,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .views import CreateStatAPI
|
from .views import *
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('stats/', CreateStatAPI.as_view())
|
path('create/', CreateStatAPI.as_view()),
|
||||||
|
path('last/', LastStatAPI.as_view()),
|
||||||
|
path('history/', StatsHistoryAPI.as_view()),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,11 +1,63 @@
|
|||||||
import os
|
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.request import Request
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
|
|
||||||
|
from .models import WeatherStats
|
||||||
|
from project.settings_context import TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
|
||||||
|
|
||||||
class IsMikrokontroller(permissions.BasePermission):
|
class IsMikrokontroller(permissions.BasePermission):
|
||||||
|
|
||||||
def has_permission(self, request: Request, view):
|
def has_permission(self, request: Request, view):
|
||||||
mikro_key = os.environ['MIKRO_SECRET_KEY']
|
mikro_key = os.environ['MIKRO_SECRET_KEY']
|
||||||
request_key = request.headers.get('X-Mikro-Key')
|
request_key = request.headers.get('X-Mikro-Key')
|
||||||
|
|
||||||
return request_key == mikro_key
|
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"""
|
||||||
|
<b> Weather average stats for 30 mins ({now_formatted})</b>
|
||||||
|
|
||||||
|
<b>Humidity Air:</b> {format_value(averages['avg_humidity_air'])}
|
||||||
|
<b>Humidity Ground:</b> {format_value(averages['avg_humidity_ground'])}
|
||||||
|
<b>Humidity Temperature:</b> {format_value(averages['avg_temperature', '°C'])}
|
||||||
|
<b>Light:</b> {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)
|
||||||
|
|
||||||
|
|
||||||
@ -1,13 +1,21 @@
|
|||||||
from rest_framework.generics import CreateAPIView
|
from django.core.cache import cache
|
||||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
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 .models import WeatherStats
|
||||||
from .serializers import WeatherStatSerializer
|
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.
|
# Create your views here.
|
||||||
@extend_schema(tags=['Weather'],
|
@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=[
|
parameters=[
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name='X-Mikro-Key',
|
name='X-Mikro-Key',
|
||||||
@ -21,4 +29,45 @@ from .utils import IsMikrokontroller
|
|||||||
class CreateStatAPI(CreateAPIView):
|
class CreateStatAPI(CreateAPIView):
|
||||||
serializer_class = WeatherStatSerializer
|
serializer_class = WeatherStatSerializer
|
||||||
permission_classes = [ IsMikrokontroller ]
|
permission_classes = [ IsMikrokontroller ]
|
||||||
queryset = WeatherStats.objects.all()
|
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
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user