diff --git a/gulp/publish-frontend.mjs b/gulp/publish-frontend.mjs index ee5b9c0..c930404 100644 --- a/gulp/publish-frontend.mjs +++ b/gulp/publish-frontend.mjs @@ -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 })) diff --git a/index.mjs b/index.mjs index aea3c6c..8a5e06f 100644 --- a/index.mjs +++ b/index.mjs @@ -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)); } diff --git a/server/scripts/modules/almanac.mjs b/server/scripts/modules/almanac.mjs index 3eee356..ca60897 100644 --- a/server/scripts/modules/almanac.mjs +++ b/server/scripts/modules/almanac.mjs @@ -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); diff --git a/server/scripts/modules/extendedforecast.mjs b/server/scripts/modules/extendedforecast.mjs index 33537e4..efb7591 100644 --- a/server/scripts/modules/extendedforecast.mjs +++ b/server/scripts/modules/extendedforecast.mjs @@ -209,4 +209,4 @@ const shortenExtendedForecastText = (long) => { }; // register display -registerDisplay(new ExtendedForecast(8, 'extended-forecast')); +registerDisplay(new ExtendedForecast(9, 'extended-forecast')); diff --git a/server/scripts/modules/hourly-graph.mjs b/server/scripts/modules/hourly-graph.mjs index 9eed947..7ce0364 100644 --- a/server/scripts/modules/hourly-graph.mjs +++ b/server/scripts/modules/hourly-graph.mjs @@ -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')); diff --git a/server/scripts/modules/hourly.mjs b/server/scripts/modules/hourly.mjs index faf601e..7eacef3 100644 --- a/server/scripts/modules/hourly.mjs +++ b/server/scripts/modules/hourly.mjs @@ -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); diff --git a/server/scripts/modules/latestobservations.mjs b/server/scripts/modules/latestobservations.mjs index a7e2446..d82e299 100644 --- a/server/scripts/modules/latestobservations.mjs +++ b/server/scripts/modules/latestobservations.mjs @@ -205,4 +205,4 @@ const shortenCurrentConditions = (_condition) => { return condition; }; // register display -registerDisplay(new LatestObservations(2, 'latest-observations')); +registerDisplay(new LatestObservations(3, 'latest-observations')); diff --git a/server/scripts/modules/localforecast.mjs b/server/scripts/modules/localforecast.mjs index e305714..825430f 100644 --- a/server/scripts/modules/localforecast.mjs +++ b/server/scripts/modules/localforecast.mjs @@ -262,4 +262,4 @@ const parse = (forecast, forecastUrl) => { })); }; // register display -registerDisplay(new LocalForecast(7, 'local-forecast')); +registerDisplay(new LocalForecast(8, 'local-forecast')); diff --git a/server/scripts/modules/personal-weather.mjs b/server/scripts/modules/personal-weather.mjs new file mode 100644 index 0000000..29efc5e --- /dev/null +++ b/server/scripts/modules/personal-weather.mjs @@ -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); diff --git a/server/scripts/modules/radar.mjs b/server/scripts/modules/radar.mjs index 2ffc03e..ef8cc39 100644 --- a/server/scripts/modules/radar.mjs +++ b/server/scripts/modules/radar.mjs @@ -231,4 +231,4 @@ class Radar extends WeatherDisplay { } // register display -registerDisplay(new Radar(11, 'radar')); +registerDisplay(new Radar(12, 'radar')); diff --git a/server/scripts/modules/regionalforecast.mjs b/server/scripts/modules/regionalforecast.mjs index 81ac972..2d0d5cb 100644 --- a/server/scripts/modules/regionalforecast.mjs +++ b/server/scripts/modules/regionalforecast.mjs @@ -235,4 +235,4 @@ const getAndFormatPoint = async (lat, lon) => { }; // register display -registerDisplay(new RegionalForecast(6, 'regional-forecast')); +registerDisplay(new RegionalForecast(7, 'regional-forecast')); diff --git a/server/scripts/modules/spc-outlook.mjs b/server/scripts/modules/spc-outlook.mjs index b0aad2a..f32a06a 100644 --- a/server/scripts/modules/spc-outlook.mjs +++ b/server/scripts/modules/spc-outlook.mjs @@ -146,4 +146,4 @@ class SpcOutlook extends WeatherDisplay { } // register display -registerDisplay(new SpcOutlook(10, 'spc-outlook')); +registerDisplay(new SpcOutlook(11, 'spc-outlook')); diff --git a/server/scripts/modules/travelforecast.mjs b/server/scripts/modules/travelforecast.mjs index d5b4f02..0a31869 100644 --- a/server/scripts/modules/travelforecast.mjs +++ b/server/scripts/modules/travelforecast.mjs @@ -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)); diff --git a/server/styles/scss/_current-weather.scss b/server/styles/scss/_current-weather.scss index 4606de3..cdc722d 100644 --- a/server/styles/scss/_current-weather.scss +++ b/server/styles/scss/_current-weather.scss @@ -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 { diff --git a/src/personal-weather.mjs b/src/personal-weather.mjs new file mode 100644 index 0000000..4224d31 --- /dev/null +++ b/src/personal-weather.mjs @@ -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; \ No newline at end of file diff --git a/views/index.ejs b/views/index.ejs index c7b670d..0481927 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -63,6 +63,9 @@ + <% if (!DISABLE_PERSONAL) { %> + + <% } %> <% } %> @@ -109,6 +112,11 @@
<%- include('partials/current-weather.ejs') %>
+ <% if (!DISABLE_PERSONAL) { %> +
+ <%- include('partials/personal-weather.ejs') %> +
+ <% } %>
<%- include('partials/local-forecast.ejs') %>
diff --git a/views/partials/personal-weather.ejs b/views/partials/personal-weather.ejs new file mode 100644 index 0000000..a32341e --- /dev/null +++ b/views/partials/personal-weather.ejs @@ -0,0 +1,40 @@ +<%- include('header.ejs', {titleDual:{ top: 'Personal' , bottom: 'Weather Station' }, noaaLogo: false, hasTime: true}) %> +
+
+
+
+
+
Wind:
+
+
+
+
+
+
+
+
Humidity:
+
+
+
+
Dewpoint:
+
+
+
+
Ceiling:
+
+
+
+
Visibility:
+
+
+
+
Pressure:
+
+
+
+
+
+
+
+
+
\ No newline at end of file