2020-09-04 13:02:20 -05:00
// current weather conditions display
2022-11-22 16:19:10 -06:00
import STATUS from './status.mjs' ;
2025-05-29 08:30:01 -05:00
import { preloadImg } from './utils/image.mjs' ;
2025-06-24 23:05:51 -04:00
import { safeJson } from './utils/fetch.mjs' ;
2022-11-22 16:19:10 -06:00
import { directionToNSEW } from './utils/calc.mjs' ;
import { locationCleanup } from './utils/string.mjs' ;
2025-05-14 15:03:35 -05:00
import { getLargeIcon } from './icons.mjs' ;
2022-11-22 16:29:10 -06:00
import WeatherDisplay from './weatherdisplay.mjs' ;
2022-12-06 16:14:56 -06:00
import { registerDisplay } from './navigation.mjs' ;
2025-06-24 23:05:51 -04:00
import augmentObservationWithMetar from './utils/metar.mjs' ;
2022-12-06 16:25:28 -06:00
import {
2025-02-23 23:29:39 -06:00
temperature , windSpeed , pressure , distanceMeters , distanceKilometers ,
2022-12-06 16:25:28 -06:00
} from './utils/units.mjs' ;
2025-06-24 23:05:51 -04:00
import { debugFlag } from './utils/debug.mjs' ;
2020-09-04 13:02:20 -05:00
2022-12-13 16:31:18 -06:00
// some stations prefixed do not provide all the necessary data
const skipStations = [ 'U' , 'C' , 'H' , 'W' , 'Y' , 'T' , 'S' , 'M' , 'O' , 'L' , 'A' , 'F' , 'B' , 'N' , 'V' , 'R' , 'D' , 'E' , 'I' , 'G' , 'J' ] ;
2020-09-04 13:02:20 -05:00
class CurrentWeather extends WeatherDisplay {
2020-10-29 16:44:28 -05:00
constructor ( navId , elemId ) {
2022-11-21 21:50:22 -06:00
super ( navId , elemId , 'Current Conditions' , true ) ;
2020-09-04 13:02:20 -05:00
}
2025-04-02 20:52:33 -05:00
async getData ( weatherParameters , refresh ) {
2022-12-12 13:53:33 -06:00
// always load the data for use in the lower scroll
2025-04-02 20:52:33 -05:00
const superResult = super . getData ( weatherParameters , refresh ) ;
2025-04-02 20:58:53 -05:00
// note: current weather does not use old data on a silent refresh
// this is deliberate because it can pull data from more than one station in sequence
2020-09-25 09:55:29 -05:00
2022-12-13 16:31:18 -06:00
// filter for 4-letter observation stations, only those contain sky conditions and thus an icon
2025-04-02 20:52:33 -05:00
const filteredStations = this . weatherParameters . stations . filter ( ( station ) => station ? . properties ? . stationIdentifier ? . length === 4 && ! skipStations . includes ( station . properties . stationIdentifier . slice ( 0 , 1 ) ) ) ;
2022-12-13 16:31:18 -06:00
2020-09-04 13:02:20 -05:00
// Load the observations
2023-01-17 11:26:57 -06:00
let observations ;
let station ;
2020-10-16 15:16:46 -05:00
// station number counter
let stationNum = 0 ;
2022-12-13 16:31:18 -06:00
while ( ! observations && stationNum < filteredStations . length ) {
2020-10-16 15:16:46 -05:00
// get the station
2022-12-13 16:31:18 -06:00
station = filteredStations [ stationNum ] ;
2020-10-29 16:44:28 -05:00
stationNum += 1 ;
2025-06-24 23:05:51 -04:00
let candidateObservation ;
2020-10-16 15:16:46 -05:00
try {
2022-03-01 15:54:19 -06:00
// eslint-disable-next-line no-await-in-loop
2025-06-24 23:05:51 -04:00
candidateObservation = await safeJson ( ` ${ station . id } /observations ` , {
2020-10-16 15:16:46 -05:00
data : {
2025-06-24 23:05:51 -04:00
limit : 2 , // we need the two most recent observations to calculate pressure direction
2020-10-16 15:16:46 -05:00
} ,
2022-12-12 13:53:33 -06:00
retryCount : 3 ,
stillWaiting : ( ) => this . stillWaiting ( ) ,
2020-10-16 15:16:46 -05:00
} ) ;
2025-06-24 23:05:51 -04:00
} catch ( error ) {
console . error ( ` Unexpected error getting Current Conditions for station ${ station . properties . stationIdentifier } : ${ error . message } (trying next station) ` ) ;
candidateObservation = undefined ;
}
2020-09-04 13:02:20 -05:00
2025-06-24 23:05:51 -04:00
// Check if request was successful and has data
if ( candidateObservation && candidateObservation . features ? . length > 0 ) {
// Check if the observation data is old
const observationTime = new Date ( candidateObservation . features [ 0 ] . properties . timestamp ) ;
const ageInMinutes = ( new Date ( ) - observationTime ) / ( 1000 * 60 ) ;
2025-05-29 20:23:55 -05:00
2025-06-24 23:05:51 -04:00
if ( ageInMinutes > 180 && debugFlag ( 'currentweather' ) ) {
console . warn ( ` Current Observations for station ${ station . properties . stationIdentifier } are ${ ageInMinutes . toFixed ( 0 ) } minutes old (from ${ observationTime . toISOString ( ) } ), trying next station ` ) ;
}
2025-06-19 22:50:09 -05:00
2025-06-24 23:05:51 -04:00
// Attempt making observation data usable with METAR data
const originalData = { ... candidateObservation . features [ 0 ] . properties } ;
candidateObservation . features [ 0 ] . properties = augmentObservationWithMetar ( candidateObservation . features [ 0 ] . properties ) ;
const metarFields = [
{ name : 'temperature' , check : ( orig , metar ) => orig . temperature ? . value === null && metar . temperature ? . value !== null } ,
{ name : 'windSpeed' , check : ( orig , metar ) => orig . windSpeed ? . value === null && metar . windSpeed ? . value !== null } ,
{ name : 'windDirection' , check : ( orig , metar ) => orig . windDirection ? . value === null && metar . windDirection ? . value !== null } ,
{ name : 'windGust' , check : ( orig , metar ) => orig . windGust ? . value === null && metar . windGust ? . value !== null } ,
{ name : 'dewpoint' , check : ( orig , metar ) => orig . dewpoint ? . value === null && metar . dewpoint ? . value !== null } ,
{ name : 'barometricPressure' , check : ( orig , metar ) => orig . barometricPressure ? . value === null && metar . barometricPressure ? . value !== null } ,
{ name : 'relativeHumidity' , check : ( orig , metar ) => orig . relativeHumidity ? . value === null && metar . relativeHumidity ? . value !== null } ,
{ name : 'visibility' , check : ( orig , metar ) => orig . visibility ? . value === null && metar . visibility ? . value !== null } ,
{ name : 'ceiling' , check : ( orig , metar ) => orig . cloudLayers ? . [ 0 ] ? . base ? . value === null && metar . cloudLayers ? . [ 0 ] ? . base ? . value !== null } ,
] ;
const augmentedData = candidateObservation . features [ 0 ] . properties ;
const metarReplacements = metarFields . filter ( ( field ) => field . check ( originalData , augmentedData ) ) . map ( ( field ) => field . name ) ;
if ( debugFlag ( 'currentweather' ) && metarReplacements . length > 0 ) {
console . log ( ` Current Conditions for station ${ station . properties . stationIdentifier } were augmented with METAR data for ${ metarReplacements . join ( ', ' ) } ` ) ;
}
// test data quality - check required fields and allow one optional field to be missing
const requiredFields = [
{ name : 'temperature' , check : ( props ) => props . temperature ? . value === null , required : true } ,
{ name : 'textDescription' , check : ( props ) => props . textDescription === null || props . textDescription === '' , required : true } ,
{ name : 'icon' , check : ( props ) => props . icon === null , required : true } ,
{ name : 'windSpeed' , check : ( props ) => props . windSpeed ? . value === null , required : false } ,
{ name : 'dewpoint' , check : ( props ) => props . dewpoint ? . value === null , required : false } ,
{ name : 'barometricPressure' , check : ( props ) => props . barometricPressure ? . value === null , required : false } ,
{ name : 'visibility' , check : ( props ) => props . visibility ? . value === null , required : false } ,
{ name : 'relativeHumidity' , check : ( props ) => props . relativeHumidity ? . value === null , required : false } ,
{ name : 'ceiling' , check : ( props ) => props . cloudLayers ? . [ 0 ] ? . base ? . value === null , required : false } ,
] ;
const missingRequired = requiredFields . filter ( ( field ) => field . required && field . check ( augmentedData ) ) . map ( ( field ) => field . name ) ;
const missingOptional = requiredFields . filter ( ( field ) => ! field . required && field . check ( augmentedData ) ) . map ( ( field ) => field . name ) ;
const missingOptionalCount = missingOptional . length ;
// Allow one optional field to be missing
if ( missingRequired . length === 0 && missingOptionalCount <= 1 ) {
// Station data is good, use it
observations = candidateObservation ;
if ( debugFlag ( 'currentweather' ) && missingOptional . length > 0 ) {
console . log ( ` Data for station ${ station . properties . stationIdentifier } is missing optional fields: ${ missingOptional . join ( ', ' ) } (acceptable) ` ) ;
}
} else {
const allMissing = [ ... missingRequired , ... missingOptional ] ;
if ( debugFlag ( 'currentweather' ) ) {
console . log ( ` Data for station ${ station . properties . stationIdentifier } is missing fields: ${ allMissing . join ( ', ' ) } ( ${ missingRequired . length } required, ${ missingOptionalCount } optional) (trying next station) ` ) ;
}
}
} else if ( debugFlag ( 'verbose-failures' ) ) {
if ( ! candidateObservation ) {
console . log ( ` Current Observations for station ${ station . properties . stationIdentifier } failed, trying next station ` ) ;
} else {
console . log ( ` No features returned for station ${ station . properties . stationIdentifier } , trying next station ` ) ;
2020-10-16 15:16:46 -05:00
}
}
}
// test for data received
if ( ! observations ) {
2025-06-24 23:05:51 -04:00
console . error ( 'Current Conditions failure: all nearby weather stations exhausted!' ) ;
2022-12-14 13:08:49 -06:00
if ( this . isEnabled ) this . setStatus ( STATUS . failed ) ;
2022-12-08 14:41:15 -06:00
// send failed to subscribers
this . getDataCallback ( undefined ) ;
2020-09-04 13:02:20 -05:00
return ;
}
2020-09-23 11:49:15 -05:00
2020-09-04 13:02:20 -05:00
// we only get here if there was no error above
2023-01-17 11:26:57 -06:00
this . data = parseData ( { ... observations , station } ) ;
2020-10-20 20:04:51 -05:00
this . getDataCallback ( ) ;
2022-12-12 13:53:33 -06:00
// stop here if we're disabled
if ( ! superResult ) return ;
// preload the icon
2025-05-14 15:03:35 -05:00
preloadImg ( getLargeIcon ( observations . features [ 0 ] . properties . icon ) ) ;
2022-12-12 13:53:33 -06:00
this . setStatus ( STATUS . loaded ) ;
2020-09-04 13:02:20 -05:00
}
2020-10-29 16:44:28 -05:00
async drawCanvas ( ) {
2020-09-24 22:44:51 -05:00
super . drawCanvas ( ) ;
2020-09-04 13:02:20 -05:00
2023-01-17 11:26:57 -06:00
let condition = this . data . observations . textDescription ;
if ( condition . length > 15 ) {
condition = shortConditions ( condition ) ;
2020-09-04 13:02:20 -05:00
}
2022-08-02 21:39:27 -05:00
2023-01-17 11:26:57 -06:00
const fill = {
temp : this . data . Temperature + String . fromCharCode ( 176 ) ,
condition ,
wind : this . data . WindDirection . padEnd ( 3 , '' ) + this . data . WindSpeed . toString ( ) . padStart ( 3 , ' ' ) ,
location : locationCleanup ( this . data . station . properties . name ) . substr ( 0 , 20 ) ,
humidity : ` ${ this . data . Humidity } % ` ,
dewpoint : this . data . DewPoint + String . fromCharCode ( 176 ) ,
ceiling : ( this . data . Ceiling === 0 ? 'Unlimited' : this . data . Ceiling + this . data . CeilingUnit ) ,
visibility : this . data . Visibility + this . data . VisibilityUnit ,
pressure : ` ${ this . data . Pressure } ${ this . data . PressureDirection } ` ,
icon : { type : 'img' , src : this . data . Icon } ,
} ;
2025-06-19 22:50:09 -05:00
if ( this . data . WindGust !== '-' ) fill [ 'wind-gusts' ] = ` Gusts to ${ this . data . WindGust } ` ;
2023-01-17 11:26:57 -06:00
if ( this . data . observations . heatIndex . value && this . data . HeatIndex !== this . data . Temperature ) {
2022-08-02 21:39:27 -05:00
fill [ 'heat-index-label' ] = 'Heat Index:' ;
2023-01-17 11:26:57 -06:00
fill [ 'heat-index' ] = this . data . HeatIndex + String . fromCharCode ( 176 ) ;
} else if ( this . data . observations . windChill . value && this . data . WindChill !== '' && this . data . WindChill < this . data . Temperature ) {
2022-08-02 21:39:27 -05:00
fill [ 'heat-index-label' ] = 'Wind Chill:' ;
2023-01-17 11:26:57 -06:00
fill [ 'heat-index' ] = this . data . WindChill + String . fromCharCode ( 176 ) ;
2020-09-04 13:02:20 -05:00
}
2020-09-04 13:38:58 -05:00
2022-08-02 21:39:27 -05:00
const area = this . elem . querySelector ( '.main' ) ;
area . innerHTML = '' ;
area . append ( this . fillTemplate ( 'weather' , fill ) ) ;
2020-09-04 13:38:58 -05:00
2020-09-04 13:02:20 -05:00
this . finishDraw ( ) ;
}
2020-10-20 20:04:51 -05:00
// make data available outside this class
// promise allows for data to be requested before it is available
2022-12-12 13:53:33 -06:00
async getCurrentWeather ( stillWaiting ) {
2025-05-02 23:22:00 -05:00
// an external caller has requested data, set up auto reload
this . setAutoReload ( ) ;
2022-12-12 13:53:33 -06:00
if ( stillWaiting ) this . stillWaitingCallbacks . push ( stillWaiting ) ;
2020-10-20 20:04:51 -05:00
return new Promise ( ( resolve ) => {
2023-01-17 11:26:57 -06:00
if ( this . data ) resolve ( this . data ) ;
2020-10-20 20:04:51 -05:00
// data not available, put it into the data callback queue
2023-01-17 11:26:57 -06:00
this . getDataCallbacks . push ( ( ) => resolve ( this . data ) ) ;
2020-10-20 20:04:51 -05:00
} ) ;
2020-09-24 22:44:51 -05:00
}
2020-10-29 16:44:28 -05:00
}
2022-12-09 13:51:51 -06:00
const shortConditions = ( _condition ) => {
let condition = _condition ;
condition = condition . replace ( /Light/g , 'L' ) ;
condition = condition . replace ( /Heavy/g , 'H' ) ;
condition = condition . replace ( /Partly/g , 'P' ) ;
condition = condition . replace ( /Mostly/g , 'M' ) ;
condition = condition . replace ( /Few/g , 'F' ) ;
condition = condition . replace ( /Thunderstorm/g , 'T\'storm' ) ;
condition = condition . replace ( / in /g , '' ) ;
condition = condition . replace ( /Vicinity/g , '' ) ;
condition = condition . replace ( / and /g , ' ' ) ;
condition = condition . replace ( /Freezing Rain/g , 'Frz Rn' ) ;
condition = condition . replace ( /Freezing/g , 'Frz' ) ;
condition = condition . replace ( /Unknown Precip/g , '' ) ;
condition = condition . replace ( /L Snow Fog/g , 'L Snw/Fog' ) ;
condition = condition . replace ( / with /g , '/' ) ;
return condition ;
} ;
2023-01-17 11:26:57 -06:00
// format the received data
const parseData = ( data ) => {
2025-02-23 23:29:39 -06:00
// get the unit converter
const windConverter = windSpeed ( ) ;
const temperatureConverter = temperature ( ) ;
const metersConverter = distanceMeters ( ) ;
const kilometersConverter = distanceKilometers ( ) ;
const pressureConverter = pressure ( ) ;
2023-01-17 11:26:57 -06:00
const observations = data . features [ 0 ] . properties ;
// values from api are provided in metric
data . observations = observations ;
2025-02-23 23:29:39 -06:00
data . Temperature = temperatureConverter ( observations . temperature . value ) ;
data . TemperatureUnit = temperatureConverter . units ;
data . DewPoint = temperatureConverter ( observations . dewpoint . value ) ;
data . Ceiling = metersConverter ( observations . cloudLayers [ 0 ] ? . base ? . value ? ? 0 ) ;
data . CeilingUnit = metersConverter . units ;
data . Visibility = kilometersConverter ( observations . visibility . value ) ;
data . VisibilityUnit = kilometersConverter . units ;
data . Pressure = pressureConverter ( observations . barometricPressure . value ) ;
data . PressureUnit = pressureConverter . units ;
data . HeatIndex = temperatureConverter ( observations . heatIndex . value ) ;
data . WindChill = temperatureConverter ( observations . windChill . value ) ;
data . WindSpeed = windConverter ( observations . windSpeed . value ) ;
2023-01-17 11:26:57 -06:00
data . WindDirection = directionToNSEW ( observations . windDirection . value ) ;
2025-02-23 23:29:39 -06:00
data . WindGust = windConverter ( observations . windGust . value ) ;
data . WindSpeed = windConverter ( data . WindSpeed ) ;
data . WindUnit = windConverter . units ;
2023-01-17 11:26:57 -06:00
data . Humidity = Math . round ( observations . relativeHumidity . value ) ;
2025-05-14 15:03:35 -05:00
data . Icon = getLargeIcon ( observations . icon ) ;
2023-01-17 11:26:57 -06:00
data . PressureDirection = '' ;
data . TextConditions = observations . textDescription ;
// difference since last measurement (pascals, looking for difference of more than 150)
const pressureDiff = ( observations . barometricPressure . value - data . features [ 1 ] . properties . barometricPressure . value ) ;
if ( pressureDiff > 150 ) data . PressureDirection = 'R' ;
if ( pressureDiff < - 150 ) data . PressureDirection = 'F' ;
return data ;
} ;
2022-12-14 16:28:33 -06:00
const display = new CurrentWeather ( 1 , 'current-weather' ) ;
2022-12-06 16:14:56 -06:00
registerDisplay ( display ) ;
export default display . getCurrentWeather . bind ( display ) ;