mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-18 09:39:30 -07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13f08b62cc | ||
|
|
eacd82b4f4 | ||
|
|
91f669e828 | ||
|
|
cdbe3d968f | ||
|
|
0a388cdb83 | ||
|
|
8678d9f053 | ||
|
|
a592668d0d | ||
|
|
b3faf95e39 | ||
|
|
3a304d7c08 |
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@@ -15,6 +15,9 @@
|
|||||||
"**/*.min.js",
|
"**/*.min.js",
|
||||||
"**/vendor/**"
|
"**/vendor/**"
|
||||||
],
|
],
|
||||||
|
"runtimeArgs": [
|
||||||
|
"--autoplay-policy=no-user-gesture-required"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Data:stations",
|
"name": "Data:stations",
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ Thanks to the WeatherStar community for providing these discussions to further e
|
|||||||
|
|
||||||
* [Stream as FFMPEG](https://github.com/netbymatt/ws4kp/issues/37#issuecomment-2008491948)
|
* [Stream as FFMPEG](https://github.com/netbymatt/ws4kp/issues/37#issuecomment-2008491948)
|
||||||
* [Weather like it's 1999](https://blog.scottlabs.io/2024/02/weather-like-its-1999/) Raspberry pi, streaming, music and CRT all combined into a complete solution.
|
* [Weather like it's 1999](https://blog.scottlabs.io/2024/02/weather-like-its-1999/) Raspberry pi, streaming, music and CRT all combined into a complete solution.
|
||||||
|
* [ws4channels](https://github.com/rice9797/ws4channels) A Dockerized Node.js application to stream WeatherStar 4000 data into Channels DVR using Puppeteer and FFmpeg.
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
A hook is provided as `/server/scripts/custom.js` to allow customizations to your own fork of this project, without accidentally pushing your customizations back upstream to the git repository. An sample file is provided at `/server/scripts/custom.sample.js` and should be renamed to `custom.js` activate it.
|
A hook is provided as `/server/scripts/custom.js` to allow customizations to your own fork of this project, without accidentally pushing your customizations back upstream to the git repository. An sample file is provided at `/server/scripts/custom.sample.js` and should be renamed to `custom.js` activate it.
|
||||||
|
|||||||
@@ -1150,7 +1150,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"city": "Tucsan",
|
"city": "Tucson",
|
||||||
"lat": 32.2216,
|
"lat": 32.2216,
|
||||||
"lon": -110.9698,
|
"lon": -110.9698,
|
||||||
"point": {
|
"point": {
|
||||||
|
|||||||
@@ -575,7 +575,7 @@
|
|||||||
"lon": -77.9447
|
"lon": -77.9447
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"city": "Tucsan",
|
"city": "Tucson",
|
||||||
"lat": 32.2216,
|
"lat": 32.2216,
|
||||||
"lon": -110.9698
|
"lon": -110.9698
|
||||||
}
|
}
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "5.16.2",
|
"version": "5.16.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "5.16.2",
|
"version": "5.16.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ejs": "^3.1.5",
|
"ejs": "^3.1.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "5.16.2",
|
"version": "5.16.5",
|
||||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1151,7 +1151,7 @@ const RegionalCities = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"city": "Tucsan",
|
"city": "Tucson",
|
||||||
"lat": 32.2216,
|
"lat": 32.2216,
|
||||||
"lon": -110.9698,
|
"lon": -110.9698,
|
||||||
"point": {
|
"point": {
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ class CurrentWeather extends WeatherDisplay {
|
|||||||
// make data available outside this class
|
// make data available outside this class
|
||||||
// promise allows for data to be requested before it is available
|
// promise allows for data to be requested before it is available
|
||||||
async getCurrentWeather(stillWaiting) {
|
async getCurrentWeather(stillWaiting) {
|
||||||
|
// an external caller has requested data, set up auto reload
|
||||||
|
this.setAutoReload();
|
||||||
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
|
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (this.data) resolve(this.data);
|
if (this.data) resolve(this.data);
|
||||||
|
|||||||
@@ -76,9 +76,9 @@ class Hourly extends WeatherDisplay {
|
|||||||
const temperature = data.temperature.toString().padStart(3);
|
const temperature = data.temperature.toString().padStart(3);
|
||||||
const feelsLike = data.apparentTemperature.toString().padStart(3);
|
const feelsLike = data.apparentTemperature.toString().padStart(3);
|
||||||
fillValues.temp = temperature;
|
fillValues.temp = temperature;
|
||||||
// only plot apparent temperature if there is a difference
|
|
||||||
// if (temperature !== feelsLike) line.querySelector('.like').innerHTML = feelsLike;
|
// apparent temperature is color coded if different from actual temperature (after fill is applied)
|
||||||
if (temperature !== feelsLike) fillValues.like = feelsLike;
|
fillValues.like = feelsLike;
|
||||||
|
|
||||||
// wind
|
// wind
|
||||||
let wind = 'Calm';
|
let wind = 'Calm';
|
||||||
@@ -91,7 +91,17 @@ class Hourly extends WeatherDisplay {
|
|||||||
// image
|
// image
|
||||||
fillValues.icon = { type: 'img', src: data.icon };
|
fillValues.icon = { type: 'img', src: data.icon };
|
||||||
|
|
||||||
return this.fillTemplate('hourly-row', fillValues);
|
const filledRow = this.fillTemplate('hourly-row', fillValues);
|
||||||
|
|
||||||
|
// alter the color of the feels like column to reflect wind chill or heat index
|
||||||
|
if (feelsLike < temperature) {
|
||||||
|
filledRow.querySelector('.like').classList.add('wind-chill');
|
||||||
|
}
|
||||||
|
if (feelsLike > temperature) {
|
||||||
|
filledRow.querySelector('.like').classList.add('heat-index');
|
||||||
|
}
|
||||||
|
|
||||||
|
return filledRow;
|
||||||
});
|
});
|
||||||
|
|
||||||
list.append(...lines);
|
list.append(...lines);
|
||||||
@@ -129,6 +139,8 @@ class Hourly extends WeatherDisplay {
|
|||||||
// promise allows for data to be requested before it is available
|
// promise allows for data to be requested before it is available
|
||||||
async getCurrentData(stillWaiting) {
|
async getCurrentData(stillWaiting) {
|
||||||
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
|
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
|
||||||
|
// an external caller has requested data, set up auto reload
|
||||||
|
this.setAutoReload();
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (this.data) resolve(this.data);
|
if (this.data) resolve(this.data);
|
||||||
// data not available, put it into the data callback queue
|
// data not available, put it into the data callback queue
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ let playlist;
|
|||||||
let currentTrack = 0;
|
let currentTrack = 0;
|
||||||
let player;
|
let player;
|
||||||
|
|
||||||
const mediaPlaying = new Setting('mediaPlaying', 'Media Playing', 'boolean', false, null, true);
|
const mediaPlaying = new Setting('mediaPlaying', {
|
||||||
|
name: 'Media Playing',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
sticky: true,
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// add the event handler to the page
|
// add the event handler to the page
|
||||||
|
|||||||
@@ -8,27 +8,54 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const settings = { speed: { value: 1.0 } };
|
const settings = { speed: { value: 1.0 } };
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
// create settings
|
// create settings see setting.mjs for defaults
|
||||||
settings.wide = new Setting('wide', 'Widescreen', 'checkbox', false, wideScreenChange, true);
|
settings.wide = new Setting('wide', {
|
||||||
settings.kiosk = new Setting('kiosk', 'Kiosk', 'checkbox', false, kioskChange, false);
|
name: 'Widescreen',
|
||||||
settings.speed = new Setting('speed', 'Speed', 'select', 1.0, null, true, [
|
defaultValue: false,
|
||||||
[0.5, 'Very Fast'],
|
changeAction: wideScreenChange,
|
||||||
[0.75, 'Fast'],
|
sticky: true,
|
||||||
[1.0, 'Normal'],
|
});
|
||||||
[1.25, 'Slow'],
|
settings.kiosk = new Setting('kiosk', {
|
||||||
[1.5, 'Very Slow'],
|
name: 'Kiosk',
|
||||||
]);
|
defaultValue: false,
|
||||||
settings.units = new Setting('units', 'Units', 'select', 'us', unitChange, true, [
|
changeAction: kioskChange,
|
||||||
['us', 'US'],
|
sticky: false,
|
||||||
['si', 'Metric'],
|
});
|
||||||
]);
|
settings.speed = new Setting('speed', {
|
||||||
settings.refreshTime = new Setting('refreshTime', 'Refresh Time', 'select', 30_000, null, false, [
|
name: 'Speed',
|
||||||
[30_000, 'TESTING'],
|
type: 'select',
|
||||||
[300_000, '5 minutes'],
|
defaultValue: 1.0,
|
||||||
[600_000, '10 minutes'],
|
values: [
|
||||||
[900_000, '15 minutes'],
|
[0.5, 'Very Fast'],
|
||||||
[1_800_000, '30 minutes'],
|
[0.75, 'Fast'],
|
||||||
]);
|
[1.0, 'Normal'],
|
||||||
|
[1.25, 'Slow'],
|
||||||
|
[1.5, 'Very Slow'],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
settings.units = new Setting('units', {
|
||||||
|
name: 'Units',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'us',
|
||||||
|
changeAction: unitChange,
|
||||||
|
values: [
|
||||||
|
['us', 'US'],
|
||||||
|
['si', 'Metric'],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
settings.refreshTime = new Setting('refreshTime', {
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 600_000,
|
||||||
|
sticky: false,
|
||||||
|
values: [
|
||||||
|
[30_000, 'TESTING'],
|
||||||
|
[300_000, '5 minutes'],
|
||||||
|
[600_000, '10 minutes'],
|
||||||
|
[900_000, '15 minutes'],
|
||||||
|
[1_800_000, '30 minutes'],
|
||||||
|
],
|
||||||
|
visible: false,
|
||||||
|
});
|
||||||
|
|
||||||
// generate html objects
|
// generate html objects
|
||||||
const settingHtml = Object.values(settings).map((d) => d.generate());
|
const settingHtml = Object.values(settings).map((d) => d.generate());
|
||||||
|
|||||||
@@ -2,41 +2,58 @@ import { parseQueryString } from '../share.mjs';
|
|||||||
|
|
||||||
const SETTINGS_KEY = 'Settings';
|
const SETTINGS_KEY = 'Settings';
|
||||||
|
|
||||||
|
const DEFAULTS = {
|
||||||
|
shortName: undefined,
|
||||||
|
name: undefined,
|
||||||
|
type: 'checkbox',
|
||||||
|
defaultValue: undefined,
|
||||||
|
changeAction: () => { },
|
||||||
|
sticky: true,
|
||||||
|
values: [],
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
class Setting {
|
class Setting {
|
||||||
constructor(shortName, name, type, defaultValue, changeAction, sticky, values) {
|
constructor(shortName, _options) {
|
||||||
// store values
|
if (shortName === undefined) {
|
||||||
|
throw new Error('No name provided for setting');
|
||||||
|
}
|
||||||
|
// merge options with defaults
|
||||||
|
const options = { ...DEFAULTS, ...(_options ?? {}) };
|
||||||
|
|
||||||
|
// store values and combine with defaults
|
||||||
this.shortName = shortName;
|
this.shortName = shortName;
|
||||||
this.name = name;
|
this.name = options.name ?? shortName;
|
||||||
this.defaultValue = defaultValue;
|
this.defaultValue = options.defaultValue;
|
||||||
this.myValue = defaultValue;
|
this.myValue = this.defaultValue;
|
||||||
this.type = type ?? 'checkbox';
|
this.type = options?.type;
|
||||||
this.sticky = sticky;
|
this.sticky = options.sticky;
|
||||||
this.values = values;
|
this.values = options.values;
|
||||||
// a default blank change function is provided
|
this.visible = options.visible;
|
||||||
this.changeAction = changeAction ?? (() => { });
|
this.changeAction = options.changeAction;
|
||||||
|
|
||||||
// get value from url
|
// get value from url
|
||||||
const urlValue = parseQueryString()?.[`settings-${shortName}-${type}`];
|
const urlValue = parseQueryString()?.[`settings-${shortName}-${this.type}`];
|
||||||
let urlState;
|
let urlState;
|
||||||
if (type === 'checkbox' && urlValue !== undefined) {
|
if (this.type === 'checkbox' && urlValue !== undefined) {
|
||||||
urlState = urlValue === 'true';
|
urlState = urlValue === 'true';
|
||||||
}
|
}
|
||||||
if (type === 'select' && urlValue !== undefined) {
|
if (this.type === 'select' && urlValue !== undefined) {
|
||||||
urlState = parseFloat(urlValue);
|
urlState = parseFloat(urlValue);
|
||||||
}
|
}
|
||||||
if (type === 'select' && urlValue !== undefined && Number.isNaN(urlState)) {
|
if (this.type === 'select' && urlValue !== undefined && Number.isNaN(urlState)) {
|
||||||
// couldn't parse as a float, store as a string
|
// couldn't parse as a float, store as a string
|
||||||
urlState = urlValue;
|
urlState = urlValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get existing value if present
|
// get existing value if present
|
||||||
const storedValue = urlState ?? this.getFromLocalStorage();
|
const storedValue = urlState ?? this.getFromLocalStorage();
|
||||||
if (sticky && storedValue !== null) {
|
if ((this.sticky || urlValue !== undefined) && storedValue !== null) {
|
||||||
this.myValue = storedValue;
|
this.myValue = storedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// call the change function on startup
|
// call the change function on startup
|
||||||
switch (type) {
|
switch (this.type) {
|
||||||
case 'select':
|
case 'select':
|
||||||
this.selectChange({ target: { value: this.myValue } });
|
this.selectChange({ target: { value: this.myValue } });
|
||||||
break;
|
break;
|
||||||
@@ -142,6 +159,7 @@ class Setting {
|
|||||||
if (storedValue !== undefined) {
|
if (storedValue !== undefined) {
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
|
case 'checkbox':
|
||||||
return storedValue;
|
return storedValue;
|
||||||
case 'select':
|
case 'select':
|
||||||
return storedValue;
|
return storedValue;
|
||||||
@@ -181,12 +199,15 @@ class Setting {
|
|||||||
|
|
||||||
selectHighlight(newValue) {
|
selectHighlight(newValue) {
|
||||||
// set the dropdown to the provided value
|
// set the dropdown to the provided value
|
||||||
this.element.querySelectorAll('option').forEach((elem) => {
|
this?.element?.querySelectorAll('option')?.forEach?.((elem) => {
|
||||||
elem.selected = (newValue?.toFixed?.(2) === elem.value) || (newValue === elem.value);
|
elem.selected = (newValue?.toFixed?.(2) === elem.value) || (newValue === elem.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
generate() {
|
generate() {
|
||||||
|
// don't generate a control for not visible items
|
||||||
|
if (!this.visible) return '';
|
||||||
|
// call the appropriate control generator
|
||||||
switch (this.type) {
|
switch (this.type) {
|
||||||
case 'select':
|
case 'select':
|
||||||
return this.generateSelect();
|
return this.generateSelect();
|
||||||
|
|||||||
@@ -134,10 +134,9 @@ class WeatherDisplay {
|
|||||||
// refresh doesn't delete existing data, and is reused if the silent refresh fails
|
// refresh doesn't delete existing data, and is reused if the silent refresh fails
|
||||||
if (!refresh) {
|
if (!refresh) {
|
||||||
this.data = undefined;
|
this.data = undefined;
|
||||||
|
// clear any refresh timers
|
||||||
|
this.clearAutoReload();
|
||||||
}
|
}
|
||||||
// clear any refresh timers
|
|
||||||
clearTimeout(this.autoRefreshHandle);
|
|
||||||
this.autoRefreshHandle = null;
|
|
||||||
|
|
||||||
// store weatherParameters locally in case we need them later
|
// store weatherParameters locally in case we need them later
|
||||||
if (weatherParameters) this.weatherParameters = weatherParameters;
|
if (weatherParameters) this.weatherParameters = weatherParameters;
|
||||||
@@ -150,8 +149,8 @@ class WeatherDisplay {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up auto reload
|
// set up auto reload if necessary
|
||||||
this.autoRefreshHandle = setTimeout(() => this.getData(false, true), settings.refreshTime.value);
|
this.setAutoReload();
|
||||||
|
|
||||||
// recalculate navigation timing (in case it was modified in the constructor)
|
// recalculate navigation timing (in case it was modified in the constructor)
|
||||||
this.calcNavTiming();
|
this.calcNavTiming();
|
||||||
@@ -435,6 +434,15 @@ class WeatherDisplay {
|
|||||||
this.stillWaitingCallbacks.forEach((callback) => callback());
|
this.stillWaitingCallbacks.forEach((callback) => callback());
|
||||||
this.stillWaitingCallbacks = [];
|
this.stillWaitingCallbacks = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearAutoReload() {
|
||||||
|
clearInterval(this.autoRefreshHandle);
|
||||||
|
this.autoRefreshHandle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAutoReload() {
|
||||||
|
this.autoRefreshHandle = this.autoRefreshHandle ?? setInterval(() => this.getData(false, true), settings.refreshTime.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WeatherDisplay;
|
export default WeatherDisplay;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -82,6 +82,14 @@
|
|||||||
|
|
||||||
.like {
|
.like {
|
||||||
left: 425px;
|
left: 425px;
|
||||||
|
|
||||||
|
&.heat-index {
|
||||||
|
color: #e00;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.wind-chill {
|
||||||
|
color: c.$extended-low;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wind {
|
.wind {
|
||||||
|
|||||||
Reference in New Issue
Block a user