Compare commits

...

7 Commits

Author SHA1 Message Date
Matt Walsh
e4672d12d7 5.9.8 2023-04-22 21:24:17 -05:00
Matt Walsh
04ed3e0a52 fix npm lint script 2023-04-22 21:23:31 -05:00
Matt Walsh
58a337efbf fix hazards - current conditions race condition close #24 2023-04-22 21:16:30 -05:00
Matt Walsh
249cbb93e6 add test via multiple locations 2023-01-17 16:10:06 -06:00
Matt Walsh
888b35ea73 code cleanup 2023-01-17 14:13:51 -06:00
Matt Walsh
2a9e5b370e don't re-parse current conditions 2023-01-17 11:26:57 -06:00
Matt Walsh
87d4155d71 capture dist 2023-01-10 14:13:30 -06:00
17 changed files with 2308 additions and 7924 deletions

12
.vscode/launch.json vendored
View File

@@ -63,7 +63,17 @@
"env": {
"DIST": "1"
}
}
},
{
"name": "Test",
"program": "${workspaceFolder}/tests/index.js",
"request": "launch",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"outputCapture": "std"
},
],
"compounds": [
{

2
dist/index.html vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -31,17 +31,17 @@ const index = (req, res) => {
};
// debugging
if (process.env?.DIST !== '1') {
// debugging
app.get('/index.html', index);
app.get('/', index);
app.get('*', express.static(path.join(__dirname, './server')));
} else {
if (process.env?.DIST === '1') {
// distribution
app.use('/images', express.static(path.join(__dirname, './server/images')));
app.use('/fonts', express.static(path.join(__dirname, './server/fonts')));
app.use('/scripts', express.static(path.join(__dirname, './server/scripts')));
app.use('/', express.static(path.join(__dirname, './dist')));
} else {
// debugging
app.get('/index.html', index);
app.get('/', index);
app.get('*', express.static(path.join(__dirname, './server')));
}
const server = app.listen(port, () => {

8447
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
{
"name": "ws4kp",
"version": "5.9.7",
"version": "5.9.8",
"description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build:css": "sass ./server/styles/scss/style.scss ./server/styles/compiled.css",
"lint": "eslint ./server/scripts/**",
"lint:fix": "eslint --fix ./server/scripts/**"
"lint": "eslint ./server/scripts/**/*.mjs",
"lint:fix": "eslint --fix ./server/scripts/**/*.mjs"
},
"repository": {
"type": "git",
@@ -25,8 +25,8 @@
"eslint": "^8.21.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-sonarjs": "^0.17.0",
"eslint-plugin-unicorn": "^45.0.2",
"eslint-plugin-sonarjs": "^0.19.0",
"eslint-plugin-unicorn": "^46.0.0",
"express": "^4.17.1",
"gulp": "^4.0.2",
"gulp-concat": "^2.6.1",

View File

@@ -30,8 +30,9 @@ class CurrentWeather extends WeatherDisplay {
const filteredStations = weatherParameters.stations.filter((station) => station?.properties?.stationIdentifier?.length === 4 && !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
// Load the observations
let observations; let
station;
let observations;
let station;
// station number counter
let stationNum = 0;
while (!observations && stationNum < filteredStations.length) {
@@ -73,7 +74,7 @@ class CurrentWeather extends WeatherDisplay {
}
// we only get here if there was no error above
this.data = { ...observations, station };
this.data = parseData({ ...observations, station });
this.getDataCallback();
// stop here if we're disabled
@@ -84,89 +85,37 @@ class CurrentWeather extends WeatherDisplay {
this.setStatus(STATUS.loaded);
}
// format the data for use outside this function
parseData() {
if (!this.data) return false;
const data = {};
const observations = this.data.features[0].properties;
// values from api are provided in metric
data.observations = observations;
data.Temperature = Math.round(observations.temperature.value);
data.TemperatureUnit = 'C';
data.DewPoint = Math.round(observations.dewpoint.value);
data.Ceiling = Math.round(observations.cloudLayers[0]?.base?.value ?? 0);
data.CeilingUnit = 'm.';
data.Visibility = Math.round(observations.visibility.value / 1000);
data.VisibilityUnit = ' km.';
data.WindSpeed = Math.round(observations.windSpeed.value);
data.WindDirection = directionToNSEW(observations.windDirection.value);
data.Pressure = Math.round(observations.barometricPressure.value);
data.HeatIndex = Math.round(observations.heatIndex.value);
data.WindChill = Math.round(observations.windChill.value);
data.WindGust = Math.round(observations.windGust.value);
data.WindUnit = 'KPH';
data.Humidity = Math.round(observations.relativeHumidity.value);
data.Icon = getWeatherIconFromIconLink(observations.icon);
data.PressureDirection = '';
data.TextConditions = observations.textDescription;
data.station = this.data.station;
// difference since last measurement (pascals, looking for difference of more than 150)
const pressureDiff = (observations.barometricPressure.value - this.data.features[1].properties.barometricPressure.value);
if (pressureDiff > 150) data.PressureDirection = 'R';
if (pressureDiff < -150) data.PressureDirection = 'F';
data.Temperature = celsiusToFahrenheit(data.Temperature);
data.TemperatureUnit = 'F';
data.DewPoint = celsiusToFahrenheit(data.DewPoint);
data.Ceiling = Math.round(metersToFeet(data.Ceiling) / 100) * 100;
data.CeilingUnit = 'ft.';
data.Visibility = kilometersToMiles(observations.visibility.value / 1000);
data.VisibilityUnit = ' mi.';
data.WindSpeed = kphToMph(data.WindSpeed);
data.WindUnit = 'MPH';
data.Pressure = pascalToInHg(data.Pressure).toFixed(2);
data.HeatIndex = celsiusToFahrenheit(data.HeatIndex);
data.WindChill = celsiusToFahrenheit(data.WindChill);
data.WindGust = kphToMph(data.WindGust);
return data;
}
async drawCanvas() {
super.drawCanvas();
const fill = {};
// parse each time to deal with a change in units if necessary
const data = this.parseData();
fill.temp = data.Temperature + String.fromCharCode(176);
let Conditions = data.observations.textDescription;
if (Conditions.length > 15) {
Conditions = shortConditions(Conditions);
let condition = this.data.observations.textDescription;
if (condition.length > 15) {
condition = shortConditions(condition);
}
fill.condition = Conditions;
fill.wind = data.WindDirection.padEnd(3, '') + data.WindSpeed.toString().padStart(3, ' ');
if (data.WindGust) fill['wind-gusts'] = `Gusts to ${data.WindGust}`;
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 },
};
fill.location = locationCleanup(this.data.station.properties.name).substr(0, 20);
if (this.data.WindGust) fill['wind-gusts'] = `Gusts to ${this.data.WindGust}`;
fill.humidity = `${data.Humidity}%`;
fill.dewpoint = data.DewPoint + String.fromCharCode(176);
fill.ceiling = (data.Ceiling === 0 ? 'Unlimited' : data.Ceiling + data.CeilingUnit);
fill.visibility = data.Visibility + data.VisibilityUnit;
fill.pressure = `${data.Pressure} ${data.PressureDirection}`;
if (data.observations.heatIndex.value && data.HeatIndex !== data.Temperature) {
if (this.data.observations.heatIndex.value && this.data.HeatIndex !== this.data.Temperature) {
fill['heat-index-label'] = 'Heat Index:';
fill['heat-index'] = data.HeatIndex + String.fromCharCode(176);
} else if (data.observations.windChill.value && data.WindChill !== '' && data.WindChill < data.Temperature) {
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) {
fill['heat-index-label'] = 'Wind Chill:';
fill['heat-index'] = data.WindChill + String.fromCharCode(176);
fill['heat-index'] = this.data.WindChill + String.fromCharCode(176);
}
fill.icon = { type: 'img', src: data.Icon };
const area = this.elem.querySelector('.main');
area.innerHTML = '';
@@ -180,9 +129,9 @@ class CurrentWeather extends WeatherDisplay {
async getCurrentWeather(stillWaiting) {
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
return new Promise((resolve) => {
if (this.data) resolve(this.parseData());
if (this.data) resolve(this.data);
// data not available, put it into the data callback queue
this.getDataCallbacks.push(() => resolve(this.parseData()));
this.getDataCallbacks.push(() => resolve(this.data));
});
}
}
@@ -206,6 +155,52 @@ const shortConditions = (_condition) => {
return condition;
};
// format the received data
const parseData = (data) => {
const observations = data.features[0].properties;
// values from api are provided in metric
data.observations = observations;
data.Temperature = Math.round(observations.temperature.value);
data.TemperatureUnit = 'C';
data.DewPoint = Math.round(observations.dewpoint.value);
data.Ceiling = Math.round(observations.cloudLayers[0]?.base?.value ?? 0);
data.CeilingUnit = 'm.';
data.Visibility = Math.round(observations.visibility.value / 1000);
data.VisibilityUnit = ' km.';
data.WindSpeed = Math.round(observations.windSpeed.value);
data.WindDirection = directionToNSEW(observations.windDirection.value);
data.Pressure = Math.round(observations.barometricPressure.value);
data.HeatIndex = Math.round(observations.heatIndex.value);
data.WindChill = Math.round(observations.windChill.value);
data.WindGust = Math.round(observations.windGust.value);
data.WindUnit = 'KPH';
data.Humidity = Math.round(observations.relativeHumidity.value);
data.Icon = getWeatherIconFromIconLink(observations.icon);
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';
// convert to us units
data.Temperature = celsiusToFahrenheit(data.Temperature);
data.TemperatureUnit = 'F';
data.DewPoint = celsiusToFahrenheit(data.DewPoint);
data.Ceiling = Math.round(metersToFeet(data.Ceiling) / 100) * 100;
data.CeilingUnit = 'ft.';
data.Visibility = kilometersToMiles(observations.visibility.value / 1000);
data.VisibilityUnit = ' mi.';
data.WindSpeed = kphToMph(data.WindSpeed);
data.WindUnit = 'MPH';
data.Pressure = pascalToInHg(data.Pressure).toFixed(2);
data.HeatIndex = celsiusToFahrenheit(data.HeatIndex);
data.WindChill = celsiusToFahrenheit(data.WindChill);
data.WindGust = kphToMph(data.WindGust);
return data;
};
const display = new CurrentWeather(1, 'current-weather');
registerDisplay(display);

View File

@@ -55,8 +55,8 @@ class ExtendedForecast extends WeatherDisplay {
const fill = {
icon: { type: 'img', src: Day.icon },
condition: Day.text,
date: Day.dayName,
};
fill.date = Day.dayName;
const { low } = Day;
if (low !== undefined) {

View File

@@ -8,7 +8,6 @@ import { DateTime } from '../vendor/auto/luxon.mjs';
class HourlyGraph extends WeatherDisplay {
constructor(navId, elemId, defaultActive) {
// special height and width for scrolling
super(navId, elemId, 'Hourly Graph', defaultActive);
// move the top right data into the correct location on load

View File

@@ -123,10 +123,15 @@ const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
case 'tropical_storm':
return addPath('Thunderstorm.gif');
case 'wind':
case 'wind_few':
case 'wind_sct':
case 'wind_bkn':
case 'wind_ovc':
case 'wind-n':
case 'wind_few-n':
case 'wind_bkn-n':
case 'wind_ovc-n':
return addPath('Wind.gif');
case 'wind_skc':
@@ -207,6 +212,9 @@ const getWeatherIconFromIconLink = (link, _isNightTime) => {
return addPath('CC_Fog.gif');
case 'rain_sleet':
case 'rain_sleet-n':
case 'sleet':
case 'sleet-n':
return addPath('Sleet.gif');
case 'rain_showers':
@@ -242,6 +250,8 @@ const getWeatherIconFromIconLink = (link, _isNightTime) => {
case 'snow_fzra-n':
case 'fzra':
case 'fzra-n':
case 'rain_fzra':
case 'rain_fzra-n':
return addPath('CC_FreezingRain.gif');
case 'snow_sleet':
@@ -265,9 +275,10 @@ const getWeatherIconFromIconLink = (link, _isNightTime) => {
case 'wind_sct':
case 'wind_bkn':
case 'wind_ovc':
return addPath('CC_Windy.gif');
case 'wind_skc':
case 'wind_few-n':
case 'wind_bkn-n':
case 'wind_ovc-n':
case 'wind_skc-n':
case 'wind_sct-n':
return addPath('CC_Windy.gif');

View File

@@ -112,6 +112,12 @@ const updateStatus = (value) => {
value.status = displays[firstDisplayIndex].status;
}
// if hazards data arrives after the firstDisplayIndex loads, then we need to hot wire this to the first display
if (value.id === 0 && value.status === STATUS.loaded && displays[0].timing.totalScreens === 0) {
value.id = firstDisplayIndex;
value.status = displays[firstDisplayIndex].status;
}
// if this is the first display and we're playing, load it up so it starts playing
if (isPlaying() && value.id === firstDisplayIndex && value.status === STATUS.loaded) {
navTo(msg.command.firstFrame);

1
tests/README.md Normal file
View File

@@ -0,0 +1 @@
Currently, tests take a different approach from typical unit testing. The test methodology loads several forecasts for different locations and logs them all to one logger so errors can be found such as missing icons, locations that do not have all of the necessary data or other changes that may occur between geographical locations.

42
tests/index.js Normal file
View File

@@ -0,0 +1,42 @@
const puppeteer = require('puppeteer');
const { setTimeout } = require('node:timers/promises');
const { readFile } = require('fs/promises');
const messageFormatter = require('./messageformatter');
(async () => {
const browser = await puppeteer.launch({
// headless: false,
slowMo: 10,
timeout: 10_000,
dumpio: true,
});
// get the list of locations
const LOCATIONS = JSON.parse(await readFile('./tests/locations.json'));
// get the page
const page = (await browser.pages())[0];
await page.goto('http://localhost:8080');
page.on('console', messageFormatter);
// run all the locations
for (let i = 0; i < LOCATIONS.length; i += 1) {
const location = LOCATIONS[i];
console.log(location);
// eslint-disable-next-line no-await-in-loop
await tester(location, page);
}
browser.close();
})();
const tester = async (location, page) => {
// Set the address
await page.type('#txtAddress', location);
await setTimeout(500);
// get the page
await page.click('#btnGetLatLng');
// wait for errors
await setTimeout(5000);
};

52
tests/locations.json Normal file
View File

@@ -0,0 +1,52 @@
[
"New York, New York",
"Los Angeles, California",
"Chicago, Illinois",
"Houston, Texas",
"Phoenix, Arizona",
"Philadelphia, Pennsylvania",
"San Antonio, Texas",
"San Diego, California",
"Dallas, Texas",
"San Jose, California",
"Austin, Texas",
"Jacksonville, Florida",
"Fort Worth, Texas",
"Columbus, Ohio",
"Charlotte, North Carolina",
"Indianapolis, Indiana",
"San Francisco, California",
"Seattle, Washington",
"Denver, Colorado",
"Nashville, Tennessee",
"Washington, District of Columbia",
"Oklahoma City, Oklahoma",
"Boston, Massachusetts",
"El Paso, Texas",
"Portland, Oregon",
"Las Vegas, Nevada",
"Memphis, Tennessee",
"Detroit, Michigan",
"Baltimore, Maryland",
"Milwaukee, Wisconsin",
"Albuquerque, New Mexico",
"Fresno, California",
"Tucson, Arizona",
"Sacramento, California",
"Mesa, Arizona",
"Kansas City, Missouri",
"Atlanta, Georgia",
"Omaha, Nebraska",
"Colorado Springs, Colorado",
"Raleigh, North Carolina",
"Long Beach, California",
"Virginia Beach, Virginia",
"Oakland, California",
"Miami, Florida",
"Minneapolis, Minnesota",
"Bakersfield, California",
"Tulsa, Oklahoma",
"Aurora, Colorado",
"Arlington, Texas",
"Wichita, Kansas"
]

28
tests/messageformatter.js Normal file
View File

@@ -0,0 +1,28 @@
const chalk = require('chalk');
const describe = (jsHandle) => jsHandle.executionContext().evaluate(
// serialize |obj| however you want
(obj) => `OBJ: ${typeof obj}, ${obj}`,
jsHandle,
);
const colors = {
LOG: chalk.grey,
ERR: chalk.red,
WAR: chalk.yellow,
INF: chalk.cyan,
};
const formatter = async (message) => {
const args = await Promise.all(message.args().map((arg) => describe(arg)));
// make ability to paint different console[types]
const type = message.type().substr(0, 3).toUpperCase();
const color = colors[type] || chalk.blue;
let text = '';
for (let i = 0; i < args.length; i += 1) {
text += `[${i}] ${args[i]} `;
}
console.log(color(`CONSOLE.${type}: ${message.text()}\n${text} `));
};
module.exports = formatter;

1436
tests/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

15
tests/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "ws4kp-tests",
"version": "1.0.0",
"description": "Currently, tests take a different approach from typical unit testing. The test methodology loads several forecasts for different locations and logs them all to one logger so errors can be found such as missing icons, locations that do not have all of the necessary data or other changes that may occur between geographical locations.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"dependencies": {
"chalk": "^4.0.0",
"puppeteer": "^19.5.2"
}
}