openmeteo adapter
This commit is contained in:
@@ -1,15 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from openpilot.starpilot.common.starpilot_utilities import calculate_distance_to_point, get_starpilot_api_info, is_url_pingable
|
||||
from openpilot.starpilot.common.starpilot_variables import STARPILOT_API
|
||||
from openpilot.starpilot.common.starpilot_utilities import calculate_distance_to_point
|
||||
|
||||
CACHE_DISTANCE = 25
|
||||
CHECK_INTERVAL = 15 * 60
|
||||
MAX_RETRIES = 3
|
||||
RETRY_DELAY = 60
|
||||
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
|
||||
|
||||
# WMO Weather interpretation codes -> OpenWeatherMap condition IDs
|
||||
# Reference: https://open-meteo.com/en/docs#weather_variable_documentation
|
||||
WMO_TO_OWM = {
|
||||
0: 800, # Clear sky
|
||||
1: 801, # Mainly clear
|
||||
2: 802, # Partly cloudy
|
||||
3: 803, # Overcast
|
||||
45: 741, # Fog
|
||||
48: 741, # Depositing rime fog
|
||||
51: 300, # Drizzle: Light
|
||||
53: 301, # Drizzle: Moderate
|
||||
55: 302, # Drizzle: Dense
|
||||
56: 311, # Freezing Drizzle: Light
|
||||
57: 312, # Freezing Drizzle: Dense
|
||||
61: 500, # Rain: Slight
|
||||
63: 501, # Rain: Moderate
|
||||
65: 502, # Rain: Heavy
|
||||
66: 511, # Freezing Rain: Light
|
||||
67: 511, # Freezing Rain: Heavy
|
||||
71: 600, # Snow fall: Slight
|
||||
73: 601, # Snow fall: Moderate
|
||||
75: 602, # Snow fall: Heavy
|
||||
77: 600, # Snow grains
|
||||
80: 520, # Rain showers: Slight
|
||||
81: 521, # Rain showers: Moderate
|
||||
82: 522, # Rain showers: Violent
|
||||
85: 620, # Snow showers: Slight
|
||||
86: 621, # Snow showers: Heavy
|
||||
95: 200, # Thunderstorm: Slight/Moderate
|
||||
96: 201, # Thunderstorm with slight hail
|
||||
99: 202, # Thunderstorm with heavy hail
|
||||
}
|
||||
|
||||
# Reference: https://openweathermap.org/weather-conditions
|
||||
WEATHER_CATEGORIES = {
|
||||
@@ -35,14 +72,47 @@ WEATHER_CATEGORIES = {
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _iso_to_unix(iso_str):
|
||||
"""Convert an ISO8601 string (without timezone) to a UTC Unix timestamp."""
|
||||
try:
|
||||
return int(datetime.fromisoformat(iso_str).replace(tzinfo=timezone.utc).timestamp())
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def _normalize_response(data):
|
||||
"""Convert Open-Meteo's columnar response into the row-based format expected by StarPilot."""
|
||||
daily = data.get("daily", {})
|
||||
current = data.get("current", {})
|
||||
hourly = data.get("hourly", {})
|
||||
|
||||
current_wmo = current.get("weather_code", 0)
|
||||
|
||||
normalized = {
|
||||
"current": {
|
||||
"sunrise": _iso_to_unix(daily.get("sunrise", [None])[0] or ""),
|
||||
"sunset": _iso_to_unix(daily.get("sunset", [None])[0] or ""),
|
||||
"weather": [{"id": WMO_TO_OWM.get(current_wmo, 800)}],
|
||||
},
|
||||
"hourly": [
|
||||
{
|
||||
"dt": _iso_to_unix(t),
|
||||
"weather": [{"id": WMO_TO_OWM.get(wc, 800)}],
|
||||
}
|
||||
for t, wc in zip(hourly.get("time", []), hourly.get("weather_code", []))
|
||||
],
|
||||
}
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
class WeatherChecker:
|
||||
def __init__(self, StarPilotPlanner):
|
||||
self.starpilot_planner = StarPilotPlanner
|
||||
|
||||
self.is_daytime = False
|
||||
|
||||
self.api_25_calls = 0
|
||||
self.api_3_calls = 0
|
||||
self.increase_following_distance = 0
|
||||
self.increase_stopped_distance = 0
|
||||
self.reduce_acceleration = 0
|
||||
@@ -56,17 +126,7 @@ class WeatherChecker:
|
||||
self.last_updated = None
|
||||
self.requesting = False
|
||||
|
||||
self.user_api_key = self.starpilot_planner.params.get("WeatherToken", encoding="utf-8")
|
||||
|
||||
if self.user_api_key:
|
||||
self.check_interval = 60
|
||||
else:
|
||||
self.check_interval = 15 * 60
|
||||
|
||||
self.api_token, self.build_metadata, self.device_type, self.dongle_id = get_starpilot_api_info()
|
||||
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({"Accept-Language": "en", "User-Agent": "starpilot-api/1.0"})
|
||||
self.check_interval = CHECK_INTERVAL
|
||||
|
||||
self.executor = ThreadPoolExecutor(max_workers=1)
|
||||
|
||||
@@ -137,33 +197,24 @@ class WeatherChecker:
|
||||
self.update_offsets(starpilot_toggles)
|
||||
|
||||
def make_request():
|
||||
if not is_url_pingable(STARPILOT_API):
|
||||
return None
|
||||
|
||||
payload = {
|
||||
"api_key": self.user_api_key,
|
||||
"api_token": self.api_token,
|
||||
"build_metadata": self.build_metadata,
|
||||
"device": self.device_type,
|
||||
"starpilot_dongle_id": self.dongle_id,
|
||||
"lat": self.starpilot_planner.gps_position["latitude"],
|
||||
"lon": self.starpilot_planner.gps_position["longitude"],
|
||||
params = {
|
||||
"latitude": self.starpilot_planner.gps_position["latitude"],
|
||||
"longitude": self.starpilot_planner.gps_position["longitude"],
|
||||
"current": "weather_code",
|
||||
"hourly": "weather_code",
|
||||
"daily": "sunrise,sunset",
|
||||
"timezone": "UTC",
|
||||
}
|
||||
url = f"{OPEN_METEO_URL}?{urllib.parse.urlencode(params)}"
|
||||
|
||||
for attempt in range(1, MAX_RETRIES + 1):
|
||||
try:
|
||||
response = self.session.post(f"{STARPILOT_API}/weather", json=payload, headers={"Content-Type": "application/json"}, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
if data.get("api_version") == "2.5":
|
||||
self.api_25_calls += 1
|
||||
else:
|
||||
self.api_3_calls += 1
|
||||
return data
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "starpilot/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
return _normalize_response(json.loads(response.read().decode()))
|
||||
except Exception:
|
||||
if attempt < MAX_RETRIES:
|
||||
time.sleep(RETRY_DELAY)
|
||||
time.sleep(5)
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
@@ -130,14 +130,6 @@ class StarPilotTracking:
|
||||
if self.starpilot_events.stopped_for_light:
|
||||
self.starpilot_stats["StopLightTime"] = self.starpilot_stats.get("StopLightTime", 0) + DT_MDL
|
||||
|
||||
weather_api_calls = self.starpilot_stats.get("WeatherAPICalls", {})
|
||||
weather_api_calls["2.5"] = weather_api_calls.get("2.5", 0) + self.starpilot_weather.api_25_calls
|
||||
weather_api_calls["3.0"] = weather_api_calls.get("3.0", 0) + self.starpilot_weather.api_3_calls
|
||||
self.starpilot_stats["WeatherAPICalls"] = weather_api_calls
|
||||
|
||||
self.starpilot_weather.api_25_calls = 0
|
||||
self.starpilot_weather.api_3_calls = 0
|
||||
|
||||
suffix = "unknown"
|
||||
for category in WEATHER_CATEGORIES.values():
|
||||
if any(start <= self.starpilot_weather.weather_id <= end for start, end in category["ranges"]):
|
||||
|
||||
@@ -185,7 +185,6 @@ StarPilotLongitudinalPanel::StarPilotLongitudinalPanel(StarPilotSettingsWindow *
|
||||
{"ReduceAccelerationSnow", tr("Reduce Acceleration by:"), tr("<b>Lower the maximum acceleration in snow.</b> Increase for softer takeoffs; decrease for quicker but less stable takeoffs."), ""},
|
||||
{"ReduceLateralAccelerationSnow", tr("Reduce Speed in Curves by:"), tr("<b>Lower the desired speed while driving through curves in snow.</b> Increase for safer, gentler turns; decrease for more aggressive driving in curves."), ""},
|
||||
|
||||
{"SetWeatherKey", tr("Set Your Own Key"), tr("<b>Set your own \"OpenWeatherMap\" key to increase the weather update rate.</b><br><br><i>Personal keys grant 1,000 free calls per day, allowing for updates every minute. The default key is shared and only updates every 15 minutes.</i>"), ""},
|
||||
|
||||
{"SpeedLimitController", tr("Speed Limit Controller"), tr("<b>Limit openpilot's maximum driving speed to the current speed limit</b> obtained from downloaded maps, Mapbox, or the dashboard for supported vehicles (Ford, Genesis, Hyundai, Kia, Lexus, Toyota)."), "../../starpilot/assets/toggle_icons/icon_speed_limit.png"},
|
||||
{"SLCFallback", tr("Fallback Speed"), tr("<b>The speed used by \"Speed Limit Controller\" when no speed limit is found.</b><br><br>- <b>Set Speed</b>: Use the cruise set speed<br>- <b>Experimental Mode</b>: Estimate the limit using the driving model<br>- <b>Previous Limit</b>: Keep using the last confirmed limit"), ""},
|
||||
@@ -406,70 +405,6 @@ StarPilotLongitudinalPanel::StarPilotLongitudinalPanel(StarPilotSettingsWindow *
|
||||
qolOpen = true;
|
||||
});
|
||||
longitudinalToggle = weatherToggle;
|
||||
} else if (param == "SetWeatherKey") {
|
||||
weatherKeyControl = new StarPilotButtonsControl(title, desc, icon, {tr("ADD"), tr("TEST")});
|
||||
QObject::connect(weatherKeyControl, &StarPilotButtonsControl::buttonClicked, [this](int id) {
|
||||
if (id == 0) {
|
||||
if (!params.get("WeatherToken").empty()) {
|
||||
if (StarPilotConfirmationDialog::yesorno(tr("Are you sure you want to remove your key?"), this)) {
|
||||
params.remove("WeatherToken");
|
||||
|
||||
weatherKeyControl->setText(0, tr("ADD"));
|
||||
weatherKeyControl->setVisibleButton(1, false);
|
||||
}
|
||||
} else {
|
||||
int keyLength = 32;
|
||||
QString currentKey = QString::fromStdString(params.get("WeatherToken"));
|
||||
QString newKey = InputDialog::getText(tr("Enter your \"OpenWeatherMap\" key"), this, tr("Characters: 0/%1").arg(keyLength), false, -1, currentKey, keyLength).trimmed();
|
||||
if (!newKey.isEmpty()) {
|
||||
params.put("WeatherToken", newKey.toStdString());
|
||||
|
||||
weatherKeyControl->setText(0, tr("REMOVE"));
|
||||
weatherKeyControl->setVisibleButton(1, true);
|
||||
}
|
||||
}
|
||||
} else if (id == 1) {
|
||||
weatherKeyControl->setValue(tr("Testing..."));
|
||||
|
||||
QString key = QString::fromStdString(params.get("WeatherToken")).trimmed();
|
||||
QString url30 = QString("https://api.openweathermap.org/data/3.0/onecall?lat=42.4293&lon=-83.9850&exclude=current,minutely,hourly,daily,alerts&appid=%1").arg(key);
|
||||
|
||||
QNetworkRequest request(url30);
|
||||
QNetworkReply *reply = networkManager->get(request);
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [=]() {
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
weatherKeyControl->setValue("");
|
||||
ConfirmationDialog::alert(tr("Key is valid!"), this);
|
||||
return;
|
||||
}
|
||||
|
||||
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (status == 401 || status == 403) {
|
||||
QString url25 = QString("https://api.openweathermap.org/data/2.5/weather?lat=42.4293&lon=-83.9850&appid=%1").arg(key);
|
||||
|
||||
QNetworkRequest request25(url25);
|
||||
QNetworkReply *reply25 = networkManager->get(request25);
|
||||
QObject::connect(reply25, &QNetworkReply::finished, this, [=]() {
|
||||
reply25->deleteLater();
|
||||
|
||||
weatherKeyControl->setValue("");
|
||||
if (reply25->error() == QNetworkReply::NoError) {
|
||||
ConfirmationDialog::alert(tr("Your key is valid for version 2.5, but version 3.0 is highly recommended! Please subscribe to the \"One Call API 3.0\" plan!"), this);
|
||||
} else {
|
||||
int status25 = reply25->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
ConfirmationDialog::alert(tr("Invalid key! (Error: %1)").arg(status25), this);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
weatherKeyControl->setValue("");
|
||||
ConfirmationDialog::alert(tr("An error occurred: %1").arg(reply->errorString()), this);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
longitudinalToggle = weatherKeyControl;
|
||||
} else if (param == "LowVisibilityOffsets") {
|
||||
ButtonControl *manageLowVisibilitOffsetsButton = new ButtonControl(title, tr("MANAGE"), desc);
|
||||
QObject::connect(manageLowVisibilitOffsetsButton, &ButtonControl::clicked, [longitudinalLayout, weatherLowVisibilityPanel, this]() {
|
||||
@@ -872,9 +807,7 @@ void StarPilotLongitudinalPanel::showEvent(QShowEvent *event) {
|
||||
vEgoStartingToggle->setTitle(QString(tr("Start Speed (Default: %1)")).arg(QString::number(parent->vEgoStarting, 'f', 2)));
|
||||
vEgoStoppingToggle->setTitle(QString(tr("Stop Speed (Default: %1)")).arg(QString::number(parent->vEgoStopping, 'f', 2)));
|
||||
|
||||
bool keyExists = !params.get("WeatherToken").empty();
|
||||
weatherKeyControl->setText(0, keyExists ? tr("REMOVE") : tr("ADD"));
|
||||
weatherKeyControl->setVisibleButton(1, keyExists && fs.starpilot_scene.online);
|
||||
|
||||
|
||||
updateToggles();
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ private:
|
||||
|
||||
QSet<QString> parentKeys;
|
||||
|
||||
StarPilotButtonsControl *weatherKeyControl;
|
||||
|
||||
|
||||
StarPilotParamValueControl *longitudinalActuatorDelayToggle;
|
||||
StarPilotParamValueControl *startAccelToggle;
|
||||
|
||||
Reference in New Issue
Block a user