mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-23 03:59:30 -07:00
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
This commit is contained in:
@@ -1,14 +1,16 @@
|
|||||||
// display extended forecast graphically
|
// 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 STATUS from './status.mjs';
|
||||||
import { json } from './utils/fetch.mjs';
|
import { safeJson } from './utils/fetch.mjs';
|
||||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||||
import { getLargeIcon } from './icons.mjs';
|
import { getLargeIcon } from './icons.mjs';
|
||||||
import { preloadImg } from './utils/image.mjs';
|
import { preloadImg } from './utils/image.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import { registerDisplay } from './navigation.mjs';
|
import { registerDisplay } from './navigation.mjs';
|
||||||
import settings from './settings.mjs';
|
import settings from './settings.mjs';
|
||||||
|
import filterExpiredPeriods from './utils/forecast-utils.mjs';
|
||||||
|
import { debugFlag } from './utils/debug.mjs';
|
||||||
|
|
||||||
class ExtendedForecast extends WeatherDisplay {
|
class ExtendedForecast extends WeatherDisplay {
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
@@ -21,27 +23,30 @@ class ExtendedForecast extends WeatherDisplay {
|
|||||||
async getData(weatherParameters, refresh) {
|
async getData(weatherParameters, refresh) {
|
||||||
if (!super.getData(weatherParameters, refresh)) return;
|
if (!super.getData(weatherParameters, refresh)) return;
|
||||||
|
|
||||||
// request us or si units
|
|
||||||
try {
|
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: {
|
data: {
|
||||||
units: settings.units.value,
|
units: settings.units.value,
|
||||||
},
|
},
|
||||||
retryCount: 3,
|
retryCount: 3,
|
||||||
stillWaiting: () => this.stillWaiting(),
|
stillWaiting: () => this.stillWaiting(),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
console.error('Unable to get extended forecast');
|
// if there's no new data and no previous data, fail
|
||||||
console.error(error.status, error.responseJSON);
|
|
||||||
// if there's no previous data, fail
|
|
||||||
if (!this.data) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// we only get here if there was no error above
|
// we only get here if there was data (new or existing)
|
||||||
this.screenIndex = 0;
|
this.screenIndex = 0;
|
||||||
this.setStatus(STATUS.loaded);
|
this.setStatus(STATUS.loaded);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Unexpected error getting Extended Forecast: ${error.message}`);
|
||||||
|
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async drawCanvas() {
|
async drawCanvas() {
|
||||||
@@ -49,7 +54,7 @@ class ExtendedForecast extends WeatherDisplay {
|
|||||||
|
|
||||||
// determine bounds
|
// determine bounds
|
||||||
// grab the first three or second set of three array elements
|
// 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
|
// create each day template
|
||||||
const days = forecast.map((Day) => {
|
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
|
// the api provides the forecast in 12 hour increments, flatten to day increments with high and low temperatures
|
||||||
const parse = (fullForecast) => {
|
const parse = (fullForecast, forecastUrl) => {
|
||||||
// create a list of days starting with today
|
// filter out expired periods first
|
||||||
const Days = [0, 1, 2, 3, 4, 5, 6];
|
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 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' });
|
return date.toLocaleString({ weekday: 'short' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (debugFlag('extendedforecast')) {
|
||||||
|
console.log(`ExtendedForecast: Generated date labels: [${dates.join(', ')}]`);
|
||||||
|
}
|
||||||
|
|
||||||
// track the destination forecast index
|
// track the destination forecast index
|
||||||
let destIndex = 0;
|
let destIndex = 0;
|
||||||
const forecast = [];
|
const forecast = [];
|
||||||
fullForecast.forEach((period) => {
|
|
||||||
|
for (let i = startIndex; i < activePeriods.length; i += 1) {
|
||||||
|
const period = activePeriods[i];
|
||||||
|
|
||||||
// create the destination object if necessary
|
// create the destination object if necessary
|
||||||
if (!forecast[destIndex]) {
|
if (!forecast[destIndex]) {
|
||||||
forecast.push({
|
forecast.push({
|
||||||
@@ -110,12 +148,21 @@ const parse = (fullForecast) => {
|
|||||||
if (period.isDaytime) {
|
if (period.isDaytime) {
|
||||||
// day time is the high temperature
|
// day time is the high temperature
|
||||||
fDay.high = period.temperature;
|
fDay.high = period.temperature;
|
||||||
destIndex += 1;
|
// Wait for the corresponding night period to increment
|
||||||
} else {
|
} else {
|
||||||
// low temperature
|
// low temperature
|
||||||
fDay.low = period.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;
|
return forecast;
|
||||||
};
|
};
|
||||||
|
|||||||
30
server/scripts/modules/utils/forecast-utils.mjs
Normal file
30
server/scripts/modules/utils/forecast-utils.mjs
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user