This commit is contained in:
2026-04-11 03:54:15 +03:30
parent 883573004c
commit 36d6b05a7f
68 changed files with 3487 additions and 841 deletions
+158
View File
@@ -3,6 +3,164 @@ Static mock data for Yield & Harvest Prediction API.
Mirrors the yieldPredictionChart and harvestPredictionCard dashboard card shapes.
"""
CONFIG_SLIDERS_ONLY = {
"sliders": [
{
"key": "light",
"label": "نور",
"min": 0,
"max": 100,
"step": 5,
"unit_type": "percent",
"default_value": 75,
"icon": "☀️",
},
{
"key": "water",
"label": "آب",
"min": 0,
"max": 100,
"step": 5,
"unit_type": "percent",
"default_value": 65,
"icon": "💧",
},
{
"key": "soil_ph",
"label": "pH خاک",
"min": 4,
"max": 9,
"step": 0.5,
"unit_type": "number",
"unit": "",
"default_value": 6.5,
},
{
"key": "growth_speed",
"label": "سرعت رشد",
"min": 0.5,
"max": 5,
"step": 0.5,
"unit_type": "number",
"unit": "x",
"default_value": 1.5,
},
],
}
CONSTANTS = {
"max_height": 280,
"max_leaves": 14,
"max_branches": 6,
"max_yield": 500,
"yield_unit": "g",
"yield_rate_unit": "g/s",
"height_unit": "px",
}
CHART_CONFIG = {
"title": "پیشرفت رشد",
"x_axis_label": "زمان (ثانیه)",
"series": [
{
"key": "height",
"label": "ارتفاع (px)",
"y_axis_id": "yHeight",
"min": 0,
"max": 280,
"unit": "px",
},
{
"key": "leaves",
"label": "تعداد برگ",
"y_axis_id": "yLeaf",
"min": 0,
"max": 14,
},
{
"key": "yield",
"label": "محصول (g)",
"y_axis_id": "yYield",
"min": 0,
"max": 500,
"unit": "g",
},
{
"key": "yield_rate",
"label": "نرخ محصول (g/s)",
"y_axis_id": "yYieldRate",
"min": 0,
"unit": "g/s",
},
],
}
_labels = [f"{i * 0.2:.1f}s" for i in range(51)]
_height = [round(142 * (i / 50) ** 0.9) for i in range(51)]
_leaf = [min(5, int(i / 10)) for i in range(51)]
_yield = [round(12.4 * (i / 50) ** 1.2, 1) for i in range(51)]
_yield_rate = [round(0.087 * max(0, (i - 15) / 35), 3) for i in range(51)]
START_RESPONSE_DATA = {
"constants": CONSTANTS,
"chart": CHART_CONFIG,
"plant": {
"height": 142,
"leaves_count": 5,
"branches_count": 2,
"fruits_count": 0,
"yield": 12.4,
"yield_rate": 0.087,
"tick": 520,
"is_healthy": True,
"can_continue": True,
},
"progress": {
"growth_progress": 50,
"light_status": 75,
"water_status": 65,
"yield_progress": 2.5,
"yield_current": 12.4,
"yield_rate_current": 0.087,
},
"chart_history": {
"labels": _labels,
"height_history": _height,
"leaf_history": _leaf,
"yield_history": _yield,
"yield_rate_history": _yield_rate,
},
}
STATE_RESPONSE_DATA = {
"plant": {
"height": 142,
"leaves_count": 5,
"branches_count": 2,
"fruits_count": 0,
"yield": 12.4,
"yield_rate": 0.087,
"tick": 520,
"is_healthy": True,
"can_continue": True,
},
"progress": {
"growth_progress": 50,
"light_status": 75,
"water_status": 65,
"yield_progress": 2.5,
"yield_current": 12.4,
"yield_rate_current": 0.087,
},
"chart": {
"labels": _labels,
"height_history": _height,
"leaf_history": _leaf,
"yield_history": _yield,
"yield_rate_history": _yield_rate,
},
}
YIELD_PREDICTION_CARD = {
"id": "yield_prediction",
"title": "پیش‌بینی عملکرد",
+42
View File
@@ -1,5 +1,47 @@
from rest_framework import serializers
from .mock_data import CONFIG_SLIDERS_ONLY
START_ENVIRONMENT_KEYS = [
item["key"]
for item in CONFIG_SLIDERS_ONLY["sliders"]
if item["key"] != "growth_speed"
]
def _defaults_from_sliders():
return {
item["key"]: item["default_value"]
for item in CONFIG_SLIDERS_ONLY["sliders"]
}
START_REQUEST_EXAMPLE = {
"environment": {
key: value for key, value in _defaults_from_sliders().items() if key != "growth_speed"
},
"growth_speed": _defaults_from_sliders().get("growth_speed", 1.5),
}
START_REQUEST_EXAMPLE_STATIC = {
"environment": {
"light": 75,
"water": 65,
"soil_ph": 6.5,
},
"growth_speed": 1.5,
}
def success_response():
return {"status": "success"}
def success_with_data(data):
return {"status": "success", "data": data}
class YieldPredictionCardSerializer(serializers.Serializer):
id = serializers.CharField(required=False, allow_blank=True)
+37
View File
@@ -0,0 +1,37 @@
from copy import deepcopy
from .mock_data import HARVEST_PREDICTION_CARD, YIELD_PREDICTION_CARD, YIELD_PREDICTION_CHART
from .models import YieldHarvestPredictionLog
def get_yield_harvest_summary_data(farm=None):
data = {
"yield_prediction_card": deepcopy(YIELD_PREDICTION_CARD),
"yield_prediction_chart": deepcopy(YIELD_PREDICTION_CHART),
"harvest_prediction_card": deepcopy(HARVEST_PREDICTION_CARD),
}
if farm is None:
return data
log = YieldHarvestPredictionLog.objects.filter(farm=farm).first()
if log is None:
return data
if log.yield_stats:
data["yield_prediction_card"]["stats"] = log.yield_stats
if log.yield_chip_text:
data["yield_prediction_card"]["chipText"] = log.yield_chip_text
if log.chart_data:
data["yield_prediction_chart"] = deepcopy(log.chart_data)
if log.harvest_date:
data["harvest_prediction_card"]["date"] = log.harvest_date.isoformat()
if log.days_until_harvest is not None:
data["harvest_prediction_card"]["daysUntil"] = log.days_until_harvest
if log.optimal_window_start:
data["harvest_prediction_card"]["optimalWindowStart"] = log.optimal_window_start.isoformat()
if log.optimal_window_end:
data["harvest_prediction_card"]["optimalWindowEnd"] = log.optimal_window_end.isoformat()
return data
+25 -1
View File
@@ -1,6 +1,30 @@
from django.urls import path
from .views import YieldHarvestSummaryView
from .views import (
ConfigView,
EnvironmentView,
ResetView,
StartView,
StateView,
StopView,
YieldHarvestSummaryView,
)
ConfigView.__module__ = "plant_simulator.views"
EnvironmentView.__module__ = "plant_simulator.views"
ResetView.__module__ = "plant_simulator.views"
StartView.__module__ = "plant_simulator.views"
StateView.__module__ = "plant_simulator.views"
StopView.__module__ = "plant_simulator.views"
plant_simulator_urlpatterns = [
path("config/", ConfigView.as_view(), name="plant-simulator-config"),
path("state/", StateView.as_view(), name="plant-simulator-state"),
path("start/", StartView.as_view(), name="plant-simulator-start"),
path("stop/", StopView.as_view(), name="plant-simulator-stop"),
path("reset/", ResetView.as_view(), name="plant-simulator-reset"),
path("environment/", EnvironmentView.as_view(), name="plant-simulator-environment"),
]
urlpatterns = [
path("summary/", YieldHarvestSummaryView.as_view(), name="yield-harvest-summary"),
+62 -7
View File
@@ -1,12 +1,8 @@
"""
Yield & Harvest Prediction API views.
Response format: {"status": "success", "data": <payload>}. HTTP 200 only.
Fetches all three prediction payloads (yield card, yield chart, harvest card)
from the AI external adapter in a single call and persists a log entry
if a valid farm_uuid is provided.
Yield & Harvest Prediction and Plant Simulator API views.
"""
from rest_framework import status
from rest_framework import serializers, status
from rest_framework.response import Response
from rest_framework.views import APIView
from drf_spectacular.types import OpenApiTypes
@@ -15,8 +11,67 @@ from drf_spectacular.utils import OpenApiParameter, extend_schema
from config.swagger import status_response
from external_api_adapter import request as external_api_request
from farm_hub.models import FarmHub
from .mock_data import CONFIG_SLIDERS_ONLY, START_RESPONSE_DATA, STATE_RESPONSE_DATA
from .models import YieldHarvestPredictionLog
from .serializers import YieldHarvestSummarySerializer
from .serializers import YieldHarvestSummarySerializer, success_response, success_with_data
class ConfigView(APIView):
@extend_schema(
tags=["Plant Simulator"],
responses={200: status_response("PlantSimulatorConfigResponse", data=serializers.JSONField())},
)
def get(self, request):
return Response(success_with_data(CONFIG_SLIDERS_ONLY), status=status.HTTP_200_OK)
class StateView(APIView):
@extend_schema(
tags=["Plant Simulator"],
responses={200: status_response("PlantSimulatorStateResponse", data=serializers.JSONField())},
)
def get(self, request):
return Response(success_with_data(STATE_RESPONSE_DATA), status=status.HTTP_200_OK)
class StartView(APIView):
@extend_schema(
tags=["Plant Simulator"],
request=OpenApiTypes.OBJECT,
responses={200: status_response("PlantSimulatorStartResponse", data=serializers.JSONField())},
)
def post(self, request):
return Response(success_with_data(START_RESPONSE_DATA), status=status.HTTP_200_OK)
class StopView(APIView):
@extend_schema(
tags=["Plant Simulator"],
request=OpenApiTypes.OBJECT,
responses={200: status_response("PlantSimulatorStopResponse")},
)
def post(self, request):
return Response(success_response(), status=status.HTTP_200_OK)
class ResetView(APIView):
@extend_schema(
tags=["Plant Simulator"],
request=OpenApiTypes.OBJECT,
responses={200: status_response("PlantSimulatorResetResponse")},
)
def post(self, request):
return Response(success_response(), status=status.HTTP_200_OK)
class EnvironmentView(APIView):
@extend_schema(
tags=["Plant Simulator"],
request=OpenApiTypes.OBJECT,
responses={200: status_response("PlantSimulatorEnvironmentResponse")},
)
def patch(self, request):
return Response(success_response(), status=status.HTTP_200_OK)
class YieldHarvestSummaryView(APIView):