mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-17 09:09:30 -07:00
add personal-weather and rearrange pages
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import 'dotenv/config';
|
||||
import { config } from 'dotenv';
|
||||
config({
|
||||
path: ['gulp/.env', '.env']
|
||||
})
|
||||
import {
|
||||
src, dest, series, parallel,
|
||||
} from 'gulp';
|
||||
@@ -83,6 +86,10 @@ const mjsSources = [
|
||||
'server/scripts/index.mjs',
|
||||
];
|
||||
|
||||
if (!process.env.DISABLE_PERSONAL) {
|
||||
mjsSources.push('server/scripts/modues/personal-weather.mjs')
|
||||
}
|
||||
|
||||
const buildJs = () => src(mjsSources)
|
||||
.pipe(webpack(webpackOptions))
|
||||
.pipe(dest(RESOURCES_PATH));
|
||||
@@ -113,6 +120,7 @@ const compressHtml = async () => src(htmlSources)
|
||||
version,
|
||||
OVERRIDES,
|
||||
query: {},
|
||||
DISABLE_PERSONAL: process.env.DISABLE_PERSONAL === '1',
|
||||
}))
|
||||
.pipe(rename({ extname: '.html' }))
|
||||
.pipe(htmlmin({ collapseWhitespace: true }))
|
||||
|
||||
@@ -9,6 +9,7 @@ import playlist from './src/playlist.mjs';
|
||||
import OVERRIDES from './src/overrides.mjs';
|
||||
import cache from './proxy/cache.mjs';
|
||||
import devTools from './src/com.chrome.devtools.mjs';
|
||||
import ambientRelay from "./src/personal-weather.mjs";
|
||||
|
||||
const travelCities = JSON.parse(await readFile('./datagenerators/output/travelcities.json'));
|
||||
const regionalCities = JSON.parse(await readFile('./datagenerators/output/regionalcities.json'));
|
||||
@@ -59,6 +60,7 @@ const renderIndex = (req, res, production = false) => {
|
||||
version,
|
||||
OVERRIDES,
|
||||
query: req.query,
|
||||
DISABLE_PERSONAL: process.env.DISABLE_PERSONAL === '1'
|
||||
});
|
||||
};
|
||||
|
||||
@@ -170,6 +172,7 @@ if (process.env?.DIST === '1') {
|
||||
app.use('/resources', express.static('./server/scripts/modules'));
|
||||
app.get('/', index);
|
||||
app.get('/.well-known/appspecific/com.chrome.devtools.json', devTools);
|
||||
app.get('/ambient-relay/api/latest', ambientRelay);
|
||||
app.get('*name', express.static('./server', staticOptions));
|
||||
}
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ const formatTimesForColumn = (times) => {
|
||||
};
|
||||
|
||||
// register display
|
||||
const display = new Almanac(9, 'almanac');
|
||||
const display = new Almanac(10, 'almanac');
|
||||
registerDisplay(display);
|
||||
|
||||
export default display.getSun.bind(display);
|
||||
|
||||
@@ -209,4 +209,4 @@ const shortenExtendedForecastText = (long) => {
|
||||
};
|
||||
|
||||
// register display
|
||||
registerDisplay(new ExtendedForecast(8, 'extended-forecast'));
|
||||
registerDisplay(new ExtendedForecast(9, 'extended-forecast'));
|
||||
|
||||
@@ -148,4 +148,4 @@ const drawPath = (path, ctx, options) => {
|
||||
const formatTime = (time) => time.setZone(timeZone()).toFormat('ha').slice(0, -1);
|
||||
|
||||
// register display
|
||||
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
|
||||
registerDisplay(new HourlyGraph(5, 'hourly-graph'));
|
||||
|
||||
@@ -255,7 +255,7 @@ const expand = (data, maxHours = 24) => {
|
||||
};
|
||||
|
||||
// register display
|
||||
const display = new Hourly(3, 'hourly', false);
|
||||
const display = new Hourly(4, 'hourly', false);
|
||||
registerDisplay(display);
|
||||
|
||||
export default display.getHourlyData.bind(display);
|
||||
|
||||
@@ -205,4 +205,4 @@ const shortenCurrentConditions = (_condition) => {
|
||||
return condition;
|
||||
};
|
||||
// register display
|
||||
registerDisplay(new LatestObservations(2, 'latest-observations'));
|
||||
registerDisplay(new LatestObservations(3, 'latest-observations'));
|
||||
|
||||
@@ -262,4 +262,4 @@ const parse = (forecast, forecastUrl) => {
|
||||
}));
|
||||
};
|
||||
// register display
|
||||
registerDisplay(new LocalForecast(7, 'local-forecast'));
|
||||
registerDisplay(new LocalForecast(8, 'local-forecast'));
|
||||
|
||||
159
server/scripts/modules/personal-weather.mjs
Normal file
159
server/scripts/modules/personal-weather.mjs
Normal file
@@ -0,0 +1,159 @@
|
||||
// current weather conditions display
|
||||
import STATUS from './status.mjs';
|
||||
import { safeJson } from './utils/fetch.mjs';
|
||||
import { directionToNSEW } from './utils/calc.mjs';
|
||||
import { locationCleanup } from './utils/string.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import {
|
||||
temperature, windSpeed, pressure, distanceMeters, distanceKilometers,
|
||||
} from './utils/units.mjs';
|
||||
import { debugFlag } from './utils/debug.mjs';
|
||||
import Setting from './utils/setting.mjs';
|
||||
|
||||
class PersonalWeather extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
super(navId, elemId, 'Personal Weather Station', true);
|
||||
}
|
||||
|
||||
async getData(weatherParameters, refresh) {
|
||||
// always load the data for use in the lower scroll
|
||||
const superResult = super.getData(weatherParameters, refresh);
|
||||
|
||||
const dataUrl = '/ambient-relay/api/latest';
|
||||
|
||||
let personalData;
|
||||
try {
|
||||
personalData = await safeJson(dataUrl, {
|
||||
retryCount: 3,
|
||||
stillWaiting: () => this.stillWaiting(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Unexpected error getting personal weather station data from: ${dataUrl}: ${error.message}`);
|
||||
}
|
||||
// test for data received
|
||||
if (!personalData) {
|
||||
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||
// send failed to subscribers
|
||||
this.getDataCallback(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
// we only get here if there was no error above
|
||||
this.data = parseData(personalData);
|
||||
this.getDataCallback();
|
||||
|
||||
// stop here if we're disabled
|
||||
if (!superResult) return;
|
||||
|
||||
// Data is available, ensure we're enabled for display
|
||||
this.timing.totalScreens = 1;
|
||||
this.setStatus(STATUS.loaded);
|
||||
}
|
||||
|
||||
async drawCanvas() {
|
||||
super.drawCanvas();
|
||||
|
||||
const wind = (typeof this.data.WindSpeed === 'number') ? this.data.WindDirection.padEnd(3, '') + this.data.WindSpeed.toString().padStart(3, ' ') : this.data.WindSpeed;
|
||||
|
||||
// get location (city name) from StationInfo if available (allows for overrides)
|
||||
const location = (StationInfo[this.data.station.properties.stationIdentifier]?.city ?? locationCleanup(this.data.station.properties.name)).substr(0, 20);
|
||||
|
||||
const fill = {
|
||||
temp: this.data.Temperature + String.fromCharCode(176),
|
||||
condition,
|
||||
wind,
|
||||
location,
|
||||
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 },
|
||||
};
|
||||
|
||||
if (this.data.WindGust !== '-') fill['wind-gusts'] = `Gusts to ${this.data.WindGust}`;
|
||||
|
||||
if (this.data.observations.heatIndex.value && this.data.HeatIndex !== this.data.Temperature) {
|
||||
fill['heat-index-label'] = 'Heat Index:';
|
||||
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'] = this.data.WindChill + String.fromCharCode(176);
|
||||
}
|
||||
|
||||
const area = this.elem.querySelector('.main');
|
||||
|
||||
area.innerHTML = '';
|
||||
area.append(this.fillTemplate('weather', fill));
|
||||
|
||||
this.finishDraw();
|
||||
}
|
||||
|
||||
// make data available outside this class
|
||||
// promise allows for data to be requested before it is available
|
||||
async getCurrentWeather(stillWaiting) {
|
||||
// an external caller has requested data, set up auto reload
|
||||
this.setAutoReload();
|
||||
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
|
||||
return new Promise((resolve) => {
|
||||
if (this.data) resolve(this.data);
|
||||
// data not available, put it into the data callback queue
|
||||
this.getDataCallbacks.push(() => resolve(this.data));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// format the received data
|
||||
const parseData = (data) => {
|
||||
// get the unit converter
|
||||
const windConverter = windSpeed('us');
|
||||
const temperatureConverter = temperature('us');
|
||||
const metersConverter = distanceMeters('us');
|
||||
const kilometersConverter = distanceKilometers('us');
|
||||
const pressureConverter = pressure('us');
|
||||
|
||||
const observations = data.features[0].properties;
|
||||
// values from api are provided in metric
|
||||
data.observations = observations;
|
||||
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);
|
||||
data.WindDirection = directionToNSEW(observations.windDirection.value);
|
||||
data.WindGust = windConverter(observations.windGust.value);
|
||||
data.WindUnit = windConverter.units;
|
||||
data.Humidity = Math.round(observations.relativeHumidity.value);
|
||||
|
||||
// Get the large icon, but provide a fallback if it returns false
|
||||
const iconResult = getLargeIcon(observations.icon);
|
||||
data.Icon = iconResult || observations.icon; // Use original icon if getLargeIcon returns false
|
||||
|
||||
data.PressureDirection = '';
|
||||
data.TextConditions = observations.textDescription;
|
||||
|
||||
// set wind speed of 0 as calm
|
||||
if (data.WindSpeed === 0) data.WindSpeed = 'Calm';
|
||||
|
||||
// if two measurements are available, use the difference (in pascals) to determine pressure trend
|
||||
if (data.features.length > 1 && data.features[1].properties.barometricPressure?.value) {
|
||||
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;
|
||||
};
|
||||
|
||||
const display = new PersonalWeather(2, 'personal-weather');
|
||||
registerDisplay(display);
|
||||
|
||||
// export default display.getPersonalWeather.bind(display);
|
||||
@@ -231,4 +231,4 @@ class Radar extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// register display
|
||||
registerDisplay(new Radar(11, 'radar'));
|
||||
registerDisplay(new Radar(12, 'radar'));
|
||||
|
||||
@@ -235,4 +235,4 @@ const getAndFormatPoint = async (lat, lon) => {
|
||||
};
|
||||
|
||||
// register display
|
||||
registerDisplay(new RegionalForecast(6, 'regional-forecast'));
|
||||
registerDisplay(new RegionalForecast(7, 'regional-forecast'));
|
||||
|
||||
@@ -146,4 +146,4 @@ class SpcOutlook extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// register display
|
||||
registerDisplay(new SpcOutlook(10, 'spc-outlook'));
|
||||
registerDisplay(new SpcOutlook(11, 'spc-outlook'));
|
||||
|
||||
@@ -222,4 +222,4 @@ const getTravelCitiesDayName = (cities) => cities.reduce((dayName, city) => {
|
||||
}, '');
|
||||
|
||||
// register display, not active by default
|
||||
registerDisplay(new TravelForecast(5, 'travel', false));
|
||||
registerDisplay(new TravelForecast(6, 'travel', false));
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
@use 'shared/_colors' as c;
|
||||
@use 'shared/_utils' as u;
|
||||
|
||||
.weather-display .main.current-weather {
|
||||
// also shared with personal weather
|
||||
.weather-display .main.current-weather,
|
||||
.weather-display .main.personal-weather {
|
||||
&.main {
|
||||
|
||||
.col {
|
||||
|
||||
20
src/personal-weather.mjs
Normal file
20
src/personal-weather.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
// testing data for use with personal weather stations via
|
||||
// ambient-relay https://github.com/jasonkonen/ambient-relay
|
||||
const ambientRelay = (req, res) => {
|
||||
res.json({
|
||||
"id": 123,
|
||||
"mac_address": "00:00:00:00:00:00",
|
||||
"device_name": "My Weather Station",
|
||||
"device_location": "Backyard",
|
||||
"dateutc": 1515436500000,
|
||||
"date": "2018-01-08T18:35:00.000Z",
|
||||
"tempf": 66.9,
|
||||
"humidity": 30,
|
||||
"windspeedmph": 0.9,
|
||||
"baromrelin": 30.05,
|
||||
"dailyrainin": 0,
|
||||
"raw_data": {}
|
||||
})
|
||||
}
|
||||
|
||||
export default ambientRelay;
|
||||
@@ -63,6 +63,9 @@
|
||||
<script type="module" src="scripts/modules/settings.mjs"></script>
|
||||
<script type="module" src="scripts/modules/media.mjs"></script>
|
||||
<script type="module" src="scripts/modules/custom-rss-feed.mjs"></script>
|
||||
<% if (!DISABLE_PERSONAL) { %>
|
||||
<script type="module" src="scripts/modules/personal-weather.mjs"></script>
|
||||
<% } %>
|
||||
<script type="module" src="scripts/index.mjs"></script>
|
||||
<% } %>
|
||||
|
||||
@@ -109,6 +112,11 @@
|
||||
<div id="current-weather-html" class="weather-display">
|
||||
<%- include('partials/current-weather.ejs') %>
|
||||
</div>
|
||||
<% if (!DISABLE_PERSONAL) { %>
|
||||
<div id="personal-weather-html" class="weather-display">
|
||||
<%- include('partials/personal-weather.ejs') %>
|
||||
</div>
|
||||
<% } %>
|
||||
<div id="local-forecast-html" class="weather-display">
|
||||
<%- include('partials/local-forecast.ejs') %>
|
||||
</div>
|
||||
|
||||
40
views/partials/personal-weather.ejs
Normal file
40
views/partials/personal-weather.ejs
Normal file
@@ -0,0 +1,40 @@
|
||||
<%- include('header.ejs', {titleDual:{ top: 'Personal' , bottom: 'Weather Station' }, noaaLogo: false, hasTime: true}) %>
|
||||
<div class="main has-scroll has-box personal-weather">
|
||||
<div class="weather template">
|
||||
<div class="left col">
|
||||
<div class="temp center"></div>
|
||||
<div class="wind-container">
|
||||
<div class="wind-label">Wind:</div>
|
||||
<div class="wind"></div>
|
||||
</div>
|
||||
<div class="wind-gusts"></div>
|
||||
</div>
|
||||
<div class="right col">
|
||||
<div class="location"></div>
|
||||
<div class="row">
|
||||
<div class="label">Humidity:</div>
|
||||
<div class="humidity value"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">Dewpoint:</div>
|
||||
<div class="dewpoint value"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">Ceiling:</div>
|
||||
<div class="ceiling value"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">Visibility:</div>
|
||||
<div class="visibility value"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">Pressure:</div>
|
||||
<div class="pressure value"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="heat-index-label label"></div>
|
||||
<div class="heat-index value"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user