diff --git a/starpilot/controls/lib/weather_checker.py b/starpilot/controls/lib/weather_checker.py
index 5953efdd..bc448b83 100644
--- a/starpilot/controls/lib/weather_checker.py
+++ b/starpilot/controls/lib/weather_checker.py
@@ -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
diff --git a/starpilot/system/starpilot_tracking.py b/starpilot/system/starpilot_tracking.py
index 8ca630ae..0828c251 100644
--- a/starpilot/system/starpilot_tracking.py
+++ b/starpilot/system/starpilot_tracking.py
@@ -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"]):
diff --git a/starpilot/ui/qt/offroad/longitudinal_settings.cc b/starpilot/ui/qt/offroad/longitudinal_settings.cc
index 528a0765..41e94b1d 100644
--- a/starpilot/ui/qt/offroad/longitudinal_settings.cc
+++ b/starpilot/ui/qt/offroad/longitudinal_settings.cc
@@ -185,7 +185,6 @@ StarPilotLongitudinalPanel::StarPilotLongitudinalPanel(StarPilotSettingsWindow *
{"ReduceAccelerationSnow", tr("Reduce Acceleration by:"), tr("Lower the maximum acceleration in snow. Increase for softer takeoffs; decrease for quicker but less stable takeoffs."), ""},
{"ReduceLateralAccelerationSnow", tr("Reduce Speed in Curves by:"), tr("Lower the desired speed while driving through curves in snow. Increase for safer, gentler turns; decrease for more aggressive driving in curves."), ""},
- {"SetWeatherKey", tr("Set Your Own Key"), tr("Set your own \"OpenWeatherMap\" key to increase the weather update rate.
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."), ""},
{"SpeedLimitController", tr("Speed Limit Controller"), tr("Limit openpilot's maximum driving speed to the current speed limit 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("The speed used by \"Speed Limit Controller\" when no speed limit is found.
- Set Speed: Use the cruise set speed
- Experimental Mode: Estimate the limit using the driving model
- Previous Limit: 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();
}
diff --git a/starpilot/ui/qt/offroad/longitudinal_settings.h b/starpilot/ui/qt/offroad/longitudinal_settings.h
index ef5d71c8..18ab3ae9 100644
--- a/starpilot/ui/qt/offroad/longitudinal_settings.h
+++ b/starpilot/ui/qt/offroad/longitudinal_settings.h
@@ -50,7 +50,7 @@ private:
QSet parentKeys;
- StarPilotButtonsControl *weatherKeyControl;
+
StarPilotParamValueControl *longitudinalActuatorDelayToggle;
StarPilotParamValueControl *startAccelToggle;