Skip to main content

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

ProviderOpen-Meteo
AuthenticationNone required (free tier)
Forecast Window8 days forward
ResolutionDaily
Rate Limits10,000 requests/day (free tier)

Weather Data Fields

FieldUnitDescription
WeatherCodeWMO codeStandardized weather condition code
TemperatureMax°CDaily maximum temperature
TemperatureMin°CDaily minimum temperature
ShortwaveRadiationSumMJ/m²Total solar radiation for the day
UvIndexMaxIndexMaximum UV index
PrecipitationSummmTotal precipitation
SunriseISO 8601Sunrise time (local timezone)
SunsetISO 8601Sunset 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 DailyPredictionJob in the Quartz schedule, ensuring fresh weather data is available when predictions are generated.

Schedule

TriggerCron ExpressionFrequency
UpdateWeatherDataTrigger0 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:

CodeDescriptionCloud Attenuation (ML)
0Clear sky0%
1Mainly clear10%
2Partly cloudy25%
3Overcast50%
45, 48Fog60%
51-55Drizzle55%
61-65Rain65%
71-75Snowfall75%
80-82Rain showers60%
95-99Thunderstorm80%

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:

FeatureSourceImpact
shortwave_radiation_sumOpen-MeteoPrimary indicator of solar potential
temperature_max / temperature_minOpen-MeteoPanel efficiency varies with temperature
weather_codeOpen-MeteoCloud attenuation factor for physics model
uv_index_maxOpen-MeteoCorrelated with solar intensity
precipitation_sumOpen-MeteoHeavy precipitation reduces production
sunrise / sunsetOpen-MeteoDefines 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.