260 lines
8.7 KiB
QML
260 lines
8.7 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import qs.Constants
|
|
import qs.Utils
|
|
pragma Singleton
|
|
|
|
// Location and weather service with decoupled geocoding and weather fetching.
|
|
Singleton {
|
|
//console.log(JSON.stringify(weatherData))
|
|
|
|
id: root
|
|
|
|
property string locationFile: Paths.cacheDir + "location.json"
|
|
property int weatherUpdateFrequency: 30 * 60 // 30 minutes expressed in seconds
|
|
property bool isFetchingWeather: false
|
|
readonly property alias data: adapter
|
|
property string locationName: SettingsService.location
|
|
property string latitude: SettingsService.latitude
|
|
property string longitude: SettingsService.longitude
|
|
property string stableLatitude: ""
|
|
property string stableLongitude: ""
|
|
property string stableName: ""
|
|
// Formatted coordinates for UI display
|
|
readonly property string displayCoordinates: {
|
|
if (root.stableLatitude === "" || root.stableLongitude === "")
|
|
return "";
|
|
|
|
const lat = parseFloat(root.stableLatitude).toFixed(4);
|
|
const lon = parseFloat(root.stableLongitude).toFixed(4);
|
|
return `${lat}, ${lon}`;
|
|
}
|
|
// Weather condition detection
|
|
readonly property int currentWeatherCode: data.weather ? data.weather.current_weather.weathercode : -1
|
|
readonly property bool isDayTime: data.weather ? data.weather.current_weather.is_day : true
|
|
readonly property bool isRaining: currentWeatherCode >= 0 && ((currentWeatherCode >= 51 && currentWeatherCode <= 67) || (currentWeatherCode >= 80 && currentWeatherCode <= 82) || (currentWeatherCode >= 95 && currentWeatherCode <= 99))
|
|
readonly property bool isSnowing: currentWeatherCode >= 0 && ((currentWeatherCode >= 71 && currentWeatherCode <= 77) || (currentWeatherCode >= 85 && currentWeatherCode <= 86))
|
|
readonly property bool isCloudy: currentWeatherCode >= 0 && (currentWeatherCode === 2 || currentWeatherCode === 3)
|
|
readonly property bool isFoggy: currentWeatherCode >= 0 && (currentWeatherCode >= 40 && currentWeatherCode <= 49)
|
|
readonly property bool isClearDay: currentWeatherCode >= 0 && (currentWeatherCode === 0 || currentWeatherCode === 1) && isDayTime
|
|
readonly property bool isClearNight: currentWeatherCode >= 0 && (currentWeatherCode === 0 || currentWeatherCode === 1) && !isDayTime
|
|
|
|
function resetWeather() {
|
|
Logger.i("Location", "Resetting location and weather data");
|
|
adapter.weatherLastFetch = 0;
|
|
adapter.weather = null;
|
|
update();
|
|
}
|
|
|
|
// Main update function - geocodes location if needed, then fetches weather if enabled
|
|
function update() {
|
|
updateWeatherData();
|
|
}
|
|
|
|
// Fetch weather data if enabled and coordinates are available
|
|
function updateWeatherData() {
|
|
if (isFetchingWeather) {
|
|
Logger.w("Location", "Weather is still fetching");
|
|
return ;
|
|
}
|
|
if (adapter.latitude === "" || adapter.longitude === "") {
|
|
Logger.w("Location", "Cannot fetch weather without coordinates");
|
|
return ;
|
|
}
|
|
const needsWeatherUpdate = (adapter.weather === null) || (Time.timestamp >= adapter.weatherLastFetch + weatherUpdateFrequency);
|
|
if (needsWeatherUpdate) {
|
|
isFetchingWeather = true;
|
|
fetchWeatherData(root.latitude, root.longitude, errorCallback);
|
|
}
|
|
}
|
|
|
|
// Fetch weather data from Open-Meteo API
|
|
function fetchWeatherData(latitude, longitude, errorCallback) {
|
|
Logger.d("Location", "Fetching weather from api.open-meteo.com");
|
|
var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure,is_day&daily=temperature_2m_max,temperature_2m_min,weathercode,sunset,sunrise&timezone=auto";
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.readyState === XMLHttpRequest.DONE) {
|
|
if (xhr.status === 200) {
|
|
try {
|
|
var weatherData = JSON.parse(xhr.responseText);
|
|
// Save core data
|
|
data.weather = weatherData;
|
|
data.weatherLastFetch = Time.timestamp;
|
|
// Update stable display values only when complete and successful
|
|
SettingsService.latitude = data.latitude = weatherData.latitude.toString();
|
|
SettingsService.longitude = data.longitude = weatherData.longitude.toString();
|
|
root.coordinatesReady = true;
|
|
isFetchingWeather = false;
|
|
Logger.d("Location", "Cached weather to disk - stable coordinates updated");
|
|
} catch (e) {
|
|
errorCallback("Location", "Failed to parse weather data");
|
|
}
|
|
} else {
|
|
errorCallback("Location", "Weather fetch error: " + xhr.status);
|
|
}
|
|
}
|
|
};
|
|
xhr.open("GET", url);
|
|
xhr.send();
|
|
}
|
|
|
|
// --------------------------------
|
|
function errorCallback(module, message) {
|
|
Logger.e(module, message);
|
|
isFetchingWeather = false;
|
|
}
|
|
|
|
// --------------------------------
|
|
function weatherSymbolFromCode(code, isDay) {
|
|
if (code === 0)
|
|
return isDay ? "weather-sun" : "weather-moon";
|
|
|
|
if (code === 1 || code === 2)
|
|
return isDay ? "weather-cloud-sun" : "weather-moon-stars";
|
|
|
|
if (code === 3)
|
|
return "weather-cloud";
|
|
|
|
if (code >= 45 && code <= 48)
|
|
return "weather-cloud-haze";
|
|
|
|
if (code >= 51 && code <= 67)
|
|
return "weather-cloud-rain";
|
|
|
|
if (code >= 80 && code <= 82)
|
|
return "weather-cloud-rain";
|
|
|
|
if (code >= 71 && code <= 77)
|
|
return "weather-cloud-snow";
|
|
|
|
if (code >= 71 && code <= 77)
|
|
return "weather-cloud-snow";
|
|
|
|
if (code >= 85 && code <= 86)
|
|
return "weather-cloud-snow";
|
|
|
|
if (code >= 95 && code <= 99)
|
|
return "weather-cloud-lightning";
|
|
|
|
return "weather-cloud";
|
|
}
|
|
|
|
// --------------------------------
|
|
function weatherDescriptionFromCode(code) {
|
|
if (code === 0)
|
|
return "Clear sky";
|
|
|
|
if (code === 1)
|
|
return "Mainly clear";
|
|
|
|
if (code === 2)
|
|
return "Partly cloudy";
|
|
|
|
if (code === 3)
|
|
return "Overcast";
|
|
|
|
if (code === 45 || code === 48)
|
|
return "Fog";
|
|
|
|
if (code >= 51 && code <= 67)
|
|
return "Drizzle";
|
|
|
|
if (code >= 71 && code <= 77)
|
|
return "Snow";
|
|
|
|
if (code >= 80 && code <= 82)
|
|
return "Rain showers";
|
|
|
|
if (code >= 95 && code <= 99)
|
|
return "Thunderstorm";
|
|
|
|
return "Unknown";
|
|
}
|
|
|
|
function weatherColorFromCode(code) {
|
|
// Clear sky
|
|
if (code === 0)
|
|
return Colors.mYellow;
|
|
|
|
// Mainly clear / Partly cloudy
|
|
if (code === 1 || code === 2)
|
|
return Colors.mSky;
|
|
|
|
// Overcast
|
|
if (code === 3)
|
|
return Colors.mLavender;
|
|
|
|
// Fog
|
|
if (code === 45 || code === 48)
|
|
return Colors.mCyan;
|
|
|
|
// Drizzle / Rain / Rain showers
|
|
if ((code >= 51 && code <= 55) || (code >= 61 && code <= 65) || (code >= 80 && code <= 82))
|
|
return Colors.mBlue;
|
|
|
|
// Freezing drizzle / Freezing rain
|
|
if ((code >= 56 && code <= 57) || (code >= 66 && code <= 67))
|
|
return Colors.mPurple;
|
|
|
|
// Snow / Snow showers
|
|
if ((code >= 71 && code <= 77) || (code >= 85 && code <= 86))
|
|
return Colors.mLavender;
|
|
|
|
// Thunderstorm
|
|
if (code === 95)
|
|
return Colors.mOrange;
|
|
|
|
// Thunderstorm with hail
|
|
if (code >= 96 && code <= 99)
|
|
return Colors.mRed;
|
|
|
|
return Colors.mSky;
|
|
}
|
|
|
|
FileView {
|
|
id: locationFileView
|
|
|
|
path: locationFile
|
|
printErrors: false
|
|
onAdapterUpdated: saveTimer.start()
|
|
onLoaded: {
|
|
Logger.d("Location", "Loaded cached data");
|
|
update();
|
|
}
|
|
onLoadFailed: function(error) {
|
|
update();
|
|
}
|
|
|
|
JsonAdapter {
|
|
id: adapter
|
|
|
|
property int weatherLastFetch: 0
|
|
property var weather: null
|
|
}
|
|
|
|
}
|
|
|
|
// Update timer runs when weather is enabled or location-based scheduling is active
|
|
Timer {
|
|
id: updateTimer
|
|
|
|
interval: 20 * 1000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: {
|
|
update();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: saveTimer
|
|
|
|
running: false
|
|
interval: 1000
|
|
onTriggered: locationFileView.writeAdapter()
|
|
}
|
|
|
|
}
|