11'''Module to get entries from OpenSenseMap API and get the average temperature'''
2+ # pylint: disable=too-many-locals,too-many-branches,too-many-statements
23from datetime import datetime , timezone , timedelta
3- import re
4+ import json
45import requests
56import redis
67from app .config import create_redis_client , CACHE_TTL
@@ -26,16 +27,43 @@ def classify_temperature(average):
2627
2728 return "Unknown" # Default case
2829
30+ def _parse_partial_json_array (text : str ):
31+ """Parse as many full objects as possible from a (possibly truncated) JSON array."""
32+ decoder = json .JSONDecoder ()
33+ items = []
34+ i = text .find ('[' )
35+ if i == - 1 :
36+ return items
37+ i += 1 # past '['
38+ n = len (text )
39+ while i < n :
40+ while i < n and text [i ].isspace ():
41+ i += 1
42+ if i >= n or text [i ] == ']' :
43+ break
44+ try :
45+ obj , end = decoder .raw_decode (text , i )
46+ except json .JSONDecodeError :
47+ # truncated object at the end; stop with what we have
48+ break
49+ items .append (obj )
50+ i = end
51+ while i < n and text [i ].isspace ():
52+ i += 1
53+ if i < n and text [i ] == ',' :
54+ i += 1
55+ return items
56+
2957def get_temperature ():
3058 '''Function to get the average temperature from OpenSenseMap API.'''
3159 if REDIS_AVAILABLE :
3260 try :
3361 cached_data = redis_client .get ("temperature_data" )
3462 if cached_data :
3563 print ("Using cached data from Redis." )
36- # Return cached data with default stats (since we don't have fresh stats )
64+ cached_result = cached_data . decode ( 'utf-8' )
3765 default_stats = {"total_sensors" : 0 , "null_count" : 0 }
38- return cached_data , default_stats
66+ return cached_result , default_stats
3967 except redis .RedisError as e :
4068 print (f"Redis error: { e } . Proceeding without cache." )
4169
@@ -49,41 +77,86 @@ def get_temperature():
4977 "format" : "json"
5078 }
5179
80+ # Streaming configuration
81+ max_mb = 0.5
82+ max_bytes = int (max_mb * 1024 * 1024 )
83+
5284 print ('Getting data from OpenSenseMap API...' )
85+
5386 try :
87+ # Stream the response and count bytes
5488 response = requests .get (
5589 "https://api.opensensemap.org/boxes" ,
5690 params = params ,
57- timeout = (3 , 10 )
91+ stream = True ,
92+ timeout = (180 , 60 )
5893 )
59- print ('Data retrieved successfully!' )
94+ response .raise_for_status ()
95+
96+ downloaded = 0
97+ chunks = []
98+ truncated = False
99+
100+ for chunk in response .iter_content (chunk_size = 64 * 1024 ): # 64 KB
101+ if not chunk :
102+ break
103+ chunks .append (chunk )
104+ downloaded += len (chunk )
105+ if downloaded >= max_bytes :
106+ print (f"Reached { max_mb } MB limit ({ downloaded :,} bytes), stopping download" )
107+ truncated = True
108+ response .close ()
109+ break
110+
111+ print (f'Bytes downloaded: { downloaded :,} ' )
112+ print ('Data retrieved successfully!' + (" (partial)" if truncated else "" ))
113+
114+ # Build body and parse JSON
115+ body = b"" .join (chunks )
116+ text = body .decode (response .encoding or "utf-8" , errors = "replace" )
117+
118+ try :
119+ data = json .loads (text )
120+ except json .JSONDecodeError :
121+ if not truncated :
122+ print ("Warning: Unexpected JSON parse error. Trying partial parse." )
123+ data = _parse_partial_json_array (text )
124+ if not data :
125+ return "Error: Failed to parse JSON and no partial objects found\n " , {
126+ "total_sensors" : 0 ,
127+ "null_count" : 0
128+ }
129+
60130 except requests .Timeout :
61131 print ("API request timed out" )
62132 return "Error: API request timed out\n " , {"total_sensors" : 0 , "null_count" : 0 }
63133 except requests .RequestException as e :
64134 print (f"API request failed: { e } " )
65135 return f"Error: API request failed - { e } \n " , {"total_sensors" : 0 , "null_count" : 0 }
66136
67- _sensor_stats ["total_sensors" ] = sum (
68- 1 for line in response .text .splitlines () if re .search (r'^\s*"sensors"\s*:\s*\[' , line )
69- )
70-
71- res = [d .get ('sensors' ) for d in response .json () if 'sensors' in d ]
137+ # Process the data (keeping the existing logic)
138+ _sensor_stats ["total_sensors" ] = sum (1 for d in data if isinstance (d , dict ) and "sensors" in d )
139+ res = [d .get ('sensors' ) for d in data if isinstance (d , dict ) and 'sensors' in d ]
72140
73141 temp_list = []
74- _sensor_stats ["null_count" ] = 0 # Initialize counter for null measurements
142+ _sensor_stats ["null_count" ] = 0
75143
76144 for sensor_list in res :
77145 for measure in sensor_list :
78146 if measure .get ('unit' ) == "°C" and 'lastMeasurement' in measure :
79- last_measurement = measure ['lastMeasurement' ]
80- if last_measurement is not None and 'value' in last_measurement :
81- last_measurement_int = float (last_measurement ['value' ])
82- temp_list .append (last_measurement_int )
147+ last = measure ['lastMeasurement' ]
148+ if last is not None and isinstance (last , dict ) and 'value' in last :
149+ try :
150+ temp_list .append (float (last ['value' ]))
151+ except (TypeError , ValueError ):
152+ _sensor_stats ["null_count" ] += 1
83153 else :
84154 _sensor_stats ["null_count" ] += 1
85155
86- average = sum (temp_list ) / len (temp_list ) if temp_list else 0
156+ average = sum (temp_list ) / len (temp_list ) if temp_list else 0.0
157+
158+ if not temp_list :
159+ print ("Warning: No valid temperature readings found" )
87160
88161 # Use the dictionary-based classification
89162 status = classify_temperature (average )
0 commit comments