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

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 

17 

18logger = logging.getLogger(__name__) 

19 

20 

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 ) 

28 

29 

30def parse_csv(text: str): 

31 """Parses the CSV returned by Renewables.ninja into structured dicts.""" 

32 

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 [] 

46 

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') 

50 

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) 

56 

57 return df.to_dict(orient="records") 

58 

59 

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") 

86 

87 if not lat or not lon: 

88 return Response({"status": "error", "message": "lat and lon are required"}, status=400) 

89 

90 url = build_url(lat, lon, from_date, to_date) 

91 headers = {"Authorization": f"Token {settings.RENEWABLES_NINJA_TOKEN}"} 

92 

93 logger.info(f"Calling Renewables.ninja API: {url}") 

94 response = requests.get(url, headers=headers) 

95 

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) 

103 

104 # Continue normal processing if no rate limit error 

105 response.raise_for_status() 

106 parsed_data = parse_csv(response.text) 

107 

108 serializer = LiveSolarDataResponseSerializer({"status": "success", "data": parsed_data}) 

109 return Response(serializer.data, status=200) 

110 

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)