Skip to content

Sky science

Weather and clouds

What weather data the app fetches, what cloud cover means, how seeing is estimated today, and which forecast variables are not used yet.

Where weather comes from

Weather comes from Open-Meteo, which combines forecasts from several national weather services (ECMWF, NOAA, Météo-France, DWD, the UK Met Office, JMA and others) behind one keyless interface. The app calls it directly. A Cloudflare proxy exists behind a feature flag that is off.

Astr does not model weather. It reads a forecast, hour by hour, for the location's coordinates.

What is fetched today

One request returns an hourly series for 10 past days and 16 forecast days (the maximum Open-Meteo offers) in these variables:

QuantityOpen-Meteo variableUnitUsed for
Air temperature at 2 mtemperature_2m°Ctemperature, and the seeing estimate
Relative humidity at 2 mrelativehumidity_2m%humidity, and the seeing estimate
Total cloud covercloudcover% of skycloud cover
Wind speed at 10 mwindspeed_10mkm/hwind, and the seeing estimate
Weather codeweathercodeWMO codeweather icon

What cloud cover is

Cloud cover is not calculated by Astr. It is a variable from a numerical weather model: the fraction of the sky above a grid cell (about 11 km across) that the model puts cloud in, from 0% to 100%.

Cloud Cover12%
Cloud Cover45%
Cloud Cover86%

It means a share of the sky, not a probability that a given star is hidden. A forecast of 30% means about a third of the sky is covered, and the model does not say which third. It also cannot see clouds smaller than a grid cell or the shape of one hill's weather.

How it is used today

  • For today, the home screen takes the forecast hour closest to the current time. That is the cloud now, which can be daytime cloud, not the cloud tonight.
  • For another date, it takes the day's average cloud cover from the seven-day forecast.
  • The conditions graph plots the hourly values across the night as an area, and the prime-view window weights cloud at 70% (see Graphs).

The sky-state model replaces this with the hourly values for the night's dark hours (see Sky states).

Seeing

Seeing is how much the air blurs a star's image, which matters for planets and double stars. The true Pickering scale is an observer's judgement of a star's diffraction pattern through a telescope. The app does not have that, so it uses a proxy built from ground weather and labels it with the Pickering name.

score=10−pT−pw+bh,clamped to 1…10\text{score} = 10 - p_T - p_w + b_h, \qquad \text{clamped to } 1\ldots 10

SymbolMeaningRule
pTp_Tpenalty for the temperature range ΔT\Delta T over the next 3 hours2 if ΔT>5 ∘C\Delta T > 5\,^\circ\text{C}, 1 if ΔT>3 ∘C\Delta T > 3\,^\circ\text{C}
pwp_wpenalty for the wind speed ww now3 if w>30w > 30 km/h, 2 if w>20w > 20, 1 if w>10w > 10
bhb_hbonus for stable humid air1 if humidity is over 70% and ΔT<3 ∘C\Delta T < 3\,^\circ\text{C}
scorethe result, 1 to 10, and its label9 to 10 Excellent, 7 to 8 Good, 5 to 6 Fair, 3 to 4 Poor, 1 to 2 Extremely Poor

This is a heuristic, not a measurement, and its thresholds are not validated against observations (unverified). The physics suggests a better proxy: turbulence in the free atmosphere follows the wind shear near the jet stream (200 to 300 hPa), and the lowest kilometre follows heat flux from the ground (SPIE, "Using meteorological forecasts to predict astronomical seeing"). Open-Meteo provides wind at 250 and 300 hPa and the boundary-layer height. A replacement built on those is a proposal, to be compared against an independent seeing source before it ships.

Transparency

Transparency is how clear the air is, set mostly by aerosols and water vapour. The app has no transparency measure today; it shows humidity. The sky model uses an extinction coefficient from aerosol optical depth (see Moonlight and sky brightness), which needs a variable the app does not fetch yet.

Available but not used

QuantityOpen-Meteo variableWhat it givesWould improve
Cloud cover by heightcloud_cover_low, _mid, _highlow under 3 km, mid 3 to 8 km, high above 8 kmtelling thin high cirrus from thick low cloud
Visibilityvisibilityhorizontal viewing distance in metres, affected by low cloud, humidity and aerosolstransparency
Wind near the jet streamwind_speed_250hpa, wind_speed_300hpawind speed at those pressure levelsseeing
Boundary layer depthboundary_layer_heightdepth of the turbulent layer next to the groundseeing
Precipitable watertotal_column_integrated_water_vapourwater vapour in the whole column of airtransparency
Aerosol optical depth, dustaerosol_optical_depth, dust (Air Quality API)haze and Saharan dust, 0.4° grid, every 3 hours globallyextinction

Terms of use

The free tier is for non-commercial use, with fewer than 10,000 calls a day, 5,000 an hour and 600 a minute. Apps with subscriptions or advertisements count as commercial. Data is under CC BY 4.0, so Open-Meteo must be credited, and the Air Quality data must also credit CAMS. Astr is planned to be free, open source and tip-funded. Whether tips count as non-commercial is not stated in the terms, so it should be confirmed with Open-Meteo before the first store release (unverified).

Implementation

Excerpts of the real files, cut out by name, copied into the site and checked against the repository on every build.

open_meteo_weather_service.dart · getHourlyForecast
/// Fetches hourly weather forecast for the next 16 days (Open-Meteo max)/// Returns map with hourly arrays for: temperature_2m, relativehumidity_2m, cloudcover, windspeed_10m/// AC#3: Extended from 7 to 16 days to support +/- 10 day cloud cover windowFuture<Map<String, dynamic>> getHourlyForecast(GeoLocation location) async {  final Response response = await _dio.get(    '${ApiConfig.weatherBaseUrl}/forecast',    queryParameters: <String, dynamic>{      'latitude': location.latitude,      'longitude': location.longitude,      'hourly': 'temperature_2m,relativehumidity_2m,cloudcover,windspeed_10m,weathercode',      'forecast_days': 16, // AC#3: Extended from 7 to 16 (Open-Meteo max)      'past_days': 10, // AC#3: Include 10 days of historical data    },  );  if (response.statusCode == 200) {    final data = response.data;    final hourly = data['hourly'];        if (hourly != null) {      return <String, dynamic>{        'time': (hourly['time'] as List).cast<String>(),        'temperature': (hourly['temperature_2m'] as List).map((e) => (e as num?)?.toDouble() ?? 0.0).toList(),        'humidity': (hourly['relativehumidity_2m'] as List).map((e) => (e as num?)?.toDouble() ?? 0.0).toList(),        'cloudCover': (hourly['cloudcover'] as List).map((e) => (e as num?)?.toDouble() ?? 0.0).toList(),        'windSpeed': (hourly['windspeed_10m'] as List).map((e) => (e as num?)?.toDouble() ?? 0.0).toList(),        'weatherCode': (hourly['weathercode'] as List).map((e) => (e as num?)?.toDouble() ?? 0.0).toList(),      };    }  }    throw Exception('Failed to fetch hourly forecast data');}
weather_provider.dart · WeatherNotifier
return WeatherNotifier();