Coverage for backend/core/auxiliary/views/LiveSolarData.py: 33%
53 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
1import time
2import traceback
3import logging
4import requests
5import pandas as pd
6import re
7from io import StringIO
8from django.conf import settings
9from rest_framework.decorators import api_view
10from rest_framework.response import Response
11from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
12from core.auxiliary.serializers.LiveSolarDataSerializer import (
13 LiveSolarRequestSerializer,
14 LiveSolarDataResponseSerializer
15)
16from core.validation import api_view_validate
18logger = logging.getLogger(__name__)
21def build_url(lat, lon, from_date='2019-01-01', to_date='2019-12-31'):
22 return (
23 f"https://www.renewables.ninja/api/data/pv?"
24 f"lat={lat}&lon={lon}&date_from={from_date}&date_to={to_date}"
25 f"&capacity=1&dataset=merra2&system_loss=0.1&tracking=0"
26 f"&tilt=35&azim=180&raw=false&format=csv&local_time=true"
27 )
30def parse_csv(text: str):
31 """Parses the CSV returned by Renewables.ninja into structured dicts."""
33 # Handle HTML fallback
34 if text.strip().startswith("<!DOCTYPE html>"):
35 pattern = re.compile(
36 r'# Renewables\.ninja Weather.*?'
37 r'(time,t2m.*?\n.*?\d{4}-\d{2}-\d{2} 23:\d{2},.*?)(?=</pre>)',
38 re.DOTALL
39 )
40 match = pattern.search(text)
41 if match:
42 text = match.group(1)
43 else:
44 logger.warning("Could not parse CSV from HTML fallback content")
45 return []
47 # Read and format dataframe
48 df = pd.read_csv(StringIO(text), skiprows=3)
49 df.iloc[:, 0] = pd.to_datetime(df.iloc[:, 0], format='%Y-%m-%d %H:%M')
51 # Ensure column names match serializer expectations
52 df.rename(columns={
53 df.columns[0]: "time",
54 df.columns[-1]: "pv_output"
55 }, inplace=True)
57 return df.to_dict(orient="records")
60@extend_schema(
61 parameters=[
62 OpenApiParameter(name='flowsheet', type=OpenApiTypes.INT, location=OpenApiParameter.QUERY,
63 description="ID of the flowsheet to copy"),
64 OpenApiParameter(name='scenario_id', type=OpenApiTypes.INT, location=OpenApiParameter.QUERY,
65 description="Scenario ID for storing data"),
66 OpenApiParameter(name='lat', type=OpenApiTypes.FLOAT, location=OpenApiParameter.QUERY,
67 description="Latitude for the solar data request"),
68 OpenApiParameter(name='lon', type=OpenApiTypes.FLOAT, location=OpenApiParameter.QUERY,
69 description="Longitude for the solar data request"),
70 OpenApiParameter(name='from_date', type=OpenApiTypes.DATE, location=OpenApiParameter.QUERY,
71 description="Start date (YYYY-MM-DD)"),
72 OpenApiParameter(name='to_date', type=OpenApiTypes.DATE, location=OpenApiParameter.QUERY,
73 description="End date (YYYY-MM-DD)"),
74 ],
75 request=LiveSolarRequestSerializer, # Input schema
76 responses={200: LiveSolarDataResponseSerializer}, # Response schema
77)
78@api_view_validate
79@api_view(["GET"])
80def get_solar_data(request):
81 try:
82 lat = request.GET.get("lat")
83 lon = request.GET.get("lon")
84 from_date = request.GET.get("from_date", "2019-01-01")
85 to_date = request.GET.get("to_date", "2019-12-31")
87 if not lat or not lon:
88 return Response({"status": "error", "message": "lat and lon are required"}, status=400)
90 url = build_url(lat, lon, from_date, to_date)
91 headers = {"Authorization": f"Token {settings.RENEWABLES_NINJA_TOKEN}"}
93 logger.info(f"Calling Renewables.ninja API: {url}")
94 response = requests.get(url, headers=headers)
96 # Handle rate limiting explicitly
97 if response.status_code == 429:
98 logger.warning("Rate limit hit. Aborting further requests.")
99 return Response({
100 "status": "error",
101 "message": "Rate limit exceeded. Please try again later."
102 }, status=429)
104 # Continue normal processing if no rate limit error
105 response.raise_for_status()
106 parsed_data = parse_csv(response.text)
108 serializer = LiveSolarDataResponseSerializer({"status": "success", "data": parsed_data})
109 return Response(serializer.data, status=200)
111 except Exception as e:
112 logger.exception("Failed to fetch and process solar data")
113 return Response({
114 "status": "error",
115 "message": str(e),
116 "traceback": traceback.format_exc()
117 }, status=500)