Compare commits

...

17 Commits

Author SHA1 Message Date
Matt Walsh
36b8adc019 5.23.4 2025-06-12 09:30:54 -05:00
Matt Walsh
bfe0b4757d Merge branch 'main' of github.com:netbymatt/ws4kp 2025-06-12 09:30:38 -05:00
Matt Walsh
2b61e55783 get current conditions direct from api 2025-06-12 09:30:31 -05:00
Matt Walsh
36c4f451b3 Merge pull request #107 from rmitchellscott/fix-push-on-tag
chore(docker): fix pushing on tag
2025-06-09 15:12:38 -05:00
Mitchell Scott
268d4ae7fa chore(docker): fix pushing on tag 2025-06-07 11:46:00 -06:00
Matt Walsh
1b49e02cd8 5.23.3 2025-06-06 16:40:14 -05:00
Matt Walsh
9a55a6ec39 fix setting boolean-style query string parsing close #106 2025-06-06 16:40:08 -05:00
Matt Walsh
faaf6f770f 5.23.2 2025-06-06 16:31:13 -05:00
Matt Walsh
79e4ed6e8b clean up star4000 large font baseline differences between linux/win 2025-06-06 16:31:05 -05:00
Matt Walsh
f956df1272 5.23.1 2025-06-05 23:34:47 -05:00
Matt Walsh
089ef56b10 add event type to hazard scroll #92 2025-06-05 23:34:25 -05:00
Matt Walsh
c4e8721a2b 5.23.0 2025-06-05 22:02:07 -05:00
Matt Walsh
a2efc2f767 Merge branch 'hazard-scroll-2' #92 2025-06-05 22:01:23 -05:00
Matt Walsh
c0e1c55453 clean up location switching 2025-06-05 21:57:06 -05:00
Matt Walsh
7c50f5f1d7 issues changing locations 2025-06-02 15:57:58 -05:00
Matt Walsh
4bf3f4d1e0 scroll triggers properly on red background 2025-06-02 14:48:53 -05:00
Matt Walsh
46da573715 hazard scroll working, needs styling #92 2025-06-01 23:25:07 -05:00
17 changed files with 186 additions and 30 deletions

View File

@@ -49,7 +49,7 @@ jobs:
with: with:
context: . context: .
pull: true pull: true
push: ${{ github.ref == 'refs/heads/main' }} push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }}
platforms: linux/amd64,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@@ -139,6 +139,11 @@ Ws4kp is muted by default, but if it was unmuted on the last visit it is coded t
Chrome seems to be more lenient on auto play and will eventually let a site auto-play music if you're visited it enough recently and manually clicked to start playing music on each visit. It also has a flag you can add to the command line when launching Chrome: `chrome.exe --autoplay-policy=no-user-gesture-required`. This is the best solution when using Kiosk-style setup. Chrome seems to be more lenient on auto play and will eventually let a site auto-play music if you're visited it enough recently and manually clicked to start playing music on each visit. It also has a flag you can add to the command line when launching Chrome: `chrome.exe --autoplay-policy=no-user-gesture-required`. This is the best solution when using Kiosk-style setup.
If you're unable to pre-set the play state before entering kiosk mode (such as with a home dashboard implemenation) you can add the query string value below to the url. The browser will still follow the auto play rules outlined above.
```
?settings-mediaPlaying-boolean=true
```
## Community Notes ## Community Notes
Thanks to the WeatherStar community for providing these discussions to further extend your retro forecasts! Thanks to the WeatherStar community for providing these discussions to further extend your retro forecasts!

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "ws4kp", "name": "ws4kp",
"version": "5.22.0", "version": "5.23.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ws4kp", "name": "ws4kp",
"version": "5.22.0", "version": "5.23.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dotenv": "^16.5.0", "dotenv": "^16.5.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "ws4kp", "name": "ws4kp",
"version": "5.22.0", "version": "5.23.4",
"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",

View File

@@ -42,7 +42,6 @@ class CurrentWeather extends WeatherDisplay {
// station observations // station observations
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
observations = await json(`${station.id}/observations`, { observations = await json(`${station.id}/observations`, {
cors: true,
data: { data: {
limit: 2, limit: 2,
}, },

View File

@@ -2,6 +2,7 @@ import { locationCleanup } from './utils/string.mjs';
import { elemForEach } from './utils/elem.mjs'; import { elemForEach } from './utils/elem.mjs';
import getCurrentWeather from './currentweather.mjs'; import getCurrentWeather from './currentweather.mjs';
import { currentDisplay } from './navigation.mjs'; import { currentDisplay } from './navigation.mjs';
import getHazards from './hazards.mjs';
// constants // constants
const degree = String.fromCharCode(176); const degree = String.fromCharCode(176);
@@ -13,12 +14,18 @@ let interval;
let screenIndex = 0; let screenIndex = 0;
let sinceLastUpdate = 0; let sinceLastUpdate = 0;
let nextUpdate = DEFAULT_UPDATE; let nextUpdate = DEFAULT_UPDATE;
let resetFlag;
// start drawing conditions // start drawing conditions
// reset starts from the first item in the text scroll list // reset starts from the first item in the text scroll list
const start = () => { const start = () => {
// store see if the context is new // if already started, draw the screen on a reset flag and return
if (interval) {
if (resetFlag) drawScreen();
resetFlag = false;
return;
}
resetFlag = false;
// set up the interval if needed // set up the interval if needed
if (!interval) { if (!interval) {
interval = setInterval(incrementInterval, 500); interval = setInterval(incrementInterval, 500);
@@ -29,7 +36,10 @@ const start = () => {
}; };
const stop = (reset) => { const stop = (reset) => {
if (reset) screenIndex = 0; if (reset) {
screenIndex = 0;
resetFlag = true;
}
}; };
// increment interval, roll over // increment interval, roll over
@@ -51,6 +61,7 @@ const incrementInterval = (force) => {
return; return;
} }
screenIndex = (screenIndex + 1) % (lastScreen); screenIndex = (screenIndex + 1) % (lastScreen);
// draw new text // draw new text
drawScreen(); drawScreen();
}; };
@@ -59,10 +70,24 @@ const drawScreen = async () => {
// get the conditions // get the conditions
const data = await getCurrentWeather(); const data = await getCurrentWeather();
// add the hazards if on screen 0
if (screenIndex === 0) {
data.hazards = await getHazards(() => this.stillWaiting());
}
// nothing to do if there's no data yet // nothing to do if there's no data yet
if (!data) return; if (!data) return;
const thisScreen = screens[screenIndex](data); const thisScreen = screens[screenIndex](data);
// update classes on the scroll area
elemForEach('.weather-display .scroll', (elem) => {
elem.classList.forEach((cls) => { if (cls !== 'scroll') elem.classList.remove(cls); });
// no scroll on progress
if (elem.parentElement.id === 'progress-html') return;
thisScreen?.classes?.forEach((cls) => elem.classList.add(cls));
});
if (typeof thisScreen === 'string') { if (typeof thisScreen === 'string') {
// only a string // only a string
drawCondition(thisScreen); drawCondition(thisScreen);
@@ -74,14 +99,36 @@ const drawScreen = async () => {
break; break;
default: drawCondition(thisScreen); default: drawCondition(thisScreen);
} }
// add the header if available
if (thisScreen.header) {
setHeader(thisScreen.header);
} else {
setHeader('');
}
} else { } else {
// can't identify screen, get another one // can't identify screen, get another one
incrementInterval(true); incrementInterval(true);
} }
}; };
const hazards = (data) => {
// test for data
if (!data.hazards || data.hazards.length === 0) return false;
const hazard = `${data.hazards[0].properties.event} ${data.hazards[0].properties.description}`;
return {
text: hazard,
type: 'scroll',
classes: ['hazard'],
header: data.hazards[0].properties.event,
};
};
// the "screens" are stored in an array for easy addition and removal // the "screens" are stored in an array for easy addition and removal
const screens = [ const screens = [
// hazards
hazards,
// station name // station name
(data) => `Conditions at ${locationCleanup(data.station.properties.name).substr(0, 20)}`, (data) => `Conditions at ${locationCleanup(data.station.properties.name).substr(0, 20)}`,
@@ -127,10 +174,14 @@ const drawCondition = (text) => {
elemForEach('.weather-display .scroll .fixed', (elem) => { elemForEach('.weather-display .scroll .fixed', (elem) => {
elem.innerHTML = text; elem.innerHTML = text;
}); });
setHeader('');
};
const setHeader = (text) => {
elemForEach('.weather-display .scroll .scroll-header', (elem) => {
elem.innerHTML = text ?? '';
});
}; };
document.addEventListener('DOMContentLoaded', () => {
start();
});
// store the original number of screens // store the original number of screens
const originalScreens = screens.length; const originalScreens = screens.length;
@@ -180,7 +231,24 @@ const drawScrollCondition = (screen) => {
}, 1000); }, 1000);
}; };
const parseMessage = (event) => {
if (event?.data?.type === 'current-weather-scroll') {
if (event.data?.method === 'start') start();
if (event.data?.method === 'reload') stop(true);
}
};
// add event listener for start message
window.addEventListener('message', parseMessage);
window.CurrentWeatherScroll = { window.CurrentWeatherScroll = {
addScreen, addScreen,
reset, reset,
start,
};
export {
addScreen,
reset,
start,
}; };

View File

@@ -21,9 +21,17 @@ class Hazards extends WeatherDisplay {
// special height and width for scrolling // special height and width for scrolling
super(navId, elemId, 'Hazards', defaultActive); super(navId, elemId, 'Hazards', defaultActive);
this.showOnProgress = false; this.showOnProgress = false;
this.okToDrawCurrentConditions = false;
// force a 1-minute refresh time for the most up-to-date hazards
this.refreshTime = 60_000;
// 0 screens skips this during "play" // 0 screens skips this during "play"
this.timing.totalScreens = 0; this.timing.totalScreens = 0;
// take note of the already-shown alert ids
this.viewedAlerts = new Set();
this.viewedGetCount = 0;
} }
async getData(weatherParameters, refresh) { async getData(weatherParameters, refresh) {
@@ -32,9 +40,18 @@ class Hazards extends WeatherDisplay {
// hazards performs a silent refresh, but does not fall back to a previous fetch if no data is available // hazards performs a silent refresh, but does not fall back to a previous fetch if no data is available
// this is intentional to ensure the latest alerts only are displayed. // this is intentional to ensure the latest alerts only are displayed.
// auto reload must be set up specifically for hazards in case it is disabled via checkbox (for the bottom line scroll)
if (this.autoRefreshHandle === null) this.setAutoReload();
const alert = this.checkbox.querySelector('.alert'); const alert = this.checkbox.querySelector('.alert');
alert.classList.remove('show'); alert.classList.remove('show');
// if not a refresh (new site), all alerts are new
if (!refresh) {
this.viewedGetCount = 0;
this.viewedAlerts.clear();
}
try { try {
// get the forecast // get the forecast
const url = new URL('https://api.weather.gov/alerts/active'); const url = new URL('https://api.weather.gov/alerts/active');
@@ -47,8 +64,23 @@ class Hazards extends WeatherDisplay {
const filteredAlerts = sortedAlerts.filter((hazard) => hazard.properties.severity !== 'Unknown' && (!hasImmediate || (hazard.properties.urgency === 'Immediate'))); const filteredAlerts = sortedAlerts.filter((hazard) => hazard.properties.severity !== 'Unknown' && (!hasImmediate || (hazard.properties.urgency === 'Immediate')));
this.data = filteredAlerts; this.data = filteredAlerts;
// every 10 times through the get process (10 minutes), reset the viewed messages
if (this.viewedGetCount >= 10) {
this.viewedGetCount = 0;
this.viewedAlerts.clear();
}
this.viewedGetCount += 1;
// count up un-viewed alerts
const unViewed = this.data.reduce((count, hazard) => {
if (!this.viewedAlerts.has(hazard.id)) return count + 1;
return count;
}, 0);
// show alert indicator // show alert indicator
if (this.data.length > 0) alert.classList.add('show'); if (unViewed > 0) alert.classList.add('show');
// draw the canvas to calculate the new timings and activate hazards in the slide deck again
this.drawLongCanvas();
} catch (error) { } catch (error) {
console.error('Get hazards failed'); console.error('Get hazards failed');
console.error(error.status, error.responseJSON); console.error(error.status, error.responseJSON);
@@ -72,7 +104,10 @@ class Hazards extends WeatherDisplay {
const list = this.elem.querySelector('.hazard-lines'); const list = this.elem.querySelector('.hazard-lines');
list.innerHTML = ''; list.innerHTML = '';
const lines = this.data.map((data) => { // filter viewed alerts
const unViewed = this.data.filter((data) => !this.viewedAlerts.has(data.id));
const lines = unViewed.map((data) => {
const fillValues = {}; const fillValues = {};
// text // text
fillValues['hazard-text'] = `${data.properties.event}<br/><br/>${data.properties.description.replaceAll('\n\n', '<br/><br/>').replaceAll('\n', ' ')}`; fillValues['hazard-text'] = `${data.properties.event}<br/><br/>${data.properties.description.replaceAll('\n\n', '<br/><br/>').replaceAll('\n', ' ')}`;
@@ -91,18 +126,22 @@ class Hazards extends WeatherDisplay {
} }
// update timing // update timing
this.setTiming(list);
this.setStatus(STATUS.loaded);
}
setTiming(list) {
// set up the timing // set up the timing
this.timing.baseDelay = 20; this.timing.baseDelay = 20;
// 24 hours = 6 pages // 24 hours = 6 pages
const pages = Math.max(Math.ceil(list.scrollHeight / 400) - 3, 1); const pages = Math.max(Math.ceil(list.scrollHeight / 480) - 4);
const timingStep = 400; const timingStep = 480;
this.timing.delay = [150 + timingStep]; this.timing.delay = [150 + timingStep];
// add additional pages // add additional pages
for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep); for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep);
// add the final 3 second delay // add the final 3 second delay
this.timing.delay.push(250); this.timing.delay.push(250);
this.calcNavTiming(); this.calcNavTiming();
this.setStatus(STATUS.loaded);
} }
drawCanvas() { drawCanvas() {
@@ -151,10 +190,23 @@ class Hazards extends WeatherDisplay {
if (superValue === false) { if (superValue === false) {
// set total screens to zero to take this out of the rotation // set total screens to zero to take this out of the rotation
this.timing.totalScreens = 0; this.timing.totalScreens = 0;
// note the ids shown
this?.data?.forEach((alert) => this.viewedAlerts.add(alert.id));
} }
// return the value as expected // return the value as expected
return superValue; return superValue;
} }
// make data available outside this class
// promise allows for data to be requested before it is available
async getHazards(stillWaiting) {
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));
});
}
} }
const calcSeverity = (severity, event) => { const calcSeverity = (severity, event) => {
@@ -165,4 +217,7 @@ const calcSeverity = (severity, event) => {
}; };
// register display // register display
registerDisplay(new Hazards(0, 'hazards', true)); const display = new Hazards(0, 'hazards', true);
registerDisplay(display);
export default display.getHazards.bind(display);

View File

@@ -67,6 +67,9 @@ const getWeather = async (latLon, haveDataCallback) => {
// update the main process for display purposes // update the main process for display purposes
populateWeatherParameters(weatherParameters); populateWeatherParameters(weatherParameters);
// reset the scroll
postMessage({ type: 'current-weather-scroll', method: 'reload' });
// draw the progress canvas and hide others // draw the progress canvas and hide others
hideAllCanvases(); hideAllCanvases();
document.querySelector('#loading').style.display = 'none'; document.querySelector('#loading').style.display = 'none';

View File

@@ -38,6 +38,9 @@ class Setting {
if (this.type === 'checkbox' && urlValue !== undefined) { if (this.type === 'checkbox' && urlValue !== undefined) {
urlState = urlValue === 'true'; urlState = urlValue === 'true';
} }
if (this.type === 'boolean' && urlValue !== undefined) {
urlState = urlValue === 'true';
}
if (this.type === 'select' && urlValue !== undefined) { if (this.type === 'select' && urlValue !== undefined) {
urlState = parseFloat(urlValue); urlState = parseFloat(urlValue);
} }

View File

@@ -170,6 +170,7 @@ class WeatherDisplay {
// clean up the first-run flag in screen index // clean up the first-run flag in screen index
if (this.screenIndex < 0) this.screenIndex = 0; if (this.screenIndex < 0) this.screenIndex = 0;
if (this.okToDrawCurrentDateTime) this.drawCurrentDateTime(); if (this.okToDrawCurrentDateTime) this.drawCurrentDateTime();
if (this.okToDrawCurrentConditions) postMessage({ type: 'current-weather-scroll', method: 'start' });
} }
finishDraw() { finishDraw() {
@@ -443,7 +444,9 @@ class WeatherDisplay {
} }
setAutoReload() { setAutoReload() {
this.autoRefreshHandle = this.autoRefreshHandle ?? setInterval(() => this.getData(false, true), settings.refreshTime.value); // refresh time can be forced by the user (for hazards)
const refreshTime = this.refreshTime ?? settings.refreshTime.value;
this.autoRefreshHandle = this.autoRefreshHandle ?? setInterval(() => this.getData(false, true), refreshTime);
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -22,9 +22,10 @@
&.right { &.right {
right: 0px; right: 0px;
font-family: 'Star4000 Large'; font-family: "Star4000 Large";
font-size: 16pt; font-size: 20px;
font-weight: bold; font-weight: bold;
line-height: 24px;
.row { .row {
margin-bottom: 12px; margin-bottom: 12px;

View File

@@ -4,6 +4,7 @@
.weather-display .main.hazards { .weather-display .main.hazards {
&.main { &.main {
overflow-y: hidden; overflow-y: hidden;
height: 480px;
.hazard-lines { .hazard-lines {
min-height: 400px; min-height: 400px;
@@ -18,9 +19,10 @@
@include u.text-shadow(0px); @include u.text-shadow(0px);
position: relative; position: relative;
text-transform: uppercase; text-transform: uppercase;
margin-top: 110px; margin-top: 10px;
margin-left: 80px; margin-left: 80px;
margin-right: 80px; margin-right: 80px;
padding-bottom: 10px;
} }
} }
} }

View File

@@ -113,16 +113,32 @@
.scroll { .scroll {
@include u.text-shadow(3px, 1.5px); @include u.text-shadow(3px, 1.5px);
width: calc(640px - 2 * 30px); width: 640px;
height: 70px; height: 70px;
overflow: hidden; overflow: hidden;
margin-top: 10px; margin-top: 3px;
&.hazard {
background-color: rgb(112, 35, 35);
}
.fixed,
.scroll-header {
margin-left: 55px;
margin-right: 55px;
overflow: hidden;
}
.scroll-header {
height: 26px;
font-family: "Star4000 Small";
font-size: 20pt;
margin-top: -10px;
}
.fixed { .fixed {
font-family: 'Star4000'; font-family: 'Star4000';
font-size: 24pt; font-size: 24pt;
margin-left: 55px;
overflow: hidden;
.scroll-area { .scroll-area {
text-wrap: nowrap; text-wrap: nowrap;
@@ -132,5 +148,6 @@
// left: calc((elem width) - 640px); // left: calc((elem width) - 640px);
} }
} }
} }
} }

View File

@@ -1,8 +1,7 @@
<div class="main has-scroll hazards no-header"> <div class="main hazards no-header">
<div class="hazard-lines"> <div class="hazard-lines">
<div class="hazard template"> <div class="hazard template">
<div class="hazard-text"></div> <div class="hazard-text"></div>
</div> </div>
</div> </div>
</div> </div>
<%- include('scroll.ejs') %>

View File

@@ -1,4 +1,5 @@
<div class="scroll"> <div class="scroll">
<div class="scrolling template"></div> <div class="scrolling template"></div>
<div class="scroll-header"></div>
<div class="fixed"></div> <div class="fixed"></div>
</div> </div>