Weather Integration
Ampra integrates weather data from the Open-Meteo API to provide correlated insights and to serve as input features for the ML prediction models.
Data Source
| Provider | Open-Meteo |
|---|---|
| Authentication | None required (free tier) |
| Forecast Window | 8 days forward |
| Resolution | Daily |
| Rate Limits | 10,000 requests/day (free tier) |
Weather Data Fields
| Field | Unit | Description |
|---|---|---|
WeatherCode | WMO code | Standardized weather condition code |
TemperatureMax | °C | Daily maximum temperature |
TemperatureMin | °C | Daily minimum temperature |
ShortwaveRadiationSum | MJ/m² | Total solar radiation for the day |
UvIndexMax | Index | Maximum UV index |
PrecipitationSum | mm | Total precipitation |
Sunrise | ISO 8601 | Sunrise time (local timezone) |
Sunset | ISO 8601 | Sunset time (local timezone) |
Fetch & Storage Flow
RefreshWeatherDataAsync Implementation
public async Task RefreshWeatherDataAsync(SunSource sunSource)
{
if (sunSource.Latitude == null || sunSource.Longitude == null)
{
_logger.LogWarning("Cannot refresh weather for SunSource {Id} — no coordinates.", sunSource.Id);
return;
}
var lat = sunSource.Latitude.Value;
var lon = sunSource.Longitude.Value;
// Build Open-Meteo API URL with all required daily fields
var url = $"https://api.open-meteo.com/v1/forecast"
+ $"?latitude={lat}&longitude={lon}"
+ "&daily=weather_code,temperature_2m_max,temperature_2m_min,"
+ "sunrise,sunset,uv_index_max,precipitation_sum,shortwave_radiation_sum"
+ "&timezone=UTC&forecast_days=8";
var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<OpenMeteoResponse>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (data?.Daily == null) return;
var daily = data.Daily;
var count = daily.Time.Count;
// Parse date strings into UTC DateTime values
var parsedDates = new List<DateTime>();
for (int i = 0; i < count; i++)
{
if (DateTime.TryParse(daily.Time[i], out var d))
parsedDates.Add(DateTime.SpecifyKind(d.Date, DateTimeKind.Utc));
else
parsedDates.Add(DateTime.MinValue);
}
// Batch-load existing records to determine insert vs update
var validDates = parsedDates.Where(d => d != DateTime.MinValue).ToList();
var existingRecords = await _context.WeatherData
.Where(w => w.SunSourceId == sunSource.Id && validDates.Contains(w.Date))
.ToDictionaryAsync(w => w.Date);
// Upsert loop: update existing records or create new ones
for (int i = 0; i < count; i++)
{
var dateOnly = parsedDates[i];
if (dateOnly == DateTime.MinValue) continue;
if (!existingRecords.TryGetValue(dateOnly, out var existing))
{
existing = new WeatherData
{
SunSourceId = sunSource.Id,
Date = dateOnly,
CreatedAt = DateTime.UtcNow
};
_context.WeatherData.Add(existing);
existingRecords[dateOnly] = existing;
}
// Safe index-based field mapping with bounds checking
existing.WeatherCode = daily.WeatherCode?.Count > i ? daily.WeatherCode[i] : null;
existing.TemperatureMax = daily.Temperature2mMax?.Count > i ? daily.Temperature2mMax[i] : null;
existing.TemperatureMin = daily.Temperature2mMin?.Count > i ? daily.Temperature2mMin[i] : null;
existing.UvIndexMax = daily.UvIndexMax?.Count > i ? daily.UvIndexMax[i] : null;
existing.PrecipitationSum = daily.PrecipitationSum?.Count > i ? daily.PrecipitationSum[i] : null;
existing.ShortwaveRadiationSum = daily.ShortwaveRadiationSum?.Count > i
? daily.ShortwaveRadiationSum[i] : null;
if (daily.Sunrise?.Count > i && DateTime.TryParse(daily.Sunrise[i], out var sunrise))
existing.Sunrise = DateTime.SpecifyKind(sunrise, DateTimeKind.Utc);
if (daily.Sunset?.Count > i && DateTime.TryParse(daily.Sunset[i], out var sunset))
existing.Sunset = DateTime.SpecifyKind(sunset, DateTimeKind.Utc);
}
await _context.SaveChangesAsync();
}
Open-Meteo Response DTO
The service uses private nested classes to deserialize the API response:
private class OpenMeteoResponse
{
public DailyData Daily { get; set; } = null!;
}
private class DailyData
{
public List<string> Time { get; set; } = new();
[JsonPropertyName("weather_code")]
public List<int?> WeatherCode { get; set; } = new();
[JsonPropertyName("temperature_2m_max")]
public List<double?> Temperature2mMax { get; set; } = new();
[JsonPropertyName("temperature_2m_min")]
public List<double?> Temperature2mMin { get; set; } = new();
[JsonPropertyName("uv_index_max")]
public List<double?> UvIndexMax { get; set; } = new();
[JsonPropertyName("precipitation_sum")]
public List<double?> PrecipitationSum { get; set; } = new();
[JsonPropertyName("shortwave_radiation_sum")]
public List<double?> ShortwaveRadiationSum { get; set; } = new();
public List<string> Sunrise { get; set; } = new();
public List<string> Sunset { get; set; } = new();
}
GetWeatherDataAsync — Filtered Query
The public API only returns today and forward weather data, even though historical records are retained for ML:
public async Task<List<WeatherData>> GetWeatherDataAsync(Guid sunSourceId)
{
var today = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
return await _context.WeatherData
.Where(w => w.SunSourceId == sunSourceId && w.Date >= today)
.OrderBy(w => w.Date)
.ToListAsync();
}
API Endpoints
Get Weather Data
GET /api/weather/{sunSourceId}
Returns weather data for the source, filtered to today and forward only. Historical weather data is retained in the database for ML training but not returned by this endpoint.
Refresh Weather Data
POST /api/weather/{sunSourceId}/refresh
Manually triggers a weather data refresh from Open-Meteo. Returns HTTP 503 if the external API is unavailable.
Automated Weather Updates
The UpdateWeatherDataJob runs every 6 hours via Quartz:
UpdateWeatherDataJob Implementation
[DisallowConcurrentExecution]
public class UpdateWeatherDataJob : IJob
{
private readonly ApplicationDbContext _dbContext;
private readonly IWeatherService _weatherService;
private readonly ILogger<UpdateWeatherDataJob> _logger;
public async Task Execute(IJobExecutionContext context)
{
// 1. Find users who opted into auto weather updates
var optedInUserIds = await _dbContext.UserSettings
.Where(s => s.SettingType == (int)SettingType.AutoUpdateWeatherData
&& s.Value == "1")
.Select(s => s.UserId)
.ToListAsync();
// 2. Get all active sources for those users
var sunSources = await _dbContext.SunSources
.Where(s => optedInUserIds.Contains(s.UserId) && s.IsActive)
.ToListAsync();
_logger.LogInformation("Updating weather for {Count} sun sources across {UserCount} opted-in users",
sunSources.Count, optedInUserIds.Count);
// 3. Refresh weather for each source (fail-safe: errors don't stop the loop)
foreach (var sunSource in sunSources)
{
try
{
await _weatherService.RefreshWeatherDataAsync(sunSource);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update weather for source {SunSourceId}", sunSource.Id);
}
}
}
}
Important: This job runs before the
DailyPredictionJobin the Quartz schedule, ensuring fresh weather data is available when predictions are generated.
Schedule
| Trigger | Cron Expression | Frequency |
|---|---|---|
UpdateWeatherDataTrigger | 0 0 */6 * * ? | Every 6 hours (00:00, 06:00, 12:00, 18:00 UTC) |
Weather Codes (WMO)
The weather_code field follows the WMO 4677 standard. Common codes used by Ampra:
| Code | Description | Cloud Attenuation (ML) |
|---|---|---|
| 0 | Clear sky | 0% |
| 1 | Mainly clear | 10% |
| 2 | Partly cloudy | 25% |
| 3 | Overcast | 50% |
| 45, 48 | Fog | 60% |
| 51-55 | Drizzle | 55% |
| 61-65 | Rain | 65% |
| 71-75 | Snowfall | 75% |
| 80-82 | Rain showers | 60% |
| 95-99 | Thunderstorm | 80% |
These attenuation factors are used in the ML prediction engine's physics model to adjust clear-sky GHI estimates based on actual weather conditions.
Weather Data in ML Pipeline
Weather data serves as critical input features for the prediction engine:
| Feature | Source | Impact |
|---|---|---|
shortwave_radiation_sum | Open-Meteo | Primary indicator of solar potential |
temperature_max / temperature_min | Open-Meteo | Panel efficiency varies with temperature |
weather_code | Open-Meteo | Cloud attenuation factor for physics model |
uv_index_max | Open-Meteo | Correlated with solar intensity |
precipitation_sum | Open-Meteo | Heavy precipitation reduces production |
sunrise / sunset | Open-Meteo | Defines productive solar hours |
The weather history is passed from the API to the ML service during training and prediction calls, ensuring the ML models have accurate meteorological context.