/** * weather.js - A module for weather-related functionality * Provides real-time weather data and sunset/sunrise information * Uses OpenWeatherMap API for weather data */ class WeatherManager { constructor(options = {}) { // Default options // Get API key from options, window (injected by server from .env), or fallback const apiKey = options.apiKey || window.OPENWEATHERMAP_API_KEY || ''; this.options = { latitude: options.latitude || (window.DEFAULT_LOCATION?.latitude) || 59.3293, // Stockholm latitude longitude: options.longitude || (window.DEFAULT_LOCATION?.longitude) || 18.0686, // Stockholm longitude apiKey: apiKey, // OpenWeatherMap API key (from .env via server injection, or fallback) refreshInterval: options.refreshInterval || 30 * 60 * 1000, // 30 minutes in milliseconds ...options }; // State this.weatherData = null; this.forecastData = null; this.sunTimes = null; this.isDarkMode = false; this.lastUpdated = null; this.daylightBarUpdateInterval = null; // Initialize this.init(); } /** * Initialize the weather manager */ async init() { try { // Check for API key if (!this.options.apiKey) { console.warn('WeatherManager: No OpenWeatherMap API key configured. Set OPENWEATHERMAP_API_KEY in your .env file.'); const weatherContainer = document.getElementById('custom-weather'); if (weatherContainer) { const warningEl = document.createElement('div'); warningEl.style.cssText = 'padding: 10px; color: #c41e3a; font-size: 0.9em; text-align: center;'; warningEl.textContent = 'Weather unavailable: No API key configured. Set OPENWEATHERMAP_API_KEY in .env'; weatherContainer.prepend(warningEl); } // Still set up sun times from calculation so dark mode works await this.updateSunTimesFromCalculation(); this.updateDarkModeBasedOnTime(); this.dispatchDarkModeEvent(); return; } // Fetch weather data await this.fetchWeatherData(); // Check if it's dark outside (only affects auto mode) this.updateDarkModeBasedOnTime(); // Set up interval to check dark mode every minute (only affects auto mode) this.darkModeCheckInterval = setInterval(() => { // Only update dark mode based on time if ConfigManager has dark mode set to 'auto' if (this.shouldUseAutoDarkMode()) { this.updateDarkModeBasedOnTime(); } }, 60000); // Set up interval to refresh weather data setInterval(() => this.fetchWeatherData(), this.options.refreshInterval); // Dispatch initial dark mode state this.dispatchDarkModeEvent(); console.log('WeatherManager initialized'); } catch (error) { console.error('Error initializing WeatherManager:', error); // Fallback to calculated sun times if API fails await this.updateSunTimesFromCalculation(); this.updateDarkModeBasedOnTime(); this.dispatchDarkModeEvent(); } } /** * Check if we should use automatic dark mode based on ConfigManager settings */ shouldUseAutoDarkMode() { // If there's a ConfigManager instance with a config if (window.configManager && window.configManager.config) { // Only use auto dark mode if the setting is 'auto' return window.configManager.config.darkMode === 'auto'; } // Default to true if no ConfigManager is available return true; } /** * Fetch weather data from OpenWeatherMap API */ async fetchWeatherData() { try { // Fetch current weather (lang=se for Swedish descriptions) const currentWeatherUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${this.options.latitude}&lon=${this.options.longitude}&units=metric&lang=se&appid=${this.options.apiKey}`; const currentWeatherResponse = await fetch(currentWeatherUrl); const currentWeatherData = await currentWeatherResponse.json(); if (currentWeatherData.cod !== 200) { throw new Error(`API Error: ${currentWeatherData.message}`); } // Fetch 3-hour interval forecast (cnt=8 limits to ~24h of data) const forecastUrl = `https://api.openweathermap.org/data/2.5/forecast?lat=${this.options.latitude}&lon=${this.options.longitude}&units=metric&lang=se&cnt=8&appid=${this.options.apiKey}`; const forecastResponse = await fetch(forecastUrl); const forecastData = await forecastResponse.json(); if (forecastData.cod !== "200") { throw new Error(`API Error: ${forecastData.message}`); } // Process and store the data this.weatherData = this.processCurrentWeather(currentWeatherData); this.forecastData = this.processForecast(forecastData); this.lastUpdated = new Date(); // Extract sunrise and sunset times from the API response await this.updateSunTimesFromApi(currentWeatherData); // Update the UI with the new data this.updateWeatherUI(); console.log('Weather data updated:', this.weatherData); return this.weatherData; } catch (error) { console.error('Error fetching weather data:', error); // If we don't have any weather data yet, create some default data if (!this.weatherData) { this.weatherData = this.createDefaultWeatherData(); this.forecastData = this.createDefaultForecastData(); } // Fallback to calculated sun times await this.updateSunTimesFromCalculation(); return this.weatherData; } } /** * Process current weather data from API response */ processCurrentWeather(data) { const iconCode = data.weather[0].icon; return { temperature: Math.round(data.main.temp * 10) / 10, // Round to 1 decimal place condition: data.weather[0].main, description: data.weather[0].description, icon: this.getWeatherIconUrl(iconCode), iconCode: iconCode, // Store icon code for classification wind: { speed: Math.round(data.wind.speed * 3.6), // Convert m/s to km/h direction: data.wind.deg }, humidity: data.main.humidity, pressure: data.main.pressure, precipitation: data.rain ? (data.rain['1h'] || 0) : 0, location: data.name, country: data.sys.country, timestamp: new Date(data.dt * 1000) }; } /** * Process forecast data from API response */ processForecast(data) { // Get the next 7 forecast periods (3-hour intervals, covering ~21 hours) return data.list.slice(0, 7).map(item => { const iconCode = item.weather[0].icon; return { temperature: Math.round(item.main.temp * 10) / 10, condition: item.weather[0].main, description: item.weather[0].description, icon: this.getWeatherIconUrl(iconCode), iconCode: iconCode, // Store icon code for classification timestamp: new Date(item.dt * 1000), precipitation: item.rain ? (item.rain['3h'] || 0) : 0 }; }); } /** * Get weather icon URL from icon code */ getWeatherIconUrl(iconCode) { return `https://openweathermap.org/img/wn/${iconCode}@4x.png`; } /** * Classify a weather icon and return the appropriate CSS classes * @param {string} iconCode - OWM icon code (e.g. '01d', '13n') * @param {string} condition - Weather condition text (e.g. 'Clear', 'Clouds') * @returns {string[]} Array of CSS class names to apply */ classifyWeatherIcon(iconCode, condition) { const code = iconCode ? iconCode.replace(/[dn]$/, '') : ''; // Snow: icon 13x or condition contains 'Snow' if (code === '13' || condition.includes('Snow')) { return ['weather-snow']; } // Clear sun: icon 01x or condition is exactly 'Clear' if (code === '01' || condition === 'Clear') { return ['weather-sun', 'weather-clear-sun']; } // Sun behind clouds: icon 02-04x or cloudy condition if (['02', '03', '04'].includes(code) || (condition.includes('Clouds') && !condition.includes('Clear'))) { return ['weather-sun', 'weather-clouds-sun']; } return []; } /** * Apply weather icon CSS classes to an element */ applyWeatherIconClasses(element, iconCode, condition) { element.classList.remove('weather-sun', 'weather-snow', 'weather-clear-sun', 'weather-clouds-sun'); const classes = this.classifyWeatherIcon(iconCode, condition); if (classes.length > 0) { element.classList.add(...classes); } } /** * Create default weather data for fallback */ createDefaultWeatherData() { return { temperature: 7.1, condition: 'Clear', description: 'clear sky', icon: 'https://openweathermap.org/img/wn/01d@4x.png', iconCode: '01d', wind: { speed: 14.8, direction: 270 }, humidity: 65, pressure: 1012.0, precipitation: 0.00, location: 'Stockholm', country: 'SE', timestamp: new Date() }; } /** * Create default forecast data for fallback */ createDefaultForecastData() { const now = new Date(); const forecasts = []; // Create 7 forecast entries for (let i = 0; i < 7; i++) { const forecastTime = new Date(now); forecastTime.setHours(now.getHours() + i); forecasts.push({ temperature: 7.1 - (i * 0.3), // Decrease temperature slightly each hour condition: i < 2 ? 'Clear' : 'Clouds', description: i < 2 ? 'clear sky' : 'few clouds', icon: i < 2 ? 'https://openweathermap.org/img/wn/01n@4x.png' : 'https://openweathermap.org/img/wn/02n@4x.png', iconCode: i < 2 ? '01n' : '02n', timestamp: forecastTime, precipitation: 0 }); } return forecasts; } /** * Update the weather UI with current data */ updateWeatherUI() { if (!this.weatherData || !this.forecastData) return; try { // Update current weather const locationElement = document.querySelector('#custom-weather h3'); if (locationElement) { locationElement.textContent = this.weatherData.location; } const conditionElement = document.querySelector('#custom-weather .weather-icon div'); if (conditionElement) { conditionElement.textContent = this.weatherData.condition; } const iconElement = document.querySelector('#custom-weather .weather-icon img'); if (iconElement) { iconElement.src = this.weatherData.icon; iconElement.alt = this.weatherData.description; iconElement.setAttribute('data-condition', this.weatherData.condition); this.applyWeatherIconClasses(iconElement, this.weatherData.iconCode, this.weatherData.condition); } const temperatureElement = document.querySelector('#custom-weather .temperature'); if (temperatureElement) { temperatureElement.textContent = `${this.weatherData.temperature} °C`; } // Update forecast const forecastContainer = document.querySelector('#custom-weather .forecast'); if (forecastContainer) { // Clear existing forecast forecastContainer.innerHTML = ''; // Add current weather as "Nu" (Swedish for "Now") const nowElement = document.createElement('div'); nowElement.className = 'forecast-hour'; const nowIcon = document.createElement('img'); nowIcon.src = this.weatherData.icon; nowIcon.alt = this.weatherData.description; nowIcon.width = 56; nowIcon.setAttribute('data-condition', this.weatherData.condition); this.applyWeatherIconClasses(nowIcon, this.weatherData.iconCode, this.weatherData.condition); nowElement.innerHTML = `