1use std::collections::HashMap;
2use std::error::Error;
3use std::fs::File;
4use std::io::{BufRead, BufReader};
5use serde_json::Value;
6
7pub fn retrieve_sam(name: &str) -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
23 let url = match name {
24 "CEC Inverters" | "cecinverter" => "https://raw.githubusercontent.com/NREL/SAM/patch/deploy/libraries/CEC%20Inverters.csv",
25 "CEC Modules" | "cecmod" => "https://raw.githubusercontent.com/NREL/SAM/patch/deploy/libraries/CEC%20Modules.csv",
26 _ => return Err(format!("Unknown SAM DB string. Please use 'CEC Inverters' or 'CEC Modules'. You provided: {}", name).into()),
27 };
28
29 let response = reqwest::blocking::get(url)?.text()?;
30
31 let mut reader = csv::ReaderBuilder::new()
32 .flexible(true)
33 .from_reader(response.as_bytes());
34
35 let headers = reader.headers()?.clone();
36
37 let mut records = Vec::new();
38 for result in reader.records() {
39 let record = result?;
40 let mut map = HashMap::new();
41 for (i, field) in record.iter().enumerate() {
42 if let Some(header) = headers.get(i) {
43 map.insert(header.to_string(), field.to_string());
44 }
45 }
46 records.push(map);
50 }
51
52 Ok(records)
53}
54
55#[derive(Debug, Clone)]
61pub struct WeatherRecord {
62 pub time: String,
64 pub ghi: f64,
66 pub dni: f64,
68 pub dhi: f64,
70 pub temp_air: f64,
72 pub wind_speed: f64,
74 pub pressure: f64,
76 pub relative_humidity: f64,
78 pub infrared: Option<f64>,
80 pub wind_direction: Option<f64>,
82 pub temp_dew: Option<f64>,
84 pub albedo: Option<f64>,
86 pub precipitable_water: Option<f64>,
88 pub year: Option<i32>,
90 pub month: Option<u32>,
92 pub day: Option<u32>,
94 pub hour: Option<u32>,
96}
97
98#[derive(Debug, Clone)]
100pub struct WeatherMetadata {
101 pub latitude: f64,
102 pub longitude: f64,
103 pub elevation: Option<f64>,
104 pub tz_offset: Option<f64>,
106 pub name: Option<String>,
108 pub city: Option<String>,
110 pub state: Option<String>,
112 pub source: Option<String>,
114 pub months_selected: Option<Vec<MonthYear>>,
116 pub extra: HashMap<String, String>,
118}
119
120#[derive(Debug, Clone)]
122pub struct MonthYear {
123 pub month: i32,
124 pub year: i32,
125}
126
127#[derive(Debug, Clone)]
129pub struct WeatherData {
130 pub records: Vec<WeatherRecord>,
131 pub metadata: WeatherMetadata,
132}
133
134const PVGIS_BASE_URL: &str = "https://re.jrc.ec.europa.eu/api/v5_3/";
139
140pub fn get_pvgis_tmy(
153 latitude: f64,
154 longitude: f64,
155 outputformat: &str,
156 startyear: Option<i32>,
157 endyear: Option<i32>,
158) -> Result<WeatherData, Box<dyn Error>> {
159 let mut url = format!(
160 "{}tmy?lat={}&lon={}&outputformat={}",
161 PVGIS_BASE_URL, latitude, longitude, outputformat,
162 );
163 if let Some(sy) = startyear {
164 url.push_str(&format!("&startyear={}", sy));
165 }
166 if let Some(ey) = endyear {
167 url.push_str(&format!("&endyear={}", ey));
168 }
169
170 let response = reqwest::blocking::get(&url)?.text()?;
171
172 match outputformat {
173 "json" => parse_pvgis_tmy_json(&response),
174 _ => Err(format!("Unsupported PVGIS TMY outputformat: '{}'. Use 'json'.", outputformat).into()),
175 }
176}
177
178pub fn get_pvgis_hourly(
190 latitude: f64,
191 longitude: f64,
192 start: i32,
193 end: i32,
194 pvcalculation: bool,
195 peakpower: Option<f64>,
196 surface_tilt: Option<f64>,
197 surface_azimuth: Option<f64>,
198) -> Result<WeatherData, Box<dyn Error>> {
199 let tilt = surface_tilt.unwrap_or(0.0);
200 let aspect = surface_azimuth.unwrap_or(180.0) - 180.0;
202 let pvcalc_int = if pvcalculation { 1 } else { 0 };
203
204 let mut url = format!(
205 "{}seriescalc?lat={}&lon={}&startyear={}&endyear={}&pvcalculation={}&angle={}&aspect={}&outputformat=json",
206 PVGIS_BASE_URL, latitude, longitude, start, end, pvcalc_int, tilt, aspect,
207 );
208 if let Some(pp) = peakpower {
209 url.push_str(&format!("&peakpower={}", pp));
210 }
211
212 let response = reqwest::blocking::get(&url)?.text()?;
213 parse_pvgis_hourly_json(&response)
214}
215
216pub fn get_pvgis_horizon(
221 latitude: f64,
222 longitude: f64,
223) -> Result<Vec<(f64, f64)>, Box<dyn Error>> {
224 let url = format!(
225 "{}printhorizon?lat={}&lon={}&outputformat=json",
226 PVGIS_BASE_URL, latitude, longitude,
227 );
228
229 let response = reqwest::blocking::get(&url)?.text()?;
230 parse_pvgis_horizon_json(&response)
231}
232
233pub fn parse_pvgis_tmy_json(json_str: &str) -> Result<WeatherData, Box<dyn Error>> {
239 let root: Value = serde_json::from_str(json_str)?;
240
241 let inputs = &root["inputs"]["location"];
243 let latitude = inputs["latitude"].as_f64().unwrap_or(0.0);
244 let longitude = inputs["longitude"].as_f64().unwrap_or(0.0);
245 let elevation = inputs["elevation"].as_f64();
246
247 let months_selected = root["outputs"]["months_selected"]
249 .as_array()
250 .map(|arr| {
251 arr.iter()
252 .map(|m| MonthYear {
253 month: m["month"].as_i64().unwrap_or(0) as i32,
254 year: m["year"].as_i64().unwrap_or(0) as i32,
255 })
256 .collect()
257 });
258
259 let hourly = root["outputs"]["tmy_hourly"]
261 .as_array()
262 .ok_or("Missing outputs.tmy_hourly in PVGIS TMY response")?;
263
264 let records: Vec<WeatherRecord> = hourly
265 .iter()
266 .map(|h| WeatherRecord {
267 time: h["time(UTC)"].as_str().unwrap_or("").to_string(),
268 ghi: h["G(h)"].as_f64().unwrap_or(0.0),
269 dni: h["Gb(n)"].as_f64().unwrap_or(0.0),
270 dhi: h["Gd(h)"].as_f64().unwrap_or(0.0),
271 temp_air: h["T2m"].as_f64().unwrap_or(0.0),
272 wind_speed: h["WS10m"].as_f64().unwrap_or(0.0),
273 pressure: h["SP"].as_f64().unwrap_or(0.0),
274 relative_humidity: h["RH"].as_f64().unwrap_or(0.0),
275 infrared: h["IR(h)"].as_f64(),
276 wind_direction: h["WD10m"].as_f64(),
277 temp_dew: None,
278 albedo: None,
279 precipitable_water: None,
280 year: None,
281 month: None,
282 day: None,
283 hour: None,
284 })
285 .collect();
286
287 Ok(WeatherData {
288 records,
289 metadata: WeatherMetadata {
290 latitude,
291 longitude,
292 elevation,
293 tz_offset: None,
294 name: None,
295 city: None,
296 state: None,
297 source: None,
298 months_selected,
299 extra: HashMap::new(),
300 },
301 })
302}
303
304pub fn parse_pvgis_hourly_json(json_str: &str) -> Result<WeatherData, Box<dyn Error>> {
306 let root: Value = serde_json::from_str(json_str)?;
307
308 let inputs = &root["inputs"]["location"];
309 let latitude = inputs["latitude"].as_f64().unwrap_or(0.0);
310 let longitude = inputs["longitude"].as_f64().unwrap_or(0.0);
311 let elevation = inputs["elevation"].as_f64();
312
313 let hourly = root["outputs"]["hourly"]
314 .as_array()
315 .ok_or("Missing outputs.hourly in PVGIS hourly response")?;
316
317 let records: Vec<WeatherRecord> = hourly
318 .iter()
319 .map(|h| WeatherRecord {
320 time: h["time"].as_str().unwrap_or("").to_string(),
321 ghi: h["G(h)"].as_f64().unwrap_or(0.0),
322 dni: h["Gb(n)"].as_f64().unwrap_or(0.0),
323 dhi: h["Gd(h)"].as_f64().unwrap_or(0.0),
324 temp_air: h["T2m"].as_f64().unwrap_or(0.0),
325 wind_speed: h["WS10m"].as_f64().unwrap_or(0.0),
326 pressure: h["SP"].as_f64().unwrap_or(0.0),
327 relative_humidity: h["RH"].as_f64().unwrap_or(0.0),
328 infrared: h["IR(h)"].as_f64(),
329 wind_direction: h["WD10m"].as_f64(),
330 temp_dew: None,
331 albedo: None,
332 precipitable_water: None,
333 year: None,
334 month: None,
335 day: None,
336 hour: None,
337 })
338 .collect();
339
340 Ok(WeatherData {
341 records,
342 metadata: WeatherMetadata {
343 latitude,
344 longitude,
345 elevation,
346 tz_offset: None,
347 name: None,
348 city: None,
349 state: None,
350 source: None,
351 months_selected: None,
352 extra: HashMap::new(),
353 },
354 })
355}
356
357pub fn parse_pvgis_horizon_json(json_str: &str) -> Result<Vec<(f64, f64)>, Box<dyn Error>> {
360 let root: Value = serde_json::from_str(json_str)?;
361
362 let profile = root["outputs"]["horizon_profile"]
363 .as_array()
364 .ok_or("Missing outputs.horizon_profile in PVGIS horizon response")?;
365
366 let mut result: Vec<(f64, f64)> = profile
367 .iter()
368 .map(|p| {
369 let az = p["A"].as_f64().unwrap_or(0.0);
370 let el = p["H_hor"].as_f64().unwrap_or(0.0);
371 let az_pvlib = az + 180.0;
373 (az_pvlib, el)
374 })
375 .collect();
376
377 result.retain(|&(az, _)| az < 360.0);
379
380 Ok(result)
381}
382
383pub fn read_tmy3(filepath: &str) -> Result<WeatherData, Box<dyn Error>> {
395 let file = File::open(filepath)?;
396 let reader = BufReader::new(file);
397 let mut lines = reader.lines();
398
399 let meta_line = lines.next().ok_or("TMY3 file is empty")??;
401 let meta_fields: Vec<&str> = meta_line.split(',').collect();
402 if meta_fields.len() < 7 {
403 return Err("TMY3 metadata line has fewer than 7 fields".into());
404 }
405 let metadata = WeatherMetadata {
406 latitude: meta_fields[4].trim().parse()?,
407 longitude: meta_fields[5].trim().parse()?,
408 elevation: Some(meta_fields[6].trim().parse()?),
409 tz_offset: Some(meta_fields[3].trim().parse()?),
410 name: Some(meta_fields[1].trim().to_string()),
411 city: Some(meta_fields[1].trim().to_string()),
412 state: Some(meta_fields[2].trim().to_string()),
413 source: Some(format!("USAF {}", meta_fields[0].trim())),
414 months_selected: None,
415 extra: HashMap::new(),
416 };
417
418 let header_line = lines.next().ok_or("TMY3 file missing column header line")??;
420 let headers: Vec<String> = header_line.split(',').map(|s| s.trim().to_string()).collect();
421
422 let idx = |name: &str| -> Result<usize, Box<dyn Error>> {
423 headers.iter().position(|h| h == name)
424 .ok_or_else(|| format!("TMY3 column '{}' not found", name).into())
425 };
426 let i_date = idx("Date (MM/DD/YYYY)")?;
427 let i_time = idx("Time (HH:MM)")?;
428 let i_ghi = idx("GHI (W/m^2)")?;
429 let i_dni = idx("DNI (W/m^2)")?;
430 let i_dhi = idx("DHI (W/m^2)")?;
431 let i_temp = idx("Dry-bulb (C)")?;
432 let i_dew = idx("Dew-point (C)")?;
433 let i_rh = idx("RHum (%)")?;
434 let i_pres = idx("Pressure (mbar)")?;
435 let i_wdir = idx("Wdir (degrees)")?;
436 let i_wspd = idx("Wspd (m/s)")?;
437 let i_alb = idx("Alb (unitless)")?;
438 let i_pwat = idx("Pwat (cm)")?;
439
440 let mut records = Vec::new();
441 for line_result in lines {
442 let line = line_result?;
443 if line.trim().is_empty() {
444 continue;
445 }
446 let fields: Vec<&str> = line.split(',').collect();
447
448 let date_parts: Vec<&str> = fields[i_date].split('/').collect();
450 let month: u32 = date_parts[0].parse()?;
451 let day: u32 = date_parts[1].parse()?;
452 let year: i32 = date_parts[2].parse()?;
453
454 let time_parts: Vec<&str> = fields[i_time].split(':').collect();
456 let raw_hour: u32 = time_parts[0].parse()?;
457 let hour = raw_hour % 24;
458
459 let parse_f64 = |i: usize| -> Result<f64, Box<dyn Error>> {
460 fields.get(i).ok_or_else(|| format!("missing field {}", i))?
461 .trim().parse::<f64>().map_err(|e| e.into())
462 };
463
464 let time_str = format!("{:04}{:02}{:02}:{:02}{:02}",
465 year, month, day, hour, 0);
466
467 records.push(WeatherRecord {
468 time: time_str,
469 ghi: parse_f64(i_ghi)?,
470 dni: parse_f64(i_dni)?,
471 dhi: parse_f64(i_dhi)?,
472 temp_air: parse_f64(i_temp)?,
473 wind_speed: parse_f64(i_wspd)?,
474 pressure: parse_f64(i_pres)?,
475 relative_humidity: parse_f64(i_rh)?,
476 infrared: None,
477 wind_direction: Some(parse_f64(i_wdir)?),
478 temp_dew: Some(parse_f64(i_dew)?),
479 albedo: Some(parse_f64(i_alb)?),
480 precipitable_water: Some(parse_f64(i_pwat)?),
481 year: Some(year),
482 month: Some(month),
483 day: Some(day),
484 hour: Some(hour),
485 });
486 }
487
488 Ok(WeatherData { metadata, records })
489}
490
491pub fn read_epw(filepath: &str) -> Result<WeatherData, Box<dyn Error>> {
498 let file = File::open(filepath)?;
499 let reader = BufReader::new(file);
500 let mut lines = reader.lines();
501
502 let loc_line = lines.next().ok_or("EPW file is empty")??;
504 let loc_fields: Vec<&str> = loc_line.split(',').collect();
505 if loc_fields.len() < 10 {
506 return Err("EPW LOCATION line has fewer than 10 fields".into());
507 }
508 let metadata = WeatherMetadata {
509 latitude: loc_fields[6].trim().parse()?,
510 longitude: loc_fields[7].trim().parse()?,
511 elevation: Some(loc_fields[9].trim().parse()?),
512 tz_offset: Some(loc_fields[8].trim().parse()?),
513 name: Some(loc_fields[1].trim().to_string()),
514 city: Some(loc_fields[1].trim().to_string()),
515 state: Some(loc_fields[2].trim().to_string()),
516 source: Some(loc_fields[4].trim().to_string()),
517 months_selected: None,
518 extra: HashMap::new(),
519 };
520
521 for _ in 0..7 {
523 lines.next().ok_or("EPW file has fewer than 8 header lines")??;
524 }
525
526 let mut records = Vec::new();
533 for line_result in lines {
534 let line = line_result?;
535 if line.trim().is_empty() {
536 continue;
537 }
538 let fields: Vec<&str> = line.split(',').collect();
539 if fields.len() < 29 {
540 continue;
541 }
542
543 let parse_f64 = |i: usize| -> Result<f64, Box<dyn Error>> {
544 fields[i].trim().parse::<f64>().map_err(|e| e.into())
545 };
546
547 let try_parse_f64 = |i: usize| -> Option<f64> {
548 fields.get(i).and_then(|s| s.trim().parse::<f64>().ok())
549 };
550
551 let raw_hour: u32 = fields[3].trim().parse()?;
553 let hour = if raw_hour == 0 { 0 } else { raw_hour - 1 };
554 let year: i32 = fields[0].trim().parse()?;
555 let month: u32 = fields[1].trim().parse()?;
556 let day: u32 = fields[2].trim().parse()?;
557
558 let time_str = format!("{:04}{:02}{:02}:{:02}{:02}",
559 year, month, day, hour, 0);
560
561 records.push(WeatherRecord {
570 time: time_str,
571 temp_air: parse_f64(6)?,
572 wind_speed: parse_f64(21)?,
573 pressure: parse_f64(9)?,
574 relative_humidity: parse_f64(8)?,
575 ghi: parse_f64(13)?,
576 dni: parse_f64(14)?,
577 dhi: parse_f64(15)?,
578 infrared: Some(parse_f64(12)?),
579 wind_direction: Some(parse_f64(20)?),
580 temp_dew: Some(parse_f64(7)?),
581 precipitable_water: try_parse_f64(28),
582 albedo: try_parse_f64(32),
583 year: Some(year),
584 month: Some(month),
585 day: Some(day),
586 hour: Some(hour),
587 });
588 }
589
590 Ok(WeatherData { metadata, records })
591}