Set Open-Meteo as MVP weather source
This commit is contained in:
@@ -426,7 +426,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
|||||||
- [x] Add weather transition rules.
|
- [x] Add weather transition rules.
|
||||||
- [x] Add weather replication.
|
- [x] Add weather replication.
|
||||||
- [x] Add real-world weather provider adapter for Ground Zero by latitude/longitude. Added a tile-driven Open-Meteo adapter subsystem that can request weather for any active tile by center latitude/longitude, map provider weather codes into Agrarian weather states, apply current temperature and daily low/high to `AAgrarianGameState`, and generate a source-backed tile weather manifest so future real tiles become weather-eligible automatically.
|
- [x] Add real-world weather provider adapter for Ground Zero by latitude/longitude. Added a tile-driven Open-Meteo adapter subsystem that can request weather for any active tile by center latitude/longitude, map provider weather codes into Agrarian weather states, apply current temperature and daily low/high to `AAgrarianGameState`, and generate a source-backed tile weather manifest so future real tiles become weather-eligible automatically.
|
||||||
- [ ] Use Open-Meteo as the first global MVP weather source.
|
- [x] Use Open-Meteo as the first global MVP weather source. Added `Data/Weather/open_meteo_mvp_source.json` as the provider contract, documented the global tile lookup rule, and added `Scripts/verify_open_meteo_mvp_source.py` to validate the static contract plus live Open-Meteo responses for every source-backed tile in the generated weather manifest.
|
||||||
- [ ] Add NOAA/NWS fallback or enrichment for US tiles where useful.
|
- [ ] Add NOAA/NWS fallback or enrichment for US tiles where useful.
|
||||||
- [ ] Cache real-weather snapshots server-side so clients never call public weather APIs directly.
|
- [ ] Cache real-weather snapshots server-side so clients never call public weather APIs directly.
|
||||||
- [ ] Map real weather inputs into Agrarian weather states: temperature, precipitation, wind, cloud cover, humidity, pressure, visibility, and weather code.
|
- [ ] Map real weather inputs into Agrarian weather states: temperature, precipitation, wind, cloud cover, humidity, pressure, visibility, and weather code.
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"provider_id": "open-meteo",
|
||||||
|
"provider_name": "Open-Meteo Forecast API",
|
||||||
|
"provider_docs_url": "https://open-meteo.com/en/docs",
|
||||||
|
"forecast_endpoint": "https://api.open-meteo.com/v1/forecast",
|
||||||
|
"api_key_required": false,
|
||||||
|
"tile_lookup_rule": "Use each source-backed tile center latitude/longitude from Data/Tiles/tile_weather_manifest.json.",
|
||||||
|
"request_parameters": {
|
||||||
|
"current": [
|
||||||
|
"temperature_2m",
|
||||||
|
"relative_humidity_2m",
|
||||||
|
"precipitation",
|
||||||
|
"rain",
|
||||||
|
"showers",
|
||||||
|
"snowfall",
|
||||||
|
"weather_code",
|
||||||
|
"cloud_cover",
|
||||||
|
"pressure_msl",
|
||||||
|
"wind_speed_10m"
|
||||||
|
],
|
||||||
|
"daily": [
|
||||||
|
"temperature_2m_max",
|
||||||
|
"temperature_2m_min",
|
||||||
|
"weather_code",
|
||||||
|
"precipitation_sum",
|
||||||
|
"wind_speed_10m_max"
|
||||||
|
],
|
||||||
|
"forecast_days": 1,
|
||||||
|
"timezone": "auto"
|
||||||
|
},
|
||||||
|
"agrarian_mapping": {
|
||||||
|
"temperature": "current.temperature_2m feeds RegionalObservedTemperatureC; daily min/max feed RegionalDailyLowTemperatureC and RegionalDailyHighTemperatureC.",
|
||||||
|
"weather_state": "weather_code, precipitation, and wind speed map into Clear, Rain, ColdWind, or Storm.",
|
||||||
|
"source_tracking": "provider id and provider current timestamp are stored in the game-state weather source string until full persistence metadata is implemented."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -157,6 +157,14 @@ source-backed, generated, validated, packaged, or published tile with center
|
|||||||
coordinates, while placeholder/unknown tiles are skipped. Future source-backed
|
coordinates, while placeholder/unknown tiles are skipped. Future source-backed
|
||||||
tiles therefore become weather-eligible when their registry entries are added.
|
tiles therefore become weather-eligible when their registry entries are added.
|
||||||
|
|
||||||
|
Open-Meteo is the first global MVP weather source. The provider contract is
|
||||||
|
stored in `Data/Weather/open_meteo_mvp_source.json`, including the forecast
|
||||||
|
endpoint, requested current/daily variables, tile lookup rule, and Agrarian
|
||||||
|
mapping notes. `Scripts/verify_open_meteo_mvp_source.py` validates the static
|
||||||
|
contract and can perform a live Open-Meteo request for every source-backed tile
|
||||||
|
in `Data/Tiles/tile_weather_manifest.json`. This keeps the provider global for
|
||||||
|
all future real tiles instead of adding one-off Ground Zero weather code.
|
||||||
|
|
||||||
## Terrain And Tile Delivery
|
## Terrain And Tile Delivery
|
||||||
|
|
||||||
### MVP Tile
|
### MVP Tile
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify Open-Meteo is the global MVP weather source for source-backed tiles."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
PROVIDER_CONTRACT = ROOT / "Data" / "Weather" / "open_meteo_mvp_source.json"
|
||||||
|
TILE_MANIFEST = ROOT / "Data" / "Tiles" / "tile_weather_manifest.json"
|
||||||
|
WEATHER_SUBSYSTEM_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherProviderSubsystem.cpp"
|
||||||
|
WEATHER_SUBSYSTEM_H = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherProviderSubsystem.h"
|
||||||
|
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
|
||||||
|
TDD = ROOT / "Docs" / "TechnicalDesignDocument.md"
|
||||||
|
|
||||||
|
|
||||||
|
EXPECTED_SNIPPETS = {
|
||||||
|
WEATHER_SUBSYSTEM_H: [
|
||||||
|
"OpenMeteoForecastEndpoint",
|
||||||
|
"https://api.open-meteo.com/v1/forecast",
|
||||||
|
"RequestWeatherForTile",
|
||||||
|
],
|
||||||
|
WEATHER_SUBSYSTEM_CPP: [
|
||||||
|
"current=temperature_2m,relative_humidity_2m,precipitation",
|
||||||
|
"daily=temperature_2m_max,temperature_2m_min,weather_code,precipitation_sum,wind_speed_10m_max",
|
||||||
|
"OutSnapshot.Provider = TEXT(\"open-meteo\");",
|
||||||
|
],
|
||||||
|
ROADMAP: [
|
||||||
|
"[x] Use Open-Meteo as the first global MVP weather source.",
|
||||||
|
],
|
||||||
|
TDD: [
|
||||||
|
"Open-Meteo is the first global MVP weather source",
|
||||||
|
"Data/Weather/open_meteo_mvp_source.json",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def assert_static_contract() -> tuple[dict, dict]:
|
||||||
|
missing = []
|
||||||
|
for path, snippets in EXPECTED_SNIPPETS.items():
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
for snippet in snippets:
|
||||||
|
if snippet not in text:
|
||||||
|
missing.append(f"{path.relative_to(ROOT)}: {snippet}")
|
||||||
|
if missing:
|
||||||
|
raise RuntimeError("Open-Meteo MVP source verification failed: " + "; ".join(missing))
|
||||||
|
|
||||||
|
provider = json.loads(PROVIDER_CONTRACT.read_text(encoding="utf-8"))
|
||||||
|
manifest = json.loads(TILE_MANIFEST.read_text(encoding="utf-8"))
|
||||||
|
if provider.get("provider_id") != "open-meteo":
|
||||||
|
raise RuntimeError("Provider contract does not identify Open-Meteo")
|
||||||
|
if provider.get("api_key_required") is not False:
|
||||||
|
raise RuntimeError("Open-Meteo MVP source should not require an API key")
|
||||||
|
if not manifest.get("tiles"):
|
||||||
|
raise RuntimeError("Tile weather manifest has no source-backed weather tiles")
|
||||||
|
for tile in manifest["tiles"]:
|
||||||
|
if tile.get("provider") != "open-meteo":
|
||||||
|
raise RuntimeError(f"Unexpected tile weather provider for {tile.get('tile_id')}: {tile.get('provider')}")
|
||||||
|
return provider, manifest
|
||||||
|
|
||||||
|
|
||||||
|
def build_open_meteo_url(provider: dict, tile: dict) -> str:
|
||||||
|
params = provider["request_parameters"]
|
||||||
|
query = {
|
||||||
|
"latitude": f"{float(tile['center_latitude']):.6f}",
|
||||||
|
"longitude": f"{float(tile['center_longitude']):.6f}",
|
||||||
|
"current": ",".join(params["current"]),
|
||||||
|
"daily": ",".join(params["daily"]),
|
||||||
|
"forecast_days": str(params["forecast_days"]),
|
||||||
|
"timezone": params["timezone"],
|
||||||
|
}
|
||||||
|
return provider["forecast_endpoint"] + "?" + urllib.parse.urlencode(query, safe=",")
|
||||||
|
|
||||||
|
|
||||||
|
def assert_live_open_meteo_response(provider: dict, manifest: dict) -> None:
|
||||||
|
if os.environ.get("AGRARIAN_SKIP_LIVE_WEATHER_CHECK") == "1":
|
||||||
|
print("Skipping live Open-Meteo check because AGRARIAN_SKIP_LIVE_WEATHER_CHECK=1")
|
||||||
|
return
|
||||||
|
|
||||||
|
for tile in manifest["tiles"]:
|
||||||
|
url = build_open_meteo_url(provider, tile)
|
||||||
|
with urllib.request.urlopen(url, timeout=20) as response:
|
||||||
|
payload = json.loads(response.read().decode("utf-8"))
|
||||||
|
|
||||||
|
current = payload.get("current", {})
|
||||||
|
daily = payload.get("daily", {})
|
||||||
|
required_current = {"temperature_2m", "relative_humidity_2m", "precipitation", "weather_code", "wind_speed_10m"}
|
||||||
|
required_daily = {"temperature_2m_max", "temperature_2m_min", "weather_code", "precipitation_sum"}
|
||||||
|
missing_current = sorted(required_current - set(current))
|
||||||
|
missing_daily = sorted(required_daily - set(daily))
|
||||||
|
if missing_current or missing_daily:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Open-Meteo response for {tile['tile_id']} missing current={missing_current} daily={missing_daily}"
|
||||||
|
)
|
||||||
|
if not isinstance(daily["temperature_2m_max"], list) or not daily["temperature_2m_max"]:
|
||||||
|
raise RuntimeError(f"Open-Meteo daily max temperature is empty for {tile['tile_id']}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
provider, manifest = assert_static_contract()
|
||||||
|
assert_live_open_meteo_response(provider, manifest)
|
||||||
|
print("Open-Meteo MVP weather source verification complete.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user