From dd680c61b006be19385577a3fc8ca256ee766fea Mon Sep 17 00:00:00 2001 From: Eddy G Date: Tue, 24 Jun 2025 23:07:47 -0400 Subject: [PATCH] Enhance extended forecast parsing and error handling - Add module for expired period filtering - Switch from json() to safeJson() for centralized error handling - Improve nighttime period handling to focus on full days - Fix day/night temperature pairing logic - Add debug logging --- server/scripts/modules/extendedforecast.mjs | 87 ++++++++++++++----- .../scripts/modules/utils/forecast-utils.mjs | 30 +++++++ 2 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 server/scripts/modules/utils/forecast-utils.mjs diff --git a/server/scripts/modules/extendedforecast.mjs b/server/scripts/modules/extendedforecast.mjs index f3d02b8..c7fe98a 100644 --- a/server/scripts/modules/extendedforecast.mjs +++ b/server/scripts/modules/extendedforecast.mjs @@ -1,14 +1,16 @@ // display extended forecast graphically -// technically uses the same data as the local forecast, we'll let the browser do the caching of that +// (technically this uses the same data as the local forecast, but we'll let the cache deal with that) import STATUS from './status.mjs'; -import { json } from './utils/fetch.mjs'; +import { safeJson } from './utils/fetch.mjs'; import { DateTime } from '../vendor/auto/luxon.mjs'; import { getLargeIcon } from './icons.mjs'; import { preloadImg } from './utils/image.mjs'; import WeatherDisplay from './weatherdisplay.mjs'; import { registerDisplay } from './navigation.mjs'; import settings from './settings.mjs'; +import filterExpiredPeriods from './utils/forecast-utils.mjs'; +import { debugFlag } from './utils/debug.mjs'; class ExtendedForecast extends WeatherDisplay { constructor(navId, elemId) { @@ -21,27 +23,30 @@ class ExtendedForecast extends WeatherDisplay { async getData(weatherParameters, refresh) { if (!super.getData(weatherParameters, refresh)) return; - // request us or si units try { - this.data = await json(this.weatherParameters.forecast, { + // request us or si units using centralized safe handling + this.data = await safeJson(this.weatherParameters.forecast, { data: { units: settings.units.value, }, retryCount: 3, stillWaiting: () => this.stillWaiting(), }); - } catch (error) { - console.error('Unable to get extended forecast'); - console.error(error.status, error.responseJSON); - // if there's no previous data, fail + + // if there's no new data and no previous data, fail if (!this.data) { - this.setStatus(STATUS.failed); + // console.warn(`Unable to get extended forecast for ${this.weatherParameters.latitude},${this.weatherParameters.longitude} in ${this.weatherParameters.state}`); + if (this.isEnabled) this.setStatus(STATUS.failed); return; } + + // we only get here if there was data (new or existing) + this.screenIndex = 0; + this.setStatus(STATUS.loaded); + } catch (error) { + console.error(`Unexpected error getting Extended Forecast: ${error.message}`); + if (this.isEnabled) this.setStatus(STATUS.failed); } - // we only get here if there was no error above - this.screenIndex = 0; - this.setStatus(STATUS.loaded); } async drawCanvas() { @@ -49,7 +54,7 @@ class ExtendedForecast extends WeatherDisplay { // determine bounds // grab the first three or second set of three array elements - const forecast = parse(this.data.properties.periods).slice(0 + 3 * this.screenIndex, 3 + this.screenIndex * 3); + const forecast = parse(this.data.properties.periods, this.weatherParameters.forecast).slice(0 + 3 * this.screenIndex, 3 + this.screenIndex * 3); // create each day template const days = forecast.map((Day) => { @@ -78,19 +83,52 @@ class ExtendedForecast extends WeatherDisplay { } // the api provides the forecast in 12 hour increments, flatten to day increments with high and low temperatures -const parse = (fullForecast) => { - // create a list of days starting with today - const Days = [0, 1, 2, 3, 4, 5, 6]; +const parse = (fullForecast, forecastUrl) => { + // filter out expired periods first + const activePeriods = filterExpiredPeriods(fullForecast, forecastUrl); + if (debugFlag('extendedforecast')) { + console.log('ExtendedForecast: First few active periods:'); + activePeriods.slice(0, 4).forEach((period, index) => { + console.log(` [${index}] ${period.name}: ${period.startTime} to ${period.endTime} (isDaytime: ${period.isDaytime})`); + }); + } + + // Skip the first period if it's nighttime (like "Tonight") since extended forecast + // should focus on upcoming full days, not the end of the current day + let startIndex = 0; + let dateOffset = 0; // offset for date labels when we skip periods + + if (activePeriods.length > 0 && !activePeriods[0].isDaytime) { + startIndex = 1; + dateOffset = 1; // start date labels from tomorrow since we're skipping tonight + if (debugFlag('extendedforecast')) { + console.log(`ExtendedForecast: Skipping first period "${activePeriods[0].name}" because it's nighttime`); + } + } else if (activePeriods.length > 0) { + if (debugFlag('extendedforecast')) { + console.log(`ExtendedForecast: Starting with first period "${activePeriods[0].name}" because it's daytime`); + } + } + + // create a list of days starting with the appropriate day + const Days = [0, 1, 2, 3, 4, 5, 6]; const dates = Days.map((shift) => { - const date = DateTime.local().startOf('day').plus({ days: shift }); + const date = DateTime.local().startOf('day').plus({ days: shift + dateOffset }); return date.toLocaleString({ weekday: 'short' }); }); + if (debugFlag('extendedforecast')) { + console.log(`ExtendedForecast: Generated date labels: [${dates.join(', ')}]`); + } + // track the destination forecast index let destIndex = 0; const forecast = []; - fullForecast.forEach((period) => { + + for (let i = startIndex; i < activePeriods.length; i += 1) { + const period = activePeriods[i]; + // create the destination object if necessary if (!forecast[destIndex]) { forecast.push({ @@ -110,12 +148,21 @@ const parse = (fullForecast) => { if (period.isDaytime) { // day time is the high temperature fDay.high = period.temperature; - destIndex += 1; + // Wait for the corresponding night period to increment } else { // low temperature fDay.low = period.temperature; + // Increment after processing night period + destIndex += 1; } - }); + } + + if (debugFlag('extendedforecast')) { + console.log('ExtendedForecast: Final forecast array:'); + forecast.forEach((day, index) => { + console.log(` [${index}] ${day.dayName}: High=${day.high}°, Low=${day.low}°, Text="${day.text}"`); + }); + } return forecast; }; diff --git a/server/scripts/modules/utils/forecast-utils.mjs b/server/scripts/modules/utils/forecast-utils.mjs new file mode 100644 index 0000000..52970f0 --- /dev/null +++ b/server/scripts/modules/utils/forecast-utils.mjs @@ -0,0 +1,30 @@ +// shared utility functions for forecast processing + +/** + * Filter out expired periods from forecast data + * @param {Array} periods - Array of forecast periods + * @param {string} forecastUrl - URL used for logging (optional) + * @returns {Array} - Array of active (non-expired) periods + */ +const filterExpiredPeriods = (periods, forecastUrl = '') => { + const now = new Date(); + + const { activePeriods, removedPeriods } = periods.reduce((acc, period) => { + const endTime = new Date(period.endTime); + if (endTime > now) { + acc.activePeriods.push(period); + } else { + acc.removedPeriods.push(period); + } + return acc; + }, { activePeriods: [], removedPeriods: [] }); + + if (removedPeriods.length > 0) { + const source = forecastUrl ? ` from ${forecastUrl}` : ''; + console.log(`🚮 Forecast: Removed expired periods${source}: ${removedPeriods.map((p) => `${p.name} (ended ${p.endTime})`).join(', ')}`); + } + + return activePeriods; +}; + +export default filterExpiredPeriods;