mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-12-12 01:42:19 +00:00
Release 2.33.0 (#3903)
This commit is contained in:
committed by
GitHub
parent
62b0f7f26e
commit
b0c5924019
@@ -9,7 +9,7 @@
|
||||
font-size: 70%;
|
||||
position: relative;
|
||||
display: table;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
@@ -35,7 +35,7 @@
|
||||
top: 40%;
|
||||
width: 40%;
|
||||
height: auto;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
{% if imageUrl or imageFA %}
|
||||
{% set imageHeight = imageHeight if imageHeight else "80px" %}
|
||||
{% if imageUrl %}
|
||||
<img src="{{ imageUrl }}"
|
||||
height="{{ imageHeight }}"
|
||||
style="margin-bottom: 10px" />
|
||||
{% else %}
|
||||
<span class="bright fas fa-{{ imageFA }}"
|
||||
style="margin-bottom: 10px;
|
||||
font-size: {{ imageHeight }}"></span>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% set imageHeight = imageHeight if imageHeight else "80px" %}
|
||||
{% if imageUrl %}
|
||||
<img src="{{ imageUrl }}" height="{{ imageHeight }}" style="margin-bottom: 10px" />
|
||||
{% else %}
|
||||
<span
|
||||
class="bright fas fa-{{ imageFA }}"
|
||||
style="margin-bottom: 10px;
|
||||
font-size: {{ imageHeight }}"
|
||||
></span>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if title %}
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% if title %}
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
<span class="thin dimmed medium">{{ title if titleType == 'text' else title | safe }}</span>
|
||||
{% endif %}
|
||||
{% if message %}
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% if title %}<br />{% endif %}
|
||||
<span class="light bright small">{{ message if messageType == 'text' else message | safe }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -2,23 +2,14 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding-left: 0;
|
||||
padding-right: 10px;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.calendar .symbol span {
|
||||
padding-top: 4px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.calendar .title {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
vertical-align: top;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.calendar .time {
|
||||
padding-left: 30px;
|
||||
padding-left: 20px;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ Module.register("calendar", {
|
||||
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
||||
pastDaysCount: 0,
|
||||
displaySymbol: true,
|
||||
defaultSymbol: "calendar-alt", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
||||
defaultSymbol: "calendar-days", // Fontawesome Symbol see https://fontawesome.com/search?ic=free&o=r
|
||||
defaultSymbolClassName: "fas fa-fw fa-",
|
||||
showLocation: false,
|
||||
displayRepeatingCountTitle: false,
|
||||
@@ -168,8 +168,8 @@ Module.register("calendar", {
|
||||
|
||||
this.selfUpdate();
|
||||
},
|
||||
notificationReceived (notification, payload, sender) {
|
||||
|
||||
notificationReceived (notification, payload, sender) {
|
||||
if (notification === "FETCH_CALENDAR") {
|
||||
if (this.hasCalendarURL(payload.url)) {
|
||||
this.sendSocketNotification(notification, { url: payload.url, id: this.identifier });
|
||||
@@ -217,7 +217,6 @@ Module.register("calendar", {
|
||||
|
||||
// Override dom generator.
|
||||
getDom () {
|
||||
const ONE_SECOND = 1000; // 1,000 milliseconds
|
||||
const events = this.createEventList(true);
|
||||
const wrapper = document.createElement("table");
|
||||
wrapper.className = this.config.tableClass;
|
||||
@@ -308,15 +307,12 @@ Module.register("calendar", {
|
||||
}
|
||||
|
||||
const symbolClass = this.symbolClassForUrl(event.url);
|
||||
symbolWrapper.className = `symbol align-right ${symbolClass}`;
|
||||
symbolWrapper.className = `symbol ${symbolClass}`;
|
||||
|
||||
const symbols = this.symbolsForEvent(event);
|
||||
symbols.forEach((s, index) => {
|
||||
symbols.forEach((s) => {
|
||||
const symbol = document.createElement("span");
|
||||
symbol.className = s;
|
||||
if (index > 0) {
|
||||
symbol.style.paddingLeft = "5px";
|
||||
}
|
||||
symbolWrapper.appendChild(symbol);
|
||||
});
|
||||
eventWrapper.appendChild(symbolWrapper);
|
||||
@@ -601,7 +597,6 @@ Module.register("calendar", {
|
||||
*/
|
||||
createEventList (limitNumberOfEntries) {
|
||||
let now = moment();
|
||||
let today = now.clone().startOf("day");
|
||||
let future = now.clone().startOf("day").add(this.config.maximumNumberOfDays, "days");
|
||||
|
||||
let events = [];
|
||||
@@ -705,30 +700,24 @@ Module.register("calendar", {
|
||||
* Limit the number of days displayed
|
||||
* If limitDays is set > 0, limit display to that number of days
|
||||
*/
|
||||
if (this.config.limitDays > 0) {
|
||||
let newEvents = [];
|
||||
let lastDate = today.clone().subtract(1, "days");
|
||||
let days = 0;
|
||||
for (const ev of events) {
|
||||
let eventDate = this.timestampToMoment(ev.startDate);
|
||||
if (this.config.limitDays > 0 && events.length > 0) { // watch out for initial display before events arrive from helper
|
||||
// Group all events by date, events on the same date will be in a list with the key being the date.
|
||||
const eventsByDate = Object.groupBy(events, (ev) => this.timestampToMoment(ev.startDate).format("YYYY-MM-DD"));
|
||||
const newEvents = [];
|
||||
let currentDate = moment();
|
||||
let daysCollected = 0;
|
||||
|
||||
/*
|
||||
* if date of event is later than lastdate
|
||||
* check if we already are showing max unique days
|
||||
*/
|
||||
if (eventDate.isAfter(lastDate)) {
|
||||
// if the only entry in the first day is a full day event that day is not counted as unique
|
||||
if (!this.config.limitDaysNeverSkip && newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
|
||||
days--;
|
||||
}
|
||||
days++;
|
||||
if (days > this.config.limitDays) {
|
||||
continue;
|
||||
} else {
|
||||
lastDate = eventDate;
|
||||
}
|
||||
while (daysCollected < this.config.limitDays) {
|
||||
const dateStr = currentDate.format("YYYY-MM-DD");
|
||||
// Check if there are events on the currentDate
|
||||
if (eventsByDate[dateStr] && eventsByDate[dateStr].length > 0) {
|
||||
// If there are any events today then get all those events and select the currently active events and the events that are starting later in the day.
|
||||
newEvents.push(...eventsByDate[dateStr].filter((ev) => this.timestampToMoment(ev.endDate).isAfter(moment())));
|
||||
// Since we found a day with events, increase the daysCollected by 1
|
||||
daysCollected++;
|
||||
}
|
||||
newEvents.push(ev);
|
||||
// Search for the next day
|
||||
currentDate.add(1, "day");
|
||||
}
|
||||
events = newEvents;
|
||||
}
|
||||
@@ -887,7 +876,7 @@ Module.register("calendar", {
|
||||
* @param {string} url The calendar url
|
||||
* @param {string} property The property to look for
|
||||
* @param {string} defaultValue The value if the property is not found
|
||||
* @returns {*} The property
|
||||
* @returns {property} The property
|
||||
*/
|
||||
getCalendarProperty (url, property, defaultValue) {
|
||||
for (const calendar of this.config.calendars) {
|
||||
|
||||
@@ -3,6 +3,8 @@ const ical = require("node-ical");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
const CalendarFetcherUtils = require("./calendarfetcherutils");
|
||||
const { getUserAgent } = require("#server_functions");
|
||||
const { scheduleTimer } = require("#module_functions");
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -29,10 +31,9 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
const fetchCalendar = () => {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = null;
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
let httpsAgent = null;
|
||||
let headers = {
|
||||
"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`
|
||||
"User-Agent": getUserAgent()
|
||||
};
|
||||
|
||||
if (selfSignedCert) {
|
||||
@@ -65,31 +66,18 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
});
|
||||
} catch (error) {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
scheduleTimer(reloadTimer, reloadInterval, fetchCalendar);
|
||||
return;
|
||||
}
|
||||
this.broadcastEvents();
|
||||
scheduleTimer();
|
||||
scheduleTimer(reloadTimer, reloadInterval, fetchCalendar);
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
scheduleTimer(reloadTimer, reloadInterval, fetchCalendar);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedule the timer for the next update.
|
||||
*/
|
||||
const scheduleTimer = function () {
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
// only set timer when not running in jest
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchCalendar();
|
||||
}, reloadInterval);
|
||||
}
|
||||
};
|
||||
|
||||
/* public methods */
|
||||
|
||||
/**
|
||||
@@ -109,7 +97,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
|
||||
/**
|
||||
* Sets the on success callback
|
||||
* @param {Function} callback The on success callback.
|
||||
* @param {eventsReceivedCallback} callback The on success callback.
|
||||
*/
|
||||
this.onReceive = function (callback) {
|
||||
eventsReceivedCallback = callback;
|
||||
@@ -117,7 +105,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
||||
|
||||
/**
|
||||
* Sets the on error callback
|
||||
* @param {Function} callback The on error callback.
|
||||
* @param {fetchFailedCallback} callback The on error callback.
|
||||
*/
|
||||
this.onError = function (callback) {
|
||||
fetchFailedCallback = callback;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
const moment = require("moment-timezone");
|
||||
|
||||
const Log = require("../../../js/logger");
|
||||
const Log = require("logger");
|
||||
|
||||
const CalendarFetcherUtils = {
|
||||
|
||||
@@ -16,7 +16,7 @@ const CalendarFetcherUtils = {
|
||||
* until: the date until the event should be excluded.
|
||||
*/
|
||||
shouldEventBeExcluded (config, title) {
|
||||
let filter = {
|
||||
let result = {
|
||||
excluded: false,
|
||||
until: null
|
||||
};
|
||||
@@ -55,14 +55,14 @@ const CalendarFetcherUtils = {
|
||||
|
||||
if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
|
||||
if (until) {
|
||||
filter.until = until;
|
||||
result.until = until;
|
||||
} else {
|
||||
filter.excluded = true;
|
||||
result.excluded = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return filter;
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
// Alias modules mentioned in package.js under _moduleAliases.
|
||||
require("module-alias/register");
|
||||
const Log = require("logger");
|
||||
|
||||
const CalendarFetcher = require("./calendarfetcher");
|
||||
|
||||
@@ -20,22 +21,22 @@ const auth = {
|
||||
pass: pass
|
||||
};
|
||||
|
||||
console.log("Create fetcher ...");
|
||||
Log.log("Create fetcher ...");
|
||||
|
||||
const fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maximumNumberOfDays, auth);
|
||||
|
||||
fetcher.onReceive(function (fetcher) {
|
||||
console.log(fetcher.events());
|
||||
console.log("------------------------------------------------------------");
|
||||
Log.log(fetcher.events());
|
||||
Log.log("------------------------------------------------------------");
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
fetcher.onError(function (fetcher, error) {
|
||||
console.log("Fetcher error:");
|
||||
console.log(error);
|
||||
Log.log("Fetcher error:");
|
||||
Log.log(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
fetcher.startFetch();
|
||||
|
||||
console.log("Create fetcher done! ");
|
||||
Log.log("Create fetcher done! ");
|
||||
|
||||
@@ -36,7 +36,7 @@ Module.register("clock", {
|
||||
},
|
||||
// Define styles.
|
||||
getStyles () {
|
||||
return ["clock_styles.css"];
|
||||
return ["clock_styles.css", "font-awesome.css"];
|
||||
},
|
||||
// Define start sequence.
|
||||
start () {
|
||||
|
||||
@@ -87,9 +87,17 @@
|
||||
transform-origin: 50% 100%;
|
||||
}
|
||||
|
||||
.module.clock .digital {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.module.clock .sun,
|
||||
.module.clock .moon {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.module.clock .sun > *,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<div>
|
||||
<iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe>
|
||||
<iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe>
|
||||
</div>
|
||||
|
||||
@@ -181,7 +181,7 @@ Module.register("newsfeed", {
|
||||
* Gets a feed property by name
|
||||
* @param {object} feed A feed object.
|
||||
* @param {string} property The name of the property.
|
||||
* @returns {*} The value of the specified property for the feed.
|
||||
* @returns {property} The value of the specified property for the feed.
|
||||
*/
|
||||
getFeedProperty (feed, property) {
|
||||
let res = this.config[property];
|
||||
|
||||
@@ -1,89 +1,89 @@
|
||||
{% macro escapeText(text, dangerouslyDisableAutoEscaping=false) %}
|
||||
{% if dangerouslyDisableAutoEscaping -%}
|
||||
{{ text | safe }}
|
||||
{%- else -%}
|
||||
{{ text }}
|
||||
{%- endif %}
|
||||
{% if dangerouslyDisableAutoEscaping -%}
|
||||
{{ text | safe }}
|
||||
{%- else -%}
|
||||
{{ text }}
|
||||
{%- endif %}
|
||||
{% endmacro %}
|
||||
{% macro escapeTitle(title, url, dangerouslyDisableAutoEscaping=false, showTitleAsUrl=false) %}
|
||||
{% if dangerouslyDisableAutoEscaping %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
{% if dangerouslyDisableAutoEscaping %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a
|
||||
href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
color:#ffffff"
|
||||
target="_blank">{{ title | safe }}</a>
|
||||
{% else %}
|
||||
{{ title | safe }}
|
||||
{% endif %}
|
||||
target="_blank"
|
||||
>{{ title | safe }}</a
|
||||
>
|
||||
{% else %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
color:#ffffff"
|
||||
target="_blank">{{ title }}</a>
|
||||
{% else %}
|
||||
{{ title }}
|
||||
{% endif %}
|
||||
{{ title | safe }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if showTitleAsUrl %}
|
||||
<a
|
||||
href="{{ url }}"
|
||||
style="text-decoration:none;
|
||||
color:#ffffff"
|
||||
target="_blank"
|
||||
>{{ title }}</a
|
||||
>
|
||||
{% else %}
|
||||
{{ title }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% if loaded %}
|
||||
{% if config.showAsList %}
|
||||
<ul class="newsfeed-list">
|
||||
{% for item in items %}
|
||||
<li>
|
||||
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if item.sourceTitle and config.showSourceTitle %}
|
||||
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ item.publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
{{ escapeTitle(item.title, item.url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}
|
||||
</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ escapeText(item.description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div>
|
||||
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if sourceTitle and config.showSourceTitle %}
|
||||
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %}, {% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
|
||||
{{ escapeTitle(title, url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}
|
||||
{% if config.showAsList %}
|
||||
<ul class="newsfeed-list">
|
||||
{% for item in items %}
|
||||
<li>
|
||||
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if item.sourceTitle and config.showSourceTitle %}
|
||||
{{ item.sourceTitle }}{% if config.showPublishDate %},{% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ item.publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ escapeText(description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ escapeText(description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">{{ escapeTitle(item.title, item.url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ escapeText(item.description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ escapeText(item.description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div>
|
||||
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
|
||||
<div class="newsfeed-source light small dimmed">
|
||||
{% if sourceTitle and config.showSourceTitle %}
|
||||
{{ escapeText(sourceTitle, config.dangerouslyDisableAutoEscaping) }}{% if config.showPublishDate %},{% else %}:{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPublishDate %}{{ publishDate }}:{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elseif empty %}
|
||||
<div class="small dimmed">{{ "NEWSFEED_NO_ITEMS" | translate | safe }}</div>
|
||||
{% elseif error %}
|
||||
<div class="small dimmed">
|
||||
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}
|
||||
{% endif %}
|
||||
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">{{ escapeTitle(title, url, config.dangerouslyDisableAutoEscaping, config.showTitleAsUrl) }}</div>
|
||||
{% if config.showDescription %}
|
||||
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
|
||||
{% if config.truncDescription %}
|
||||
{{ escapeText(description | truncate(config.lengthDescription) , config.dangerouslyDisableAutoEscaping) }}
|
||||
{% else %}
|
||||
{{ escapeText(description, config.dangerouslyDisableAutoEscaping) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elseif empty %}
|
||||
<div class="small dimmed">{{ "NEWSFEED_NO_ITEMS" | translate | safe }}</div>
|
||||
{% elseif error %}
|
||||
<div class="small dimmed">{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}</div>
|
||||
{% else %}
|
||||
<div class="small dimmed">{{ "LOADING" | translate | safe }}</div>
|
||||
<div class="small dimmed">{{ "LOADING" | translate | safe }}</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -5,6 +5,8 @@ const iconv = require("iconv-lite");
|
||||
const { htmlToText } = require("html-to-text");
|
||||
const Log = require("logger");
|
||||
const NodeHelper = require("node_helper");
|
||||
const { getUserAgent } = require("#server_functions");
|
||||
const { scheduleTimer } = require("#module_functions");
|
||||
|
||||
/**
|
||||
* Responsible for requesting an update on the set interval and broadcasting the data.
|
||||
@@ -79,12 +81,12 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
|
||||
parser.on("error", (error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
scheduleTimer(reloadTimer, reloadIntervalMS, fetchNews);
|
||||
});
|
||||
|
||||
//"end" event is not broadcast if the feed is empty but "finish" is used for both
|
||||
parser.on("finish", () => {
|
||||
scheduleTimer();
|
||||
scheduleTimer(reloadTimer, reloadIntervalMS, fetchNews);
|
||||
});
|
||||
|
||||
parser.on("ttl", (minutes) => {
|
||||
@@ -100,9 +102,8 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
}
|
||||
});
|
||||
|
||||
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
|
||||
const headers = {
|
||||
"User-Agent": `Mozilla/5.0 (Node.js ${nodeVersion}) MagicMirror/${global.version}`,
|
||||
"User-Agent": getUserAgent(),
|
||||
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache"
|
||||
};
|
||||
@@ -120,23 +121,10 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchFailedCallback(this, error);
|
||||
scheduleTimer();
|
||||
scheduleTimer(reloadTimer, reloadIntervalMS, fetchNews);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedule the timer for the next update.
|
||||
*/
|
||||
const scheduleTimer = function () {
|
||||
if (process.env.JEST_WORKER_ID === undefined) {
|
||||
// only set timer when not running in jest
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = setTimeout(function () {
|
||||
fetchNews();
|
||||
}, reloadIntervalMS);
|
||||
}
|
||||
};
|
||||
|
||||
/* public methods */
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
<div class="small bright">
|
||||
{{ "MODULE_CONFIG_CHANGED" | translate({MODULE_NAME: "Newsfeed"}) | safe }}
|
||||
</div>
|
||||
<div class="small bright">{{ "MODULE_CONFIG_CHANGED" | translate({MODULE_NAME: "Newsfeed"}) | safe }}</div>
|
||||
|
||||
@@ -4,8 +4,6 @@ const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const Log = require("logger");
|
||||
|
||||
const BASE_DIR = path.normalize(`${__dirname}/../../../`);
|
||||
|
||||
class GitHelper {
|
||||
constructor () {
|
||||
this.gitRepos = [];
|
||||
@@ -35,10 +33,10 @@ class GitHelper {
|
||||
}
|
||||
|
||||
async add (moduleName) {
|
||||
let moduleFolder = BASE_DIR;
|
||||
let moduleFolder = `${global.root_path}`;
|
||||
|
||||
if (moduleName !== "MagicMirror") {
|
||||
moduleFolder = `${moduleFolder}modules/${moduleName}`;
|
||||
moduleFolder = `${moduleFolder}/modules/${moduleName}`;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const NodeHelper = require("node_helper");
|
||||
const defaultModules = require("../defaultmodules");
|
||||
|
||||
const defaultModules = require(`${global.root_path}/modules/default/defaultmodules`);
|
||||
const GitHelper = require("./git_helper");
|
||||
const UpdateHelper = require("./update_helper");
|
||||
|
||||
@@ -21,7 +22,7 @@ module.exports = NodeHelper.create({
|
||||
return modules;
|
||||
} else {
|
||||
// get modules from modules-directory
|
||||
const moduleDir = path.normalize(`${__dirname}/../../`);
|
||||
const moduleDir = path.normalize(`${global.root_path}/modules`);
|
||||
const getDirectories = (source) => {
|
||||
return fs.readdirSync(source, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory() && dirent.name !== "default")
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
{% if not suspended %}
|
||||
{% if needRestart %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-rotate"></i>
|
||||
<span>
|
||||
{% set restartTextLabel = "UPDATE_NOTIFICATION_NEED-RESTART" %}
|
||||
{{ restartTextLabel | translate() | safe }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for name, status in moduleList %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>
|
||||
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "MagicMirror" else "UPDATE_NOTIFICATION_MODULE" %}
|
||||
{{ mainTextLabel | translate({MODULE_NAME: name}) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="xsmall dimmed">
|
||||
{% set subTextLabel = "UPDATE_INFO_SINGLE" if status.behind === 1 else "UPDATE_INFO_MULTIPLE" %}
|
||||
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for name, status in updatesList %}
|
||||
<div class="small bright">
|
||||
{% if status.done %}
|
||||
<i class="fas fa-check" style="color: lightgreen;"></i>
|
||||
<span>
|
||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_DONE" %}
|
||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||
</span>
|
||||
{% else %}
|
||||
<i class="fas fa-xmark" style="color: red;"></i>
|
||||
<span>
|
||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_ERROR" %}
|
||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if needRestart %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-rotate"></i>
|
||||
<span>
|
||||
{% set restartTextLabel = "UPDATE_NOTIFICATION_NEED-RESTART" %}
|
||||
{{ restartTextLabel | translate() | safe }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for name, status in moduleList %}
|
||||
<div class="small bright">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<span>
|
||||
{% set mainTextLabel = "UPDATE_NOTIFICATION" if name === "MagicMirror" else "UPDATE_NOTIFICATION_MODULE" %}
|
||||
{{ mainTextLabel | translate({MODULE_NAME: name}) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="xsmall dimmed">
|
||||
{% set subTextLabel = "UPDATE_INFO_SINGLE" if status.behind === 1 else "UPDATE_INFO_MULTIPLE" %}
|
||||
{{ subTextLabel | translate({COMMIT_COUNT: status.behind, BRANCH_NAME: status.current}) | diffLink(status) | safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for name, status in updatesList %}
|
||||
<div class="small bright">
|
||||
{% if status.done %}
|
||||
<i class="fas fa-check" style="color: lightgreen;"></i>
|
||||
<span>
|
||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_DONE" %}
|
||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||
</span>
|
||||
{% else %}
|
||||
<i class="fas fa-xmark" style="color: red;"></i>
|
||||
<span>
|
||||
{% set updateTextLabel = "UPDATE_NOTIFICATION_ERROR" %}
|
||||
{{ updateTextLabel | translate({MODULE_NAME: name}) | safe }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,101 +1,97 @@
|
||||
{% macro humidity() %}
|
||||
{% if current.humidity %}
|
||||
<span class="humidity"><span>{{ current.humidity | decimalSymbol }}</span><sup> <i class="wi wi-humidity humidity-icon"></i></sup></span>
|
||||
{% endif %}
|
||||
{% if current.humidity %}
|
||||
<span class="humidity"
|
||||
><span>{{ current.humidity | decimalSymbol }}</span><sup> <i class="wi wi-humidity humidity-icon"></i></sup
|
||||
></span>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% if current %}
|
||||
{% if not config.onlyTemp %}
|
||||
<div class="normal medium">
|
||||
<span class="wi wi-strong-wind dimmed"></span>
|
||||
<span>
|
||||
{{ current.windSpeed | unit("wind") | round }}
|
||||
{% if config.showWindDirection %}
|
||||
<sup>
|
||||
{% if config.showWindDirectionAsArrow %}
|
||||
<i class="fas fa-long-arrow-alt-down" style="transform:rotate({{ current.windFromDirection }}deg)"></i>
|
||||
{% else %}
|
||||
{{ current.cardinalWindDirection() | translate }}
|
||||
{% endif %}
|
||||
|
||||
</sup>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if config.showHumidity === "wind" %}
|
||||
{{ humidity() }}
|
||||
{% if not config.onlyTemp %}
|
||||
<div class="normal medium">
|
||||
<span class="wi wi-strong-wind dimmed"></span>
|
||||
<span>
|
||||
{{ current.windSpeed | unit("wind") | round }}
|
||||
{% if config.showWindDirection %}
|
||||
<sup>
|
||||
{% if config.showWindDirectionAsArrow %}
|
||||
<i class="fas fa-long-arrow-alt-down" style="transform:rotate({{ current.windFromDirection }}deg)"></i>
|
||||
{% else %}
|
||||
{{ current.cardinalWindDirection() | translate }}
|
||||
{% endif %}
|
||||
{% if config.showSun %}
|
||||
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
|
||||
<span>
|
||||
{% if current.nextSunAction() === "sunset" %}
|
||||
{{ current.sunset | formatTime }}
|
||||
{% else %}
|
||||
{{ current.sunrise | formatTime }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if config.showUVIndex %}
|
||||
<td class="align-right bright uv-index">
|
||||
<div class="wi dimmed wi-hot"></div>
|
||||
{{ current.uv_index }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="large">
|
||||
{% if config.showIndoorTemperature and indoor.temperature or config.showIndoorHumidity and indoor.humidity %}
|
||||
<span class="medium fas fa-home"></span>
|
||||
<span style="display: inline-block">
|
||||
{% if config.showIndoorTemperature and indoor.temperature %}
|
||||
<sup class="small" style="position: relative; display: block; text-align: left;">
|
||||
<span>
|
||||
{{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</span>
|
||||
</sup>
|
||||
{% endif %}
|
||||
{% if config.showIndoorHumidity and indoor.humidity %}
|
||||
<sub class="small" style="position: relative; display: block; text-align: left;">
|
||||
<span>
|
||||
{{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }}
|
||||
</span>
|
||||
</sub>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="light wi weathericon wi-{{ current.weatherType }}"></span>
|
||||
<span class="light bright">{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}</span>
|
||||
{% if config.showHumidity === "temp" %}
|
||||
<span class="medium bright">{{ humidity() }}</span>
|
||||
|
||||
</sup>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if config.showHumidity === "wind" %}
|
||||
{{ humidity() }}
|
||||
{% endif %}
|
||||
{% if config.showSun %}
|
||||
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
|
||||
<span>
|
||||
{% if current.nextSunAction() === "sunset" %}
|
||||
{{ current.sunset | formatTime }}
|
||||
{% else %}
|
||||
{{ current.sunrise | formatTime }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if config.showUVIndex %}
|
||||
<td class="align-right bright uv-index">
|
||||
<div class="wi dimmed wi-hot"></div>
|
||||
{{ current.uv_index }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if (config.showFeelsLike or config.showPrecipitationAmount or config.showPrecipitationProbability) and not config.onlyTemp %}
|
||||
<div class="normal medium feelslike">
|
||||
{% if config.showFeelsLike %}
|
||||
<span class="dimmed">
|
||||
{% if config.showHumidity === "feelslike" %}
|
||||
{{ humidity() }}
|
||||
{% endif %}
|
||||
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
|
||||
</span>
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationAmount and current.precipitationAmount %}
|
||||
<span class="dimmed">
|
||||
<span class="precipitationLeadText">{{ "PRECIP_AMOUNT" | translate }}</span> {{ current.precipitationAmount | unit("precip", current.precipitationUnits) }}
|
||||
</span>
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationProbability and current.precipitationProbability %}
|
||||
<span class="dimmed">
|
||||
<span class="precipitationLeadText">{{ "PRECIP_POP" | translate }}</span> {{ current.precipitationProbability }}%
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex large type-temp">
|
||||
{% if config.showIndoorTemperature and indoor.temperature or config.showIndoorHumidity and indoor.humidity %}
|
||||
<span class="medium fas fa-home"></span>
|
||||
<span style="display: inline-block">
|
||||
{% if config.showIndoorTemperature and indoor.temperature %}
|
||||
<sup class="small" style="position: relative; display: block; text-align: left;">
|
||||
<span> {{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }} </span>
|
||||
</sup>
|
||||
{% endif %}
|
||||
{% if config.showIndoorHumidity and indoor.humidity %}
|
||||
<sub class="small" style="position: relative; display: block; text-align: left;">
|
||||
<span> {{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }} </span>
|
||||
</sub>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if config.showHumidity === "below" %}
|
||||
<span class="medium dimmed">{{ humidity() }}</span>
|
||||
{% if current.weatherType %}
|
||||
<span class="light wi weathericon wi-{{ current.weatherType }}"></span>
|
||||
{% endif %}
|
||||
<span class="light bright">{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}</span>
|
||||
{% if config.showHumidity === "temp" %}
|
||||
<span class="medium bright">{{ humidity() }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if (config.showFeelsLike or config.showPrecipitationAmount or config.showPrecipitationProbability) and not config.onlyTemp %}
|
||||
<div class="normal medium feelslike">
|
||||
{% if config.showFeelsLike %}
|
||||
<span class="dimmed">
|
||||
{% if config.showHumidity === "feelslike" %}
|
||||
{{ humidity() }}
|
||||
{% endif %}
|
||||
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
|
||||
</span>
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationAmount and current.precipitationAmount %}
|
||||
<span class="dimmed"> <span class="precipitationLeadText">{{ "PRECIP_AMOUNT" | translate }}</span> {{ current.precipitationAmount | unit("precip", current.precipitationUnits) }} </span>
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationProbability and current.precipitationProbability %}
|
||||
<span class="dimmed"> <span class="precipitationLeadText">{{ "PRECIP_POP" | translate }}</span> {{ current.precipitationProbability }}% </span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if config.showHumidity === "below" %}
|
||||
<span class="medium dimmed">{{ humidity() }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||
{% endif %}
|
||||
<!-- Uncomment the line below to see the contents of the `current` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{current | dump}}</div> -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{ current | dump }}</div> -->
|
||||
|
||||
@@ -1,52 +1,46 @@
|
||||
{% if forecast %}
|
||||
{% set numSteps = forecast | calcNumSteps %}
|
||||
{% set currentStep = 0 %}
|
||||
<table class="{{ config.tableClass }}">
|
||||
{% if config.ignoreToday %}
|
||||
{% set forecast = forecast.splice(1) %}
|
||||
{% set numSteps = forecast | calcNumSteps %}
|
||||
{% set currentStep = 0 %}
|
||||
<table class="{{ config.tableClass }}">
|
||||
{% if config.ignoreToday %}
|
||||
{% set forecast = forecast.splice(1) %}
|
||||
{% endif %}
|
||||
{% set forecast = forecast.slice(0, numSteps) %}
|
||||
{% for f in forecast %}
|
||||
<tr
|
||||
{% if config.colored %}class="colored"{% endif %}
|
||||
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}
|
||||
>
|
||||
{% if (currentStep == 0) and config.ignoreToday == false and config.absoluteDates == false %}
|
||||
<td class="day">{{ "TODAY" | translate }}</td>
|
||||
{% elif (currentStep == 1) and config.ignoreToday == false and config.absoluteDates == false %}
|
||||
<td class="day">{{ "TOMORROW" | translate }}</td>
|
||||
{% else %}
|
||||
<td class="day">{{ f.date.format("ddd") }}</td>
|
||||
{% endif %}
|
||||
{% set forecast = forecast.slice(0, numSteps) %}
|
||||
{% for f in forecast %}
|
||||
<tr {% if config.colored %}class="colored"{% endif %}
|
||||
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
||||
{% if (currentStep == 0) and config.ignoreToday == false and config.absoluteDates == false %}
|
||||
<td class="day">{{ "TODAY" | translate }}</td>
|
||||
{% elif (currentStep == 1) and config.ignoreToday == false and config.absoluteDates == false %}
|
||||
<td class="day">{{ "TOMORROW" | translate }}</td>
|
||||
{% else %}
|
||||
<td class="day">{{ f.date.format("ddd") }}</td>
|
||||
{% endif %}
|
||||
<td class="bright weather-icon">
|
||||
<span class="wi weathericon wi-{{ f.weatherType }}"></span>
|
||||
</td>
|
||||
<td class="align-right bright max-temp">
|
||||
{{ f.maxTemperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</td>
|
||||
<td class="align-right min-temp">
|
||||
{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}
|
||||
</td>
|
||||
{% if config.showPrecipitationAmount %}
|
||||
<td class="align-right bright precipitation-amount">
|
||||
{{ f.precipitationAmount | unit("precip", f.precipitationUnits) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationProbability %}
|
||||
<td class="align-right bright precipitation-prob">
|
||||
{{ f.precipitationProbability | unit('precip', '%') }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if config.showUVIndex %}
|
||||
<td class="align-right dimmed uv-index">
|
||||
{{ f.uv_index }}
|
||||
<span class="wi dimmed weathericon wi-hot"></span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% set currentStep = currentStep + 1 %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
<td class="bright weather-icon">
|
||||
<span class="wi weathericon wi-{{ f.weatherType }}"></span>
|
||||
</td>
|
||||
<td class="align-right bright max-temp">{{ f.maxTemperature | roundValue | unit("temperature") | decimalSymbol }}</td>
|
||||
<td class="align-right min-temp">{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}</td>
|
||||
{% if config.showPrecipitationAmount %}
|
||||
<td class="align-right bright precipitation-amount">{{ f.precipitationAmount | unit("precip", f.precipitationUnits) }}</td>
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationProbability %}
|
||||
<td class="align-right bright precipitation-prob">{{ f.precipitationProbability | unit('precip', '%') }}</td>
|
||||
{% endif %}
|
||||
{% if config.showUVIndex %}
|
||||
<td class="align-right dimmed uv-index">
|
||||
{{ f.uv_index }}
|
||||
<span class="wi dimmed weathericon wi-hot"></span>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% set currentStep = currentStep + 1 %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||
{% endif %}
|
||||
<!-- Uncomment the line below to see the contents of the `forecast` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{forecast | dump}}</div> -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{ forecast | dump }}</div> -->
|
||||
|
||||
@@ -1,52 +1,48 @@
|
||||
{% if hourly %}
|
||||
{% set numSteps = hourly | calcNumEntries %}
|
||||
{% set currentStep = 0 %}
|
||||
<table class="{{ config.tableClass }}">
|
||||
{% set hours = hourly.slice(0, numSteps) %}
|
||||
{% for hour in hours %}
|
||||
<tr {% if config.colored %}class="colored"{% endif %}
|
||||
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
|
||||
<td class="day">{{ hour.date | formatTime }}</td>
|
||||
<td class="bright weather-icon">
|
||||
<span class="wi weathericon wi-{{ hour.weatherType }}"></span>
|
||||
</td>
|
||||
<td class="align-right bright">
|
||||
{{ hour.temperature | roundValue | unit("temperature") }}
|
||||
</td>
|
||||
{% if config.showUVIndex %}
|
||||
<td class="align-right bright uv-index">
|
||||
{% if hour.uv_index!=0 %}
|
||||
{{ hour.uv_index }}
|
||||
<span class="wi weathericon wi-hot"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if config.showHumidity != "none" %}
|
||||
<td class="align-left bright humidity-hourly">
|
||||
{{ hour.humidity }}
|
||||
<span class="wi wi-humidity humidity-icon"></span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationAmount %}
|
||||
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
|
||||
<td class="align-right bright precipitation-amount">
|
||||
{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationProbability %}
|
||||
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
|
||||
<td class="align-right bright precipitation-prob">
|
||||
{{ hour.precipitationProbability | unit('precip', '%') }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% set currentStep = currentStep + 1 %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% set numSteps = hourly | calcNumEntries %}
|
||||
{% set currentStep = 0 %}
|
||||
<table class="{{ config.tableClass }}">
|
||||
{% set hours = hourly.slice(0, numSteps) %}
|
||||
{% for hour in hours %}
|
||||
<tr
|
||||
{% if config.colored %}class="colored"{% endif %}
|
||||
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}
|
||||
>
|
||||
<td class="day">{{ hour.date | formatTime }}</td>
|
||||
<td class="bright weather-icon">
|
||||
<span class="wi weathericon wi-{{ hour.weatherType }}"></span>
|
||||
</td>
|
||||
<td class="align-right bright">{{ hour.temperature | roundValue | unit("temperature") }}</td>
|
||||
{% if config.showUVIndex %}
|
||||
<td class="align-right bright uv-index">
|
||||
{% if hour.uv_index!=0 %}
|
||||
{{ hour.uv_index }}
|
||||
<span class="wi weathericon wi-hot"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if config.showHumidity != "none" %}
|
||||
<td class="align-left bright humidity-hourly">
|
||||
{{ hour.humidity }}
|
||||
<span class="wi wi-humidity humidity-icon"></span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationAmount %}
|
||||
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
|
||||
<td class="align-right bright precipitation-amount">{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if config.showPrecipitationProbability %}
|
||||
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
|
||||
<td class="align-right bright precipitation-prob">{{ hour.precipitationProbability | unit('precip', '%') }}</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% set currentStep = currentStep + 1 %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
|
||||
{% endif %}
|
||||
<!-- Uncomment the line below to see the contents of the `hourly` object. -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{hourly | dump}}</div> -->
|
||||
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{ hourly | dump }}</div> -->
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
* with locations you can search under column B (English Names), with the corresponding siteCode under
|
||||
* column A (Codes) and provCode under column C (Province).
|
||||
*
|
||||
* Acknowledgement: Some logic and code for parsing Environment Canada web pages is based on material from MMM-EnvCanada
|
||||
*
|
||||
* License to use Environment Canada (EC) data is detailed here:
|
||||
* https://eccc-msc.github.io/open-data/licence/readme_en/
|
||||
*/
|
||||
@@ -49,6 +51,9 @@ WeatherProvider.register("envcanada", {
|
||||
this.todayTempCacheMax = 0;
|
||||
this.todayCached = false;
|
||||
this.cacheCurrentTemp = 999;
|
||||
this.lastCityPageCurrent = " ";
|
||||
this.lastCityPageForecast = " ";
|
||||
this.lastCityPageHourly = " ";
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -63,69 +68,158 @@ WeatherProvider.register("envcanada", {
|
||||
* Override the fetchCurrentWeather method to query EC and construct a Current weather object
|
||||
*/
|
||||
fetchCurrentWeather () {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
return;
|
||||
}
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load EnvCanada site data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
this.fetchCommon("Current");
|
||||
},
|
||||
|
||||
/*
|
||||
* Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
|
||||
* Override the fetchWeatherForecast method to query EC and construct Forecast/Daily weather objects
|
||||
*/
|
||||
fetchWeatherForecast () {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
return;
|
||||
}
|
||||
const forecastWeather = this.generateWeatherObjectsFromForecast(data);
|
||||
|
||||
this.setWeatherForecast(forecastWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load EnvCanada forecast data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
this.fetchCommon("Forecast");
|
||||
|
||||
},
|
||||
|
||||
/*
|
||||
* Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
|
||||
* Override the fetchWeatherHourly method to query EC and construct Hourly weather objects
|
||||
*/
|
||||
fetchWeatherHourly () {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
return;
|
||||
}
|
||||
const hourlyWeather = this.generateWeatherObjectsFromHourly(data);
|
||||
|
||||
this.setWeatherHourly(hourlyWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load EnvCanada hourly data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
this.fetchCommon("Hourly");
|
||||
},
|
||||
|
||||
/*
|
||||
* Build the EC URL based on the Site Code and Province Code specified in the config params. Note that the
|
||||
* URL defaults to the English version simply because there is no language dependency in the data
|
||||
* being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
|
||||
* Because the process to fetch weather data is virtually the same for Current, Forecast/Daily, and Hourly weather,
|
||||
* a common module is used to access the EC weather data. The only customization (based on the caller of this routine)
|
||||
* is how the data will be parsed to satisfy the Weather module config in Config.js
|
||||
*
|
||||
* Accessing EC weather data is accomplished in 2 steps:
|
||||
*
|
||||
* 1. Query the MSC Datamart Index page, which returns a list of all the filenames for all the cities that have
|
||||
* weather data currently available.
|
||||
*
|
||||
* 2. With the city filename identified, build the appropriate URL and get the weather data (XML document) for the
|
||||
* city specified in the Weather module Config information
|
||||
*/
|
||||
fetchCommon (target) {
|
||||
const forecastURL = this.getUrl(); // Get the approriate URL for the MSC Datamart Index page
|
||||
|
||||
Log.debug(`[weather.envcanada] ${target} Index url: ${forecastURL}`);
|
||||
|
||||
this.fetchData(forecastURL, "xml") // Query the Index page URL
|
||||
.then((indexData) => {
|
||||
if (!indexData) {
|
||||
// Did not receive usable new data.
|
||||
Log.info(`weather.envcanada ${target} - did not receive usable index data`);
|
||||
this.updateAvailable(); // If there were issues, update anyways to reset timer
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* With the Index page read, we must locate the filename/link for the specified city (aka Sitecode).
|
||||
* This is done by building the city filename and searching for it on the Index page. Once found,
|
||||
* extract the full filename (a unique name that includes dat/time, filename, etc.) and then add it
|
||||
* to the Index page URL to create the proper URL pointing to the city's weather data. Finally, read the
|
||||
* URL to pull in the city's XML document so that weather data can be parsed and displayed.
|
||||
*/
|
||||
|
||||
let forecastFile = "";
|
||||
let forecastFileURL = "";
|
||||
const fileSuffix = `${this.config.siteCode}_en.xml`; // Build city filename
|
||||
const nextFile = indexData.body.innerHTML.split(fileSuffix); // Find filename on Index page
|
||||
|
||||
if (nextFile.length > 1) { // Parse out the full unqiue file city filename
|
||||
// Find the last occurrence
|
||||
forecastFile = nextFile[nextFile.length - 2].slice(-41) + fileSuffix;
|
||||
forecastFileURL = forecastURL + forecastFile; // Create full URL to the city's weather data
|
||||
}
|
||||
|
||||
Log.debug(`[weather.envcanada] ${target} Citypage url: ${forecastFileURL}`);
|
||||
|
||||
/*
|
||||
* If the Citypage filename has not changed since the last Weather refresh, the forecast has not changed and
|
||||
* and therefore we can skip reading the Citypage URL.
|
||||
*/
|
||||
|
||||
if (target === "Current" && this.lastCityPageCurrent === forecastFileURL) {
|
||||
Log.debug(`[weather.envcanada] ${target} - Newest Citypage has already been seen - skipping!`);
|
||||
this.updateAvailable(); // Update anyways to reset refresh timer
|
||||
return;
|
||||
}
|
||||
|
||||
if (target === "Forecast" && this.lastCityPageForecast === forecastFileURL) {
|
||||
Log.debug(`[weather.envcanada] ${target} - Newest Citypage has already been seen - skipping!`);
|
||||
this.updateAvailable(); // Update anyways to reset refresh timer
|
||||
return;
|
||||
}
|
||||
|
||||
if (target === "Hourly" && this.lastCityPageHourly === forecastFileURL) {
|
||||
Log.debug(`[weather.envcanada] ${target} - Newest Citypage has already been seen - skipping!`);
|
||||
this.updateAvailable(); // Update anyways to reset refresh timer
|
||||
return;
|
||||
}
|
||||
|
||||
this.fetchData(forecastFileURL, "xml") // Read city's URL to get weather data
|
||||
.then((cityData) => {
|
||||
if (!cityData) {
|
||||
// Did not receive usable new data.
|
||||
Log.info(`weather.envcanada ${target} - did not receive usable citypage data`);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* With the city's weather data read, parse the resulting XML document for the appropriate weather data
|
||||
* elements to create a weather object. Next, set Weather modules details from that object.
|
||||
*/
|
||||
Log.debug(`[weather.envcanada] ${target} - Citypage has been read and will be processed for updates`);
|
||||
|
||||
if (target === "Current") {
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(cityData);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
this.lastCityPageCurrent = forecastFileURL;
|
||||
}
|
||||
|
||||
if (target === "Forecast") {
|
||||
const forecastWeather = this.generateWeatherObjectsFromForecast(cityData);
|
||||
this.setWeatherForecast(forecastWeather);
|
||||
this.lastCityPageForecast = forecastFileURL;
|
||||
}
|
||||
|
||||
if (target === "Hourly") {
|
||||
const hourlyWeather = this.generateWeatherObjectsFromHourly(cityData);
|
||||
this.setWeatherHourly(hourlyWeather);
|
||||
this.lastCityPageHourly = forecastFileURL;
|
||||
}
|
||||
})
|
||||
.catch(function (cityRequest) {
|
||||
Log.info(`weather.envcanada ${target} - could not load citypage data from: ${forecastFileURL}`);
|
||||
})
|
||||
.finally(() => this.updateAvailable()); // Update no matter what to reset weather refresh timer
|
||||
})
|
||||
.catch(function (indexRequest) {
|
||||
Log.error(`weather.envcanada ${target} - could not load index data ... `, indexRequest);
|
||||
this.updateAvailable(); // If there were issues, update anyways to reset timer
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* Build the EC Index page URL based on current GMT hour. The Index page will provide a list of links for each city
|
||||
* that will, in turn, provide actual weather data. The URL is comprised of 3 parts:
|
||||
*
|
||||
* Fixed value + Prov code specified in Weather module Config.js + current hour as GMT
|
||||
*/
|
||||
getUrl () {
|
||||
return `https://dd.weather.gc.ca/citypage_weather/xml/${this.config.provCode}/${this.config.siteCode}_e.xml`;
|
||||
let forecastURL = `https://dd.weather.gc.ca/citypage_weather/${this.config.provCode}`;
|
||||
const hour = this.getCurrentHourGMT();
|
||||
forecastURL += `/${hour}/`;
|
||||
return forecastURL;
|
||||
},
|
||||
|
||||
/*
|
||||
* Get current hour-of-day in GMT context
|
||||
*/
|
||||
getCurrentHourGMT () {
|
||||
const now = new Date();
|
||||
return now.toISOString().substring(11, 13); // "HH" in GMT
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -151,7 +245,6 @@ WeatherProvider.register("envcanada", {
|
||||
}
|
||||
|
||||
currentWeather.windSpeed = WeatherUtils.convertWindToMs(ECdoc.querySelector("siteData currentConditions wind speed").textContent);
|
||||
|
||||
currentWeather.windFromDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
|
||||
|
||||
currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent;
|
||||
@@ -214,7 +307,7 @@ WeatherProvider.register("envcanada", {
|
||||
/*
|
||||
* The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
|
||||
* 2 elements. the first element for a day details the Today (daytime) forecast while the second
|
||||
* element details the Tonight (nightime) forecast. Element 0 is always for the current day.
|
||||
* element details the Tonight (nighttime) forecast. Element 0 is always for the current day.
|
||||
*
|
||||
* However... the forecast is somewhat 'rolling'.
|
||||
*
|
||||
@@ -225,7 +318,7 @@ WeatherProvider.register("envcanada", {
|
||||
*
|
||||
* But, if the EC forecast is queried in late afternoon, the Current Today forecast will be rolled
|
||||
* off and Element 0 will contain Current Tonight. From there, the next 5 days will be contained in
|
||||
* Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Elelement 11 will contain a forecast for a 6th day,
|
||||
* Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Element 11 will contain a forecast for a 6th day,
|
||||
* but only for the Today portion (not Tonight). This module will create a 6-day forecast using
|
||||
* Elements 0 to 11, and will ignore the additional Todat forecast in Element 11.
|
||||
*
|
||||
@@ -436,17 +529,17 @@ WeatherProvider.register("envcanada", {
|
||||
* then it will be displayed ONLY if no POP is present.
|
||||
*
|
||||
* POP Logic: By default, we want to show the POP for 'daytime' since we are presuming that is what
|
||||
* people are more interested in seeing. While EC provides a separate POP for daytime and nightime portions
|
||||
* people are more interested in seeing. While EC provides a separate POP for daytime and nighttime portions
|
||||
* of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
* ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
* the nightime forecast after a certain point in the afternoon. As such, we will be showing the nightime POP
|
||||
* ignore any nighttime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
* the nighttime forecast after a certain point in the afternoon. As such, we will be showing the nighttime POP
|
||||
* (if one exists) in that specific scenario.
|
||||
*
|
||||
* Accumulation Logic: Similar to POP, we want to show accumulation for 'daytime' since we presume that is what
|
||||
* people are interested in seeing. While EC provides a separate accumulation for daytime and nightime portions
|
||||
* people are interested in seeing. While EC provides a separate accumulation for daytime and nighttime portions
|
||||
* of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
* ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
* the nightime forecast after a certain point in that specific scenario.
|
||||
* ignore any nighttime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
* the nighttime forecast after a certain point in that specific scenario.
|
||||
*/
|
||||
setPrecipitation (weather, foreGroup, today) {
|
||||
if (foreGroup[today].querySelector("precipitation accumulation")) {
|
||||
|
||||
@@ -13,7 +13,7 @@ WeatherProvider.register("openmeteo", {
|
||||
|
||||
/*
|
||||
* Set the name of the provider.
|
||||
* Not strictly required, but helps for debugging.
|
||||
* Not strictly required but helps for debugging.
|
||||
*/
|
||||
providerName: "Open-Meteo",
|
||||
|
||||
@@ -348,7 +348,7 @@ WeatherProvider.register("openmeteo", {
|
||||
generateWeatherDayFromCurrentWeather (weather) {
|
||||
|
||||
/**
|
||||
* Since some units comes from API response "splitted" into daily, hourly and current_weather
|
||||
* Since some units come from API response "splitted" into daily, hourly and current_weather
|
||||
* every time you request it, you have to ensure to get the data from the right place every time.
|
||||
* For the current weather case, the response have the following structure (after transposing):
|
||||
* ```
|
||||
@@ -381,6 +381,7 @@ WeatherProvider.register("openmeteo", {
|
||||
currentWeather.maxTemperature = parseFloat(weather.daily[0].temperature_2m_max);
|
||||
currentWeather.weatherType = this.convertWeatherType(weather.current_weather.weathercode, currentWeather.isDayTime());
|
||||
currentWeather.humidity = parseFloat(weather.hourly[h].relativehumidity_2m);
|
||||
currentWeather.feelsLikeTemp = parseFloat(weather.hourly[h].apparent_temperature);
|
||||
currentWeather.rain = parseFloat(weather.hourly[h].rain);
|
||||
currentWeather.snow = parseFloat(weather.hourly[h].snowfall * 10);
|
||||
currentWeather.precipitationAmount = parseFloat(weather.hourly[h].precipitation);
|
||||
|
||||
@@ -254,7 +254,7 @@ WeatherProvider.register("smhi", {
|
||||
* Helper method to get a property from the returned data set.
|
||||
* @param {object} currentWeatherData Weatherdata to get from
|
||||
* @param {string} name The name of the property
|
||||
* @returns {*} The value of the property in the weatherdata
|
||||
* @returns {string} The value of the property in the weatherdata
|
||||
*/
|
||||
paramValue (currentWeatherData, name) {
|
||||
return currentWeatherData.parameters.filter((p) => p.name === name).flatMap((p) => p.values)[0];
|
||||
|
||||
@@ -218,7 +218,7 @@ WeatherProvider.register("weathergov", {
|
||||
currentWeather.minTemperature = currentWeatherData.minTemperatureLast24Hours.value;
|
||||
currentWeather.maxTemperature = currentWeatherData.maxTemperatureLast24Hours.value;
|
||||
currentWeather.humidity = Math.round(currentWeatherData.relativeHumidity.value);
|
||||
currentWeather.precipitationAmount = currentWeatherData.precipitationLastHour.value ? currentWeatherData.precipitationLastHour.value : currentWeatherData.precipitationLast3Hours.value;
|
||||
currentWeather.precipitationAmount = currentWeatherData.precipitationLastHour?.value ?? currentWeatherData.precipitationLast3Hours?.value;
|
||||
if (currentWeatherData.heatIndex.value !== null) {
|
||||
currentWeather.feelsLikeTemp = currentWeatherData.heatIndex.value;
|
||||
} else if (currentWeatherData.windChill.value !== null) {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
.weather .weathericon,
|
||||
.weather .fa-home {
|
||||
font-size: 75%;
|
||||
line-height: 65px;
|
||||
display: inline-block;
|
||||
transform: translate(0, -3px);
|
||||
}
|
||||
|
||||
.weather .humidity-icon {
|
||||
@@ -37,10 +34,6 @@
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.weather tr .weathericon {
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.weather tr.colored .min-temp {
|
||||
color: #bcddff;
|
||||
}
|
||||
@@ -48,3 +41,9 @@
|
||||
.weather tr.colored .max-temp {
|
||||
color: #ff8e99;
|
||||
}
|
||||
|
||||
.weather .type-temp {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@@ -163,11 +163,12 @@ Module.register("weather", {
|
||||
// What to do when the weather provider has new information available?
|
||||
updateAvailable () {
|
||||
Log.log("New weather information available.");
|
||||
this.updateDom(0);
|
||||
// this value was changed from 0 to 300 to stabilize weather tests:
|
||||
this.updateDom(300);
|
||||
this.scheduleUpdate();
|
||||
|
||||
if (this.weatherProvider.currentWeather()) {
|
||||
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.weatherProvider.currentWeather().weatherType.replace("-", "_") });
|
||||
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.weatherProvider.currentWeather().weatherType?.replace("-", "_") });
|
||||
}
|
||||
|
||||
const notificationPayload = {
|
||||
|
||||
@@ -53,7 +53,7 @@ const WeatherUtils = {
|
||||
/**
|
||||
* Convert temp (from degrees C) into imperial or metric unit depending on
|
||||
* your config
|
||||
* @param {number} tempInC the temperature in celsius you want to convert
|
||||
* @param {number} tempInC the temperature in Celsius you want to convert
|
||||
* @param {string} unit can be 'imperial' or 'metric'
|
||||
* @returns {number} the converted temperature
|
||||
*/
|
||||
@@ -61,6 +61,15 @@ const WeatherUtils = {
|
||||
return unit === "imperial" ? tempInC * 1.8 + 32 : tempInC;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert temp (from degrees C) into metric unit
|
||||
* @param {number} tempInF the temperature in Fahrenheit you want to convert
|
||||
* @returns {number} the converted temperature
|
||||
*/
|
||||
convertTempToMetric (tempInF) {
|
||||
return ((tempInF - 32) * 5) / 9;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert wind speed into another unit.
|
||||
* @param {number} windInMS the windspeed in meter/sec you want to convert
|
||||
@@ -118,27 +127,51 @@ const WeatherUtils = {
|
||||
return kmh * 0.27777777777778;
|
||||
},
|
||||
|
||||
/**
|
||||
* Taken from https://community.home-assistant.io/t/calculating-apparent-feels-like-temperature/370834/18
|
||||
* @param {number} temperature temperature in degrees Celsius
|
||||
* @param {number} windSpeed wind speed in meter/second
|
||||
* @param {number} humidity relative humidity in percent
|
||||
* @returns {number} the feels like temperature in degrees Celsius
|
||||
*/
|
||||
calculateFeelsLike (temperature, windSpeed, humidity) {
|
||||
const windInMph = this.convertWind(windSpeed, "imperial");
|
||||
const tempInF = this.convertTemp(temperature, "imperial");
|
||||
let feelsLike = tempInF;
|
||||
|
||||
if (windInMph > 3 && tempInF < 50) {
|
||||
feelsLike = Math.round(35.74 + 0.6215 * tempInF - 35.75 * Math.pow(windInMph, 0.16) + 0.4275 * tempInF * Math.pow(windInMph, 0.16));
|
||||
} else if (tempInF > 80 && humidity > 40) {
|
||||
feelsLike
|
||||
= -42.379
|
||||
+ 2.04901523 * tempInF
|
||||
+ 10.14333127 * humidity
|
||||
- 0.22475541 * tempInF * humidity
|
||||
- 6.83783 * Math.pow(10, -3) * tempInF * tempInF
|
||||
- 5.481717 * Math.pow(10, -2) * humidity * humidity
|
||||
+ 1.22874 * Math.pow(10, -3) * tempInF * tempInF * humidity
|
||||
+ 8.5282 * Math.pow(10, -4) * tempInF * humidity * humidity
|
||||
- 1.99 * Math.pow(10, -6) * tempInF * tempInF * humidity * humidity;
|
||||
let HI;
|
||||
let WC = tempInF;
|
||||
|
||||
// Calculate wind chill for certain conditions
|
||||
if (tempInF <= 70 && windInMph >= 3) {
|
||||
WC = 35.74 + (0.6215 * tempInF) - 35.75 * Math.pow(windInMph, 0.16) + ((0.4275 * tempInF) * Math.pow(windInMph, 0.16));
|
||||
}
|
||||
|
||||
return ((feelsLike - 32) * 5) / 9;
|
||||
// Steadman Heat Index Vorberechnung
|
||||
const STEADMAN_HI = 0.5 * (tempInF + 61.0 + ((tempInF - 68.0) * 1.2) + (humidity * 0.094));
|
||||
|
||||
if (STEADMAN_HI >= 80) {
|
||||
// Rothfusz-Komplex
|
||||
const ROTHFUSZ_HI = -42.379 + 2.04901523 * tempInF + 10.14333127 * humidity - 0.22475541 * tempInF * humidity - 0.00683783 * tempInF * tempInF - 0.05481717 * humidity * humidity + 0.00122874 * tempInF * tempInF * humidity + 0.00085282 * tempInF * humidity * humidity - 0.00000199 * tempInF * tempInF * humidity * humidity;
|
||||
|
||||
HI = ROTHFUSZ_HI;
|
||||
|
||||
if (humidity < 13 && tempInF > 80 && tempInF < 112) {
|
||||
const ADJUSTMENT = ((13 - humidity) / 4) * Math.pow(Math.abs(17 - (tempInF - 95)), 0.5) / 17; // sqrt Teil
|
||||
HI = HI - ADJUSTMENT;
|
||||
} else if (humidity > 85 && tempInF > 80 && tempInF < 87) {
|
||||
const ADJUSTMENT = ((humidity - 85) / 10) * ((87 - tempInF) / 5);
|
||||
HI = HI + ADJUSTMENT;
|
||||
}
|
||||
|
||||
} else { HI = STEADMAN_HI; }
|
||||
|
||||
// Feuchte Lastberechnung FL
|
||||
let FL;
|
||||
if (tempInF < 50) { FL = WC; }
|
||||
else if (tempInF >= 50 && tempInF < 70) { FL = ((70 - tempInF) / 20) * WC + ((tempInF - 50) / 20) * HI; }
|
||||
else if (tempInF >= 70) { FL = HI; }
|
||||
|
||||
return this.convertTempToMetric(FL);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user