mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 19:53:36 +00:00
Merge branch 'develop' of https://github.com/MichMich/MagicMirror into develop
This commit is contained in:
commit
1e9fad8278
15
.github/workflows/enforce-changelog.yml
vendored
Normal file
15
.github/workflows/enforce-changelog.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
name: "Enforce Changelog"
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Enforces the update of a changelog file on every pull request
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: dangoslen/changelog-enforcer@v1.6.1
|
||||||
|
with:
|
||||||
|
changeLogPath: 'CHANGELOG.md'
|
||||||
|
skipLabels: 'Skip Changelog'
|
35
.github/workflows/node-ci.js.yml
vendored
Normal file
35
.github/workflows/node-ci.js.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
|
name: Automated Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master, develop ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [10.x, 12.x, 14.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- run: |
|
||||||
|
Xvfb :99 -screen 0 1024x768x16 &
|
||||||
|
export DISPLAY=:99
|
||||||
|
npm install
|
||||||
|
npm run test:prettier
|
||||||
|
npm run test:js
|
||||||
|
npm run test:css
|
||||||
|
npm run test:e2e
|
||||||
|
npm run test:unit
|
@ -2,3 +2,4 @@ package-lock.json
|
|||||||
/config/**/*
|
/config/**/*
|
||||||
/vendor/**/*
|
/vendor/**/*
|
||||||
!/vendor/vendor.js
|
!/vendor/vendor.js
|
||||||
|
.github/**/*
|
||||||
|
25
.travis.yml
25
.travis.yml
@ -1,25 +0,0 @@
|
|||||||
dist: trusty
|
|
||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- 10
|
|
||||||
- lts/*
|
|
||||||
- node
|
|
||||||
before_install:
|
|
||||||
- npm i -g npm
|
|
||||||
before_script:
|
|
||||||
- yarn danger ci
|
|
||||||
- "export DISPLAY=:99.0"
|
|
||||||
- "export ELECTRON_DISABLE_SANDBOX=1"
|
|
||||||
- "sh -e /etc/init.d/xvfb start"
|
|
||||||
- sleep 5
|
|
||||||
script:
|
|
||||||
- npm run test:prettier
|
|
||||||
- npm run test:js
|
|
||||||
- npm run test:css
|
|
||||||
- npm run test:e2e
|
|
||||||
- npm run test:unit
|
|
||||||
after_script:
|
|
||||||
- npm list
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- node_modules
|
|
31
CHANGELOG.md
31
CHANGELOG.md
@ -12,27 +12,46 @@ _This release is scheduled to be released on 2021-01-01._
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added new log level "debug" to the logger.
|
- Added new log level "debug" to the logger.
|
||||||
|
- Added new parameter "useKmh" to weather module for displaying wind speed as kmh.
|
||||||
|
- Chuvash translation.
|
||||||
|
- Added Weatherbit as a provider to Weather module.
|
||||||
|
- Added SMHI as a provider to Weather module.
|
||||||
- Added Hindi & Gujarati translation.
|
- Added Hindi & Gujarati translation.
|
||||||
- Chuvash translation.
|
- Chuvash translation.
|
||||||
|
- Calendar: new options "limitDays" and "coloredEvents".
|
||||||
|
- Added new option "limitDays" - limit the number of discreet days displayed.
|
||||||
|
- Added new option "customEvents" - use custom symbol/color based on keyword in event title.
|
||||||
|
- Added GitHub workflows for automated testing and changelog enforcement.
|
||||||
|
|
||||||
### Updated
|
### Updated
|
||||||
|
|
||||||
- Weather module - forecast now show TODAY and TOMORROW instead of weekday, to make it easier to understand.
|
- Weather module - forecast now show TODAY and TOMORROW instead of weekday, to make it easier to understand.
|
||||||
- Update dependencies to latest versions.
|
- Update dependencies to latest versions.
|
||||||
|
- Update dependencies eslint, feedme, simple-git and socket.io to latest versions.
|
||||||
|
- Update lithuanian translation.
|
||||||
|
|
||||||
### Deleted
|
### Deleted
|
||||||
|
|
||||||
|
- Removed Travis CI intergration.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- JSON Parse translation files with comments crashing UI. (#2149)
|
- JSON Parse translation files with comments crashing UI. (#2149)
|
||||||
- Calendar parsing where RRULE bug returns wrong date, add Windows timezone name support. (#2145, #2151)
|
- Calendar parsing where RRULE bug returns wrong date, add Windows timezone name support. (#2145, #2151)
|
||||||
- Wrong node-ical version installed (package.json) requested version. (#2153)
|
- Wrong node-ical version installed (package.json) requested version. (#2153)
|
||||||
- Fix calendar fetcher subsequent timing (#2160)
|
- Fix calendar fetcher subsequent timing. (#2160)
|
||||||
- Rename Greek translation to correct ISO 639-1 alpha-2 code (gr > el). (#2155)
|
- Rename Greek translation to correct ISO 639-1 alpha-2 code (gr > el). (#2155)
|
||||||
- Add a space after icons of sunrise and sunset (#2169)
|
- Add a space after icons of sunrise and sunset. (#2169)
|
||||||
- Fix calendar when no DTEND record found in event, startDate overlay when endDate set (#2177)
|
- Fix calendar when no DTEND record found in event, startDate overlay when endDate set. (#2177)
|
||||||
- Fix calendar full day event east of UTC start time (#2200)
|
- Fix windspeed convertion error in ukmetoffice weather provider. (#2189)
|
||||||
|
- Fix console.debug not having timestamps. (#2199)
|
||||||
|
- Fix calendar full day event east of UTC start time. (#2200)
|
||||||
|
- Fix non-fullday recurring rule processing. (#2216)
|
||||||
|
- Catch errors when parsing calendar data with ical. (#2022)
|
||||||
|
- Corrected logic for timeFormat "relative" and "absolute".
|
||||||
|
- Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228)
|
||||||
|
- Weather module - Always displays night icons when local is other then English. (#2221)
|
||||||
|
- update Node-ical 0.12.4 , fix invalid RRULE format in cal entries
|
||||||
|
|
||||||
## [2.13.0] - 2020-10-01
|
## [2.13.0] - 2020-10-01
|
||||||
|
|
||||||
@ -237,7 +256,7 @@ Special thanks to @sdetweil for all his great contributions!
|
|||||||
|
|
||||||
- Option to show event location in calendar
|
- Option to show event location in calendar
|
||||||
- Finnish translation for "Feels" and "Weeks"
|
- Finnish translation for "Feels" and "Weeks"
|
||||||
- Russian translation for "Feels"
|
- Russian translation for “Feels”
|
||||||
- Calendar module: added `nextDaysRelative` config option
|
- Calendar module: added `nextDaysRelative` config option
|
||||||
- Add `broadcastPastEvents` config option for calendars to include events from the past `maximumNumberOfDays` in event broadcasts
|
- Add `broadcastPastEvents` config option for calendars to include events from the past `maximumNumberOfDays` in event broadcasts
|
||||||
- Added feature to broadcast news feed items `NEWS_FEED` and updated news items `NEWS_FEED_UPDATED` in default [newsfeed](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/newsfeed) module (when news is updated) with documented default and `config.js` options in [README.md](https://github.com/MichMich/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
- Added feature to broadcast news feed items `NEWS_FEED` and updated news items `NEWS_FEED_UPDATED` in default [newsfeed](https://github.com/MichMich/MagicMirror/tree/develop/modules/default/newsfeed) module (when news is updated) with documented default and `config.js` options in [README.md](https://github.com/MichMich/MagicMirror/blob/develop/modules/default/newsfeed/README.md)
|
||||||
|
@ -5,8 +5,7 @@
|
|||||||
<a href="https://david-dm.org/MichMich/MagicMirror#info=devDependencies"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
|
<a href="https://david-dm.org/MichMich/MagicMirror#info=devDependencies"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
|
||||||
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge"></a>
|
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge"></a>
|
||||||
<a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
<a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
||||||
<a href="https://travis-ci.com/MichMich/MagicMirror"><img src="https://travis-ci.com/MichMich/MagicMirror.svg" alt="Travis"></a>
|
<a href="https://github.com/MichMich/MagicMirror/actions?query=workflow%3A%22Automated+Tests%22"><img src="https://github.com/MichMich/MagicMirror/workflows/Automated%20Tests/badge.svg" alt="Tests"></a>
|
||||||
<a href="https://snyk.io/test/github/MichMich/MagicMirror"><img src="https://snyk.io/test/github/MichMich/MagicMirror/badge.svg" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/MichMich/MagicMirror" style="max-width:100%;"></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
|
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
|
||||||
@ -40,6 +39,6 @@ If we receive enough donations we might even be able to free up some working hou
|
|||||||
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
|
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<br>
|
<br>
|
||||||
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -10,7 +10,10 @@
|
|||||||
(function (root, factory) {
|
(function (root, factory) {
|
||||||
if (typeof exports === "object") {
|
if (typeof exports === "object") {
|
||||||
// add timestamps in front of log messages
|
// add timestamps in front of log messages
|
||||||
require("console-stamp")(console, "yyyy-mm-dd HH:MM:ss.l");
|
require("console-stamp")(console, {
|
||||||
|
pattern: "yyyy-mm-dd HH:MM:ss.l",
|
||||||
|
include: ["debug", "log", "info", "warn", "error"]
|
||||||
|
});
|
||||||
|
|
||||||
// Node, CommonJS-like
|
// Node, CommonJS-like
|
||||||
module.exports = factory(root.config);
|
module.exports = factory(root.config);
|
||||||
@ -21,8 +24,8 @@
|
|||||||
})(this, function (config) {
|
})(this, function (config) {
|
||||||
const logLevel = {
|
const logLevel = {
|
||||||
debug: Function.prototype.bind.call(console.debug, console),
|
debug: Function.prototype.bind.call(console.debug, console),
|
||||||
info: Function.prototype.bind.call(console.info, console),
|
|
||||||
log: Function.prototype.bind.call(console.log, console),
|
log: Function.prototype.bind.call(console.log, console),
|
||||||
|
info: Function.prototype.bind.call(console.info, console),
|
||||||
warn: Function.prototype.bind.call(console.warn, console),
|
warn: Function.prototype.bind.call(console.warn, console),
|
||||||
error: Function.prototype.bind.call(console.error, console),
|
error: Function.prototype.bind.call(console.error, console),
|
||||||
group: Function.prototype.bind.call(console.group, console),
|
group: Function.prototype.bind.call(console.group, console),
|
||||||
|
@ -100,10 +100,13 @@ Module.register("alert", {
|
|||||||
message: image + message,
|
message: image + message,
|
||||||
effect: this.config.alert_effect,
|
effect: this.config.alert_effect,
|
||||||
ttl: params.timer,
|
ttl: params.timer,
|
||||||
|
onClose: () => this.hide_alert(sender),
|
||||||
al_no: "ns-alert"
|
al_no: "ns-alert"
|
||||||
});
|
});
|
||||||
|
|
||||||
//Show alert
|
//Show alert
|
||||||
this.alerts[sender.name].show();
|
this.alerts[sender.name].show();
|
||||||
|
|
||||||
//Add timer to dismiss alert and overlay
|
//Add timer to dismiss alert and overlay
|
||||||
if (params.timer) {
|
if (params.timer) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -11,6 +11,7 @@ Module.register("calendar", {
|
|||||||
defaults: {
|
defaults: {
|
||||||
maximumEntries: 10, // Total Maximum Entries
|
maximumEntries: 10, // Total Maximum Entries
|
||||||
maximumNumberOfDays: 365,
|
maximumNumberOfDays: 365,
|
||||||
|
limitDays: 0, // Limit the number of days shown, 0 = no limit
|
||||||
displaySymbol: true,
|
displaySymbol: true,
|
||||||
defaultSymbol: "calendar", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
defaultSymbol: "calendar", // Fontawesome Symbol see https://fontawesome.com/cheatsheet?from=io
|
||||||
showLocation: false,
|
showLocation: false,
|
||||||
@ -37,6 +38,7 @@ Module.register("calendar", {
|
|||||||
hideOngoing: false,
|
hideOngoing: false,
|
||||||
colored: false,
|
colored: false,
|
||||||
coloredSymbolOnly: false,
|
coloredSymbolOnly: false,
|
||||||
|
customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched
|
||||||
tableClass: "small",
|
tableClass: "small",
|
||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
@ -153,6 +155,12 @@ Module.register("calendar", {
|
|||||||
|
|
||||||
// Override dom generator.
|
// Override dom generator.
|
||||||
getDom: function () {
|
getDom: function () {
|
||||||
|
// Define second, minute, hour, and day constants
|
||||||
|
const oneSecond = 1000; // 1,000 milliseconds
|
||||||
|
const oneMinute = oneSecond * 60;
|
||||||
|
const oneHour = oneMinute * 60;
|
||||||
|
const oneDay = oneHour * 24;
|
||||||
|
|
||||||
var events = this.createEventList();
|
var events = this.createEventList();
|
||||||
var wrapper = document.createElement("table");
|
var wrapper = document.createElement("table");
|
||||||
wrapper.className = this.config.tableClass;
|
wrapper.className = this.config.tableClass;
|
||||||
@ -173,6 +181,8 @@ Module.register("calendar", {
|
|||||||
|
|
||||||
var currentFadeStep = 0;
|
var currentFadeStep = 0;
|
||||||
var lastSeenDate = "";
|
var lastSeenDate = "";
|
||||||
|
var ev;
|
||||||
|
var needle;
|
||||||
|
|
||||||
for (var e in events) {
|
for (var e in events) {
|
||||||
var event = events[e];
|
var event = events[e];
|
||||||
@ -218,6 +228,19 @@ Module.register("calendar", {
|
|||||||
symbolWrapper.className = "symbol align-right " + symbolClass;
|
symbolWrapper.className = "symbol align-right " + symbolClass;
|
||||||
|
|
||||||
var symbols = this.symbolsForEvent(event);
|
var symbols = this.symbolsForEvent(event);
|
||||||
|
// If symbols are displayed and custom symbol is set, replace event symbol
|
||||||
|
if (this.config.displaySymbol && this.config.customEvents.length > 0) {
|
||||||
|
for (ev in this.config.customEvents) {
|
||||||
|
if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") {
|
||||||
|
needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||||
|
if (needle.test(event.title)) {
|
||||||
|
symbols[0] = this.config.customEvents[ev].symbol;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < symbols.length; i++) {
|
for (var i = 0; i < symbols.length; i++) {
|
||||||
var symbol = document.createElement("span");
|
var symbol = document.createElement("span");
|
||||||
symbol.className = "fa fa-fw fa-" + symbols[i];
|
symbol.className = "fa fa-fw fa-" + symbols[i];
|
||||||
@ -248,6 +271,23 @@ Module.register("calendar", {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Color events if custom color is specified
|
||||||
|
if (this.config.customEvents.length > 0) {
|
||||||
|
for (ev in this.config.customEvents) {
|
||||||
|
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
|
||||||
|
needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
|
||||||
|
if (needle.test(event.title)) {
|
||||||
|
eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||||
|
titleWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||||
|
if (this.config.displaySymbol) {
|
||||||
|
symbolWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
|
titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
|
||||||
|
|
||||||
var titleClass = this.titleClassForUrl(event.url);
|
var titleClass = this.titleClassForUrl(event.url);
|
||||||
@ -280,82 +320,56 @@ Module.register("calendar", {
|
|||||||
|
|
||||||
eventWrapper.appendChild(titleWrapper);
|
eventWrapper.appendChild(titleWrapper);
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
// Define second, minute, hour, and day variables
|
|
||||||
var oneSecond = 1000; // 1,000 milliseconds
|
if (this.config.timeFormat === "absolute") {
|
||||||
var oneMinute = oneSecond * 60;
|
// Use dateFormat
|
||||||
var oneHour = oneMinute * 60;
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
||||||
var oneDay = oneHour * 24;
|
// Add end time if showEnd
|
||||||
if (event.fullDayEvent) {
|
if (this.config.showEnd) {
|
||||||
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
timeWrapper.innerHTML += "-";
|
||||||
event.endDate -= oneSecond;
|
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
||||||
if (event.today) {
|
}
|
||||||
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
// For full day events we use the fullDayEventDateFormat
|
||||||
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
|
if (event.fullDayEvent) {
|
||||||
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
|
||||||
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
|
event.endDate -= oneSecond;
|
||||||
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
||||||
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
}
|
||||||
} else {
|
if (this.config.getRelative > 0 && event.startDate < now) {
|
||||||
|
// Ongoing and getRelative is set
|
||||||
|
timeWrapper.innerHTML = this.capFirst(
|
||||||
|
this.translate("RUNNING", {
|
||||||
|
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||||
|
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * oneDay) {
|
||||||
|
// Within urgency days
|
||||||
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||||
|
}
|
||||||
|
if (event.fullDayEvent && this.config.nextDaysRelative) {
|
||||||
|
// Full days events within the next two days
|
||||||
|
if (event.today) {
|
||||||
|
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
|
||||||
|
} else if (event.startDate - now < oneDay && event.startDate - now > 0) {
|
||||||
|
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
|
||||||
|
} else if (event.startDate - now < 2 * oneDay && event.startDate - now > 0) {
|
||||||
|
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
|
||||||
|
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show relative times
|
||||||
|
if (event.startDate >= now) {
|
||||||
|
// Use relative time
|
||||||
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
||||||
|
if (event.startDate - now < this.config.getRelative * oneHour) {
|
||||||
|
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* Check to see if the user displays absolute or relative dates with their events
|
// Ongoing event
|
||||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
|
||||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
|
||||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
|
||||||
*
|
|
||||||
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
|
|
||||||
*/
|
|
||||||
if (this.config.timeFormat === "absolute") {
|
|
||||||
if (this.config.urgency > 1 && event.startDate - now < this.config.urgency * oneDay) {
|
|
||||||
// This event falls within the config.urgency period that the user has set
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
|
|
||||||
} else {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").from(moment().format("YYYYMMDD")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.config.showEnd) {
|
|
||||||
timeWrapper.innerHTML += "-";
|
|
||||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.fullDayEventDateFormat));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (event.startDate >= new Date()) {
|
|
||||||
if (event.startDate - now < 2 * oneDay) {
|
|
||||||
// This event is within the next 48 hours (2 days)
|
|
||||||
if (event.startDate - now < this.config.getRelative * oneHour) {
|
|
||||||
// If event is within 6 hour, display 'in xxx' time format or moment.fromNow()
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
|
||||||
} else {
|
|
||||||
if (this.config.timeFormat === "absolute" && !this.config.nextDaysRelative) {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
|
||||||
} else {
|
|
||||||
// Otherwise just say 'Today/Tomorrow at such-n-such time'
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/* Check to see if the user displays absolute or relative dates with their events
|
|
||||||
* Also check to see if an event is happening within an 'urgency' time frameElement
|
|
||||||
* For example, if the user set an .urgency of 7 days, those events that fall within that
|
|
||||||
* time frame will be displayed with 'in xxx' time format or moment.fromNow()
|
|
||||||
*
|
|
||||||
* Note: this needs to be put in its own function, as the whole thing repeats again verbatim
|
|
||||||
*/
|
|
||||||
if (this.config.timeFormat === "absolute") {
|
|
||||||
if (this.config.urgency > 1 && event.startDate - now < this.config.urgency * oneDay) {
|
|
||||||
// This event falls within the config.urgency period that the user has set
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
|
||||||
} else {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
timeWrapper.innerHTML = this.capFirst(
|
timeWrapper.innerHTML = this.capFirst(
|
||||||
this.translate("RUNNING", {
|
this.translate("RUNNING", {
|
||||||
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
fallback: this.translate("RUNNING") + " {timeUntilEnd}",
|
||||||
@ -363,12 +377,7 @@ Module.register("calendar", {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.config.showEnd) {
|
|
||||||
timeWrapper.innerHTML += "-";
|
|
||||||
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//timeWrapper.innerHTML += ' - '+ moment(event.startDate,'x').format('lll');
|
|
||||||
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
|
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
|
||||||
eventWrapper.appendChild(timeWrapper);
|
eventWrapper.appendChild(timeWrapper);
|
||||||
}
|
}
|
||||||
@ -521,6 +530,35 @@ Module.register("calendar", {
|
|||||||
events.sort(function (a, b) {
|
events.sort(function (a, b) {
|
||||||
return a.startDate - b.startDate;
|
return a.startDate - b.startDate;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Limit the number of days displayed
|
||||||
|
// If limitDays is set > 0, limit display to that number of days
|
||||||
|
if (this.config.limitDays > 0) {
|
||||||
|
var newEvents = [];
|
||||||
|
var lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
|
||||||
|
var days = 0;
|
||||||
|
var eventDate;
|
||||||
|
for (var ev of events) {
|
||||||
|
eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
|
||||||
|
// if date of event is later than lastdate
|
||||||
|
// check if we already are showing max unique days
|
||||||
|
if (eventDate > lastDate) {
|
||||||
|
// if the only entry in the first day is a full day event that day is not counted as unique
|
||||||
|
if (newEvents.length === 1 && days === 1 && newEvents[0].fullDayEvent) {
|
||||||
|
days--;
|
||||||
|
}
|
||||||
|
days++;
|
||||||
|
if (days > this.config.limitDays) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
lastDate = eventDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newEvents.push(ev);
|
||||||
|
}
|
||||||
|
events = newEvents;
|
||||||
|
}
|
||||||
|
|
||||||
return events.slice(0, this.config.maximumEntries);
|
return events.slice(0, this.config.maximumEntries);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -76,7 +76,18 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = ical.parseICS(requestData);
|
let data = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = ical.parseICS(requestData);
|
||||||
|
} catch (error) {
|
||||||
|
fetchFailedCallback(self, error.message);
|
||||||
|
scheduleTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.debug(" parsed data=" + JSON.stringify(data));
|
||||||
|
|
||||||
const newEvents = [];
|
const newEvents = [];
|
||||||
|
|
||||||
// limitFunction doesn't do much limiting, see comment re: the dates array in rrule section below as to why we need to do the filtering ourselves
|
// limitFunction doesn't do much limiting, see comment re: the dates array in rrule section below as to why we need to do the filtering ourselves
|
||||||
@ -87,13 +98,13 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
const eventDate = function (event, time) {
|
const eventDate = function (event, time) {
|
||||||
return isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
return isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
|
||||||
};
|
};
|
||||||
|
Log.debug("there are " + Object.entries(data).length + " calendar entries");
|
||||||
Object.entries(data).forEach(([key, event]) => {
|
Object.entries(data).forEach(([key, event]) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const today = moment().startOf("day").toDate();
|
const today = moment().startOf("day").toDate();
|
||||||
const future = moment().startOf("day").add(maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
const future = moment().startOf("day").add(maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
|
||||||
let past = today;
|
let past = today;
|
||||||
|
Log.debug("have entries ");
|
||||||
if (includePastEvents) {
|
if (includePastEvents) {
|
||||||
past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate();
|
past = moment().startOf("day").subtract(maximumNumberOfDays, "days").toDate();
|
||||||
}
|
}
|
||||||
@ -110,7 +121,8 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
if (event.type === "VEVENT") {
|
if (event.type === "VEVENT") {
|
||||||
let startDate = eventDate(event, "start");
|
let startDate = eventDate(event, "start");
|
||||||
let endDate;
|
let endDate;
|
||||||
// Log.debug("\nevent="+JSON.stringify(event))
|
|
||||||
|
Log.debug("\nevent=" + JSON.stringify(event));
|
||||||
if (typeof event.end !== "undefined") {
|
if (typeof event.end !== "undefined") {
|
||||||
endDate = eventDate(event, "end");
|
endDate = eventDate(event, "end");
|
||||||
} else if (typeof event.duration !== "undefined") {
|
} else if (typeof event.duration !== "undefined") {
|
||||||
@ -212,8 +224,15 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
pastLocal = pastMoment.toDate();
|
pastLocal = pastMoment.toDate();
|
||||||
futureLocal = futureMoment.toDate();
|
futureLocal = futureMoment.toDate();
|
||||||
} else {
|
} else {
|
||||||
pastLocal = pastMoment.subtract(past.getTimezoneOffset(), "minutes").toDate();
|
// if we want past events
|
||||||
futureLocal = futureMoment.subtract(future.getTimezoneOffset(), "minutes").toDate();
|
if (includePastEvents) {
|
||||||
|
// use the calculated past time for the between from
|
||||||
|
pastLocal = pastMoment.toDate();
|
||||||
|
} else {
|
||||||
|
// otherwise use NOW.. cause we shouldnt use any before now
|
||||||
|
pastLocal = moment().toDate(); //now
|
||||||
|
}
|
||||||
|
futureLocal = futureMoment.toDate(); // future
|
||||||
}
|
}
|
||||||
Log.debug(" between=" + pastLocal + " to " + futureLocal);
|
Log.debug(" between=" + pastLocal + " to " + futureLocal);
|
||||||
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
|
||||||
@ -299,6 +318,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showRecurrence === true) {
|
if (showRecurrence === true) {
|
||||||
|
Log.debug("saving event =" + description);
|
||||||
addedEvents++;
|
addedEvents++;
|
||||||
newEvents.push({
|
newEvents.push({
|
||||||
title: recurrenceTitle,
|
title: recurrenceTitle,
|
||||||
@ -352,7 +372,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
}
|
}
|
||||||
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
|
// if the start and end are the same, then make end the 'end of day' value (start is at 00:00:00)
|
||||||
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
|
if (fullDayEvent && startDate.format("x") === endDate.format("x")) {
|
||||||
//Log.debug("end same as start")
|
|
||||||
endDate = endDate.endOf("day");
|
endDate = endDate.endOf("day");
|
||||||
}
|
}
|
||||||
// get correction for date saving and dst change between now and then
|
// get correction for date saving and dst change between now and then
|
||||||
@ -376,7 +395,21 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
|
|||||||
return a.startDate - b.startDate;
|
return a.startDate - b.startDate;
|
||||||
});
|
});
|
||||||
|
|
||||||
events = newEvents.slice(0, maximumEntries);
|
// include up to maximumEntries current or upcoming events
|
||||||
|
// If past events should be included, include all past events
|
||||||
|
const now = moment();
|
||||||
|
var entries = 0;
|
||||||
|
events = [];
|
||||||
|
for (let ne of newEvents) {
|
||||||
|
if (moment(ne.endDate, "x").isBefore(now)) {
|
||||||
|
if (includePastEvents) events.push(ne);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entries++;
|
||||||
|
// If max events has been saved, skip the rest
|
||||||
|
if (entries > maximumEntries) break;
|
||||||
|
events.push(ne);
|
||||||
|
}
|
||||||
|
|
||||||
self.broadcastEvents();
|
self.broadcastEvents();
|
||||||
scheduleTimer();
|
scheduleTimer();
|
||||||
|
@ -9,7 +9,11 @@
|
|||||||
{% if config.useBeaufort %}
|
{% if config.useBeaufort %}
|
||||||
{{ current.beaufortWindSpeed() | round }}
|
{{ current.beaufortWindSpeed() | round }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ current.windSpeed | round }}
|
{% if config.useKmh %}
|
||||||
|
{{ current.kmhWindSpeed() | round }}
|
||||||
|
{% else %}
|
||||||
|
{{ current.windSpeed | round }}
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if config.showWindDirection %}
|
{% if config.showWindDirection %}
|
||||||
<sup>
|
<sup>
|
||||||
|
@ -62,7 +62,7 @@ WeatherProvider.register("darksky", {
|
|||||||
|
|
||||||
// Implement WeatherDay generator.
|
// Implement WeatherDay generator.
|
||||||
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
currentWeather.date = moment();
|
currentWeather.date = moment();
|
||||||
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
|
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
|
||||||
@ -80,7 +80,7 @@ WeatherProvider.register("darksky", {
|
|||||||
const days = [];
|
const days = [];
|
||||||
|
|
||||||
for (const forecast of forecasts) {
|
for (const forecast of forecasts) {
|
||||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
weather.date = moment(forecast.time, "X");
|
weather.date = moment(forecast.time, "X");
|
||||||
weather.minTemperature = forecast.temperatureMin;
|
weather.minTemperature = forecast.temperatureMin;
|
||||||
|
@ -89,11 +89,15 @@ WeatherProvider.register("openweathermap", {
|
|||||||
* Generate a WeatherObject based on currentWeatherInformation
|
* Generate a WeatherObject based on currentWeatherInformation
|
||||||
*/
|
*/
|
||||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
currentWeather.humidity = currentWeatherData.main.humidity;
|
currentWeather.humidity = currentWeatherData.main.humidity;
|
||||||
currentWeather.temperature = currentWeatherData.main.temp;
|
currentWeather.temperature = currentWeatherData.main.temp;
|
||||||
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
if (this.config.windUnits === "metric") {
|
||||||
|
currentWeather.windSpeed = this.config.useKmh ? currentWeatherData.wind.speed * 3.6 : currentWeatherData.wind.speed;
|
||||||
|
} else {
|
||||||
|
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
||||||
|
}
|
||||||
currentWeather.windDirection = currentWeatherData.wind.deg;
|
currentWeather.windDirection = currentWeatherData.wind.deg;
|
||||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
||||||
currentWeather.sunrise = moment(currentWeatherData.sys.sunrise, "X");
|
currentWeather.sunrise = moment(currentWeatherData.sys.sunrise, "X");
|
||||||
@ -112,7 +116,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
return this.fetchForecastDaily(forecasts);
|
return this.fetchForecastDaily(forecasts);
|
||||||
}
|
}
|
||||||
// if weatherEndpoint does not match forecast or forecast/daily, what should be returned?
|
// if weatherEndpoint does not match forecast or forecast/daily, what should be returned?
|
||||||
const days = [new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits)];
|
const days = [new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh)];
|
||||||
return days;
|
return days;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -124,7 +128,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
return this.fetchOnecall(data);
|
return this.fetchOnecall(data);
|
||||||
}
|
}
|
||||||
// if weatherEndpoint does not match onecall, what should be returned?
|
// if weatherEndpoint does not match onecall, what should be returned?
|
||||||
const weatherData = { current: new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits), hours: [], days: [] };
|
const weatherData = { current: new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh), hours: [], days: [] };
|
||||||
return weatherData;
|
return weatherData;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -141,7 +145,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
let snow = 0;
|
let snow = 0;
|
||||||
// variable for date
|
// variable for date
|
||||||
let date = "";
|
let date = "";
|
||||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
for (const forecast of forecasts) {
|
for (const forecast of forecasts) {
|
||||||
if (date !== moment(forecast.dt, "X").format("YYYY-MM-DD")) {
|
if (date !== moment(forecast.dt, "X").format("YYYY-MM-DD")) {
|
||||||
@ -154,7 +158,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
// push weather information to days array
|
// push weather information to days array
|
||||||
days.push(weather);
|
days.push(weather);
|
||||||
// create new weather-object
|
// create new weather-object
|
||||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
minTemp = [];
|
minTemp = [];
|
||||||
maxTemp = [];
|
maxTemp = [];
|
||||||
@ -217,7 +221,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
const days = [];
|
const days = [];
|
||||||
|
|
||||||
for (const forecast of forecasts) {
|
for (const forecast of forecasts) {
|
||||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
weather.date = moment(forecast.dt, "X");
|
weather.date = moment(forecast.dt, "X");
|
||||||
weather.minTemperature = forecast.temp.min;
|
weather.minTemperature = forecast.temp.min;
|
||||||
@ -263,7 +267,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
let precip = false;
|
let precip = false;
|
||||||
|
|
||||||
// get current weather, if requested
|
// get current weather, if requested
|
||||||
const current = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const current = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
if (data.hasOwnProperty("current")) {
|
if (data.hasOwnProperty("current")) {
|
||||||
current.date = moment(data.current.dt, "X").utcOffset(data.timezone_offset / 60);
|
current.date = moment(data.current.dt, "X").utcOffset(data.timezone_offset / 60);
|
||||||
current.windSpeed = data.current.wind_speed;
|
current.windSpeed = data.current.wind_speed;
|
||||||
@ -295,7 +299,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
current.feelsLikeTemp = data.current.feels_like;
|
current.feelsLikeTemp = data.current.feels_like;
|
||||||
}
|
}
|
||||||
|
|
||||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
// get hourly weather, if requested
|
// get hourly weather, if requested
|
||||||
const hours = [];
|
const hours = [];
|
||||||
@ -331,7 +335,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hours.push(weather);
|
hours.push(weather);
|
||||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,7 +374,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
days.push(weather);
|
days.push(weather);
|
||||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
309
modules/default/weather/providers/smhi.js
Normal file
309
modules/default/weather/providers/smhi.js
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
/* global WeatherProvider, WeatherObject, SunCalc */
|
||||||
|
|
||||||
|
/* Magic Mirror
|
||||||
|
* Module: Weather
|
||||||
|
* Provider: SMHI
|
||||||
|
*
|
||||||
|
* By BuXXi https://github.com/buxxi
|
||||||
|
* MIT Licensed
|
||||||
|
*
|
||||||
|
* This class is a provider for SMHI (Sweden only).
|
||||||
|
* Note that SMHI doesn't provide sunrise and sundown, use SunCalc to calculate it.
|
||||||
|
* Metric system is the only supported unit.
|
||||||
|
*/
|
||||||
|
WeatherProvider.register("smhi", {
|
||||||
|
providerName: "SMHI",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements method in interface for fetching current weather
|
||||||
|
*/
|
||||||
|
fetchCurrentWeather() {
|
||||||
|
this.fetchData(this.getURL())
|
||||||
|
.then((data) => {
|
||||||
|
let closest = this.getClosestToCurrentTime(data.timeSeries);
|
||||||
|
let coordinates = this.resolveCoordinates(data);
|
||||||
|
let weatherObject = this.convertWeatherDataToObject(closest, coordinates);
|
||||||
|
this.setFetchedLocation(`(${coordinates.lat},${coordinates.lon})`);
|
||||||
|
this.setCurrentWeather(weatherObject);
|
||||||
|
})
|
||||||
|
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||||
|
.finally(() => this.updateAvailable());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements method in interface for fetching a forecast.
|
||||||
|
* Handling hourly forecast would be easy as not grouping by day but it seems really specific for one weather provider for now.
|
||||||
|
*/
|
||||||
|
fetchWeatherForecast() {
|
||||||
|
this.fetchData(this.getURL())
|
||||||
|
.then((data) => {
|
||||||
|
let coordinates = this.resolveCoordinates(data);
|
||||||
|
let weatherObjects = this.convertWeatherDataGroupedByDay(data.timeSeries, coordinates);
|
||||||
|
this.setFetchedLocation(`(${coordinates.lat},${coordinates.lon})`);
|
||||||
|
this.setWeatherForecast(weatherObjects);
|
||||||
|
})
|
||||||
|
.catch((error) => Log.error("Could not load data: " + error.message))
|
||||||
|
.finally(() => this.updateAvailable());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides method for setting config with checks for the precipitationValue being unset or invalid
|
||||||
|
*
|
||||||
|
* @param config
|
||||||
|
*/
|
||||||
|
setConfig(config) {
|
||||||
|
this.config = config;
|
||||||
|
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) == -1) {
|
||||||
|
console.log("invalid or not set: " + config.precipitationValue);
|
||||||
|
config.precipitationValue = "pmedian";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Of all the times returned find out which one is closest to the current time, should be the first if the data isn't old.
|
||||||
|
*
|
||||||
|
* @param times
|
||||||
|
*/
|
||||||
|
getClosestToCurrentTime(times) {
|
||||||
|
let now = moment();
|
||||||
|
let minDiff = undefined;
|
||||||
|
for (time of times) {
|
||||||
|
let diff = Math.abs(moment(time.validTime).diff(now));
|
||||||
|
if (!minDiff || diff < Math.abs(moment(minDiff.validTime).diff(now))) {
|
||||||
|
minDiff = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minDiff;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the forecast url for the configured coordinates
|
||||||
|
*/
|
||||||
|
getURL() {
|
||||||
|
let lon = this.config.lon;
|
||||||
|
let lat = this.config.lat;
|
||||||
|
return `https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/${lon}/lat/${lat}/data.json`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the returned data into a WeatherObject with required properties set for both current weather and forecast.
|
||||||
|
* The returned units is always in metric system.
|
||||||
|
* Requires coordinates to determine if its daytime or nighttime to know which icon to use and also to set sunrise and sunset.
|
||||||
|
*
|
||||||
|
* @param weatherData
|
||||||
|
* @param coordinates
|
||||||
|
* @param weatherData
|
||||||
|
* @param coordinates
|
||||||
|
*/
|
||||||
|
convertWeatherDataToObject(weatherData, coordinates) {
|
||||||
|
let currentWeather = new WeatherObject("metric", "metric", "metric"); //Weather data is only for Sweden and nobody in Sweden would use imperial
|
||||||
|
|
||||||
|
currentWeather.date = moment(weatherData.validTime);
|
||||||
|
let times = SunCalc.getTimes(currentWeather.date.toDate(), coordinates.lat, coordinates.lon);
|
||||||
|
currentWeather.sunrise = moment(times.sunrise, "X");
|
||||||
|
currentWeather.sunset = moment(times.sunset, "X");
|
||||||
|
currentWeather.humidity = this.paramValue(weatherData, "r");
|
||||||
|
currentWeather.temperature = this.paramValue(weatherData, "t");
|
||||||
|
currentWeather.windSpeed = this.paramValue(weatherData, "ws");
|
||||||
|
currentWeather.windDirection = this.paramValue(weatherData, "wd");
|
||||||
|
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), this.isDayTime(currentWeather));
|
||||||
|
|
||||||
|
//Determine the precipitation amount and category and update the weatherObject with it, the valuetype to use can be configured or uses median as default.
|
||||||
|
let precipitationValue = this.paramValue(weatherData, this.config.precipitationValue);
|
||||||
|
switch (this.paramValue(weatherData, "pcat")) {
|
||||||
|
// 0 = No precipitation
|
||||||
|
case 1: // Snow
|
||||||
|
currentWeather.snow += precipitationValue;
|
||||||
|
currentWeather.precipitation += precipitationValue;
|
||||||
|
break;
|
||||||
|
case 2: // Snow and rain, treat it as 50/50 snow and rain
|
||||||
|
currentWeather.snow += precipitationValue / 2;
|
||||||
|
currentWeather.rain += precipitationValue / 2;
|
||||||
|
currentWeather.precipitation += precipitationValue;
|
||||||
|
break;
|
||||||
|
case 3: // Rain
|
||||||
|
case 4: // Drizzle
|
||||||
|
case 5: // Freezing rain
|
||||||
|
case 6: // Freezing drizzle
|
||||||
|
currentWeather.rain += precipitationValue;
|
||||||
|
currentWeather.precipitation += precipitationValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentWeather;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes all of the data points and converts it to one WeatherObject per day.
|
||||||
|
*
|
||||||
|
* @param allWeatherData
|
||||||
|
* @param coordinates
|
||||||
|
* @param allWeatherData
|
||||||
|
* @param coordinates
|
||||||
|
*/
|
||||||
|
convertWeatherDataGroupedByDay(allWeatherData, coordinates) {
|
||||||
|
var currentWeather;
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
let allWeatherObjects = this.fillInGaps(allWeatherData).map((weatherData) => this.convertWeatherDataToObject(weatherData, coordinates));
|
||||||
|
var dayWeatherTypes = [];
|
||||||
|
|
||||||
|
for (weatherObject of allWeatherObjects) {
|
||||||
|
//If its the first object or if a day change we need to reset the summary object
|
||||||
|
if (!currentWeather || !currentWeather.date.isSame(weatherObject.date, "day")) {
|
||||||
|
currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||||
|
dayWeatherTypes = [];
|
||||||
|
currentWeather.date = weatherObject.date;
|
||||||
|
currentWeather.minTemperature = Infinity;
|
||||||
|
currentWeather.maxTemperature = -Infinity;
|
||||||
|
currentWeather.snow = 0;
|
||||||
|
currentWeather.rain = 0;
|
||||||
|
currentWeather.precipitation = 0;
|
||||||
|
result.push(currentWeather);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Keep track of what icons has been used for each hour of daytime and use the middle one for the forecast
|
||||||
|
if (this.isDayTime(weatherObject)) {
|
||||||
|
dayWeatherTypes.push(weatherObject.weatherType);
|
||||||
|
}
|
||||||
|
if (dayWeatherTypes.length > 0) {
|
||||||
|
currentWeather.weatherType = dayWeatherTypes[Math.floor(dayWeatherTypes.length / 2)];
|
||||||
|
} else {
|
||||||
|
currentWeather.weatherType = weatherObject.weatherType;
|
||||||
|
}
|
||||||
|
|
||||||
|
//All other properties is either a sum, min or max of each hour
|
||||||
|
currentWeather.minTemperature = Math.min(currentWeather.minTemperature, weatherObject.temperature);
|
||||||
|
currentWeather.maxTemperature = Math.max(currentWeather.maxTemperature, weatherObject.temperature);
|
||||||
|
currentWeather.snow += weatherObject.snow;
|
||||||
|
currentWeather.rain += weatherObject.rain;
|
||||||
|
currentWeather.precipitation += weatherObject.precipitation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve coordinates from the response data (probably preferably to use this if it's not matching the config values exactly)
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
resolveCoordinates(data) {
|
||||||
|
return { lat: data.geometry.coordinates[0][1], lon: data.geometry.coordinates[0][0] };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the weatherObject is at dayTime.
|
||||||
|
*
|
||||||
|
* @param weatherObject
|
||||||
|
*/
|
||||||
|
isDayTime(weatherObject) {
|
||||||
|
return weatherObject.date.isBetween(weatherObject.sunrise, weatherObject.sunset, undefined, "[]");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The distance between the data points is increasing in the data the more distant the prediction is.
|
||||||
|
* Find these gaps and fill them with the previous hours data to make the data returned a complete set.
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
fillInGaps(data) {
|
||||||
|
let result = [];
|
||||||
|
for (var i = 1; i < data.length; i++) {
|
||||||
|
let to = moment(data[i].validTime);
|
||||||
|
let from = moment(data[i - 1].validTime);
|
||||||
|
let hours = moment.duration(to.diff(from)).asHours();
|
||||||
|
// For each hour add a datapoint but change the validTime
|
||||||
|
for (var j = 0; j < hours; j++) {
|
||||||
|
let current = Object.assign({}, data[i]);
|
||||||
|
current.validTime = from.clone().add(j, "hours").toISOString();
|
||||||
|
result.push(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to fetch a property from the returned data set.
|
||||||
|
* The returned values is an array with always one value in it.
|
||||||
|
*
|
||||||
|
* @param currentWeatherData
|
||||||
|
* @param name
|
||||||
|
* @param currentWeatherData
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
paramValue(currentWeatherData, name) {
|
||||||
|
return currentWeatherData.parameters.filter((p) => p.name == name).flatMap((p) => p.values)[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map the icon value from SHMI to an icon that MagicMirror understands.
|
||||||
|
* Uses different icons depending if its daytime or nighttime.
|
||||||
|
* SHMI's description of what the numeric value means is the comment after the case.
|
||||||
|
*
|
||||||
|
* @param input
|
||||||
|
* @param isDayTime
|
||||||
|
* @param input
|
||||||
|
* @param isDayTime
|
||||||
|
*/
|
||||||
|
convertWeatherType(input, isDayTime) {
|
||||||
|
switch (input) {
|
||||||
|
case 1:
|
||||||
|
return isDayTime ? "day-sunny" : "night-clear"; // Clear sky
|
||||||
|
case 2:
|
||||||
|
return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; //Nearly clear sky
|
||||||
|
case 3:
|
||||||
|
return isDayTime ? "day-cloudy" : "night-cloudy"; //Variable cloudiness
|
||||||
|
case 4:
|
||||||
|
return isDayTime ? "day-cloudy" : "night-cloudy"; //Halfclear sky
|
||||||
|
case 5:
|
||||||
|
return "cloudy"; //Cloudy sky
|
||||||
|
case 6:
|
||||||
|
return "cloudy"; //Overcast
|
||||||
|
case 7:
|
||||||
|
return "fog"; //Fog
|
||||||
|
case 8:
|
||||||
|
return "showers"; //Light rain showers
|
||||||
|
case 9:
|
||||||
|
return "showers"; //Moderate rain showers
|
||||||
|
case 10:
|
||||||
|
return "showers"; //Heavy rain showers
|
||||||
|
case 11:
|
||||||
|
return "thunderstorm"; //Thunderstorm
|
||||||
|
case 12:
|
||||||
|
return "sleet"; //Light sleet showers
|
||||||
|
case 13:
|
||||||
|
return "sleet"; //Moderate sleet showers
|
||||||
|
case 14:
|
||||||
|
return "sleet"; //Heavy sleet showers
|
||||||
|
case 15:
|
||||||
|
return "snow"; //Light snow showers
|
||||||
|
case 16:
|
||||||
|
return "snow"; //Moderate snow showers
|
||||||
|
case 17:
|
||||||
|
return "snow"; //Heavy snow showers
|
||||||
|
case 18:
|
||||||
|
return "rain"; //Light rain
|
||||||
|
case 19:
|
||||||
|
return "rain"; //Moderate rain
|
||||||
|
case 20:
|
||||||
|
return "rain"; //Heavy rain
|
||||||
|
case 21:
|
||||||
|
return "thunderstorm"; //Thunder
|
||||||
|
case 22:
|
||||||
|
return "sleet"; // Light sleet
|
||||||
|
case 23:
|
||||||
|
return "sleet"; //Moderate sleet
|
||||||
|
case 24:
|
||||||
|
return "sleet"; // Heavy sleet
|
||||||
|
case 25:
|
||||||
|
return "snow"; // Light snowfall
|
||||||
|
case 26:
|
||||||
|
return "snow"; //Moderate snowfall
|
||||||
|
case 27:
|
||||||
|
return "snow"; //Heavy snowfall
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -73,7 +73,7 @@ WeatherProvider.register("ukmetoffice", {
|
|||||||
* Generate a WeatherObject based on currentWeatherInformation
|
* Generate a WeatherObject based on currentWeatherInformation
|
||||||
*/
|
*/
|
||||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
// data times are always UTC
|
// data times are always UTC
|
||||||
let nowUtc = moment.utc();
|
let nowUtc = moment.utc();
|
||||||
@ -124,7 +124,7 @@ WeatherProvider.register("ukmetoffice", {
|
|||||||
// loop round the (5) periods getting the data
|
// loop round the (5) periods getting the data
|
||||||
// for each period array, Day is [0], Night is [1]
|
// for each period array, Day is [0], Night is [1]
|
||||||
for (var j in forecasts.SiteRep.DV.Location.Period) {
|
for (var j in forecasts.SiteRep.DV.Location.Period) {
|
||||||
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
// data times are always UTC
|
// data times are always UTC
|
||||||
const dateStr = forecasts.SiteRep.DV.Location.Period[j].value;
|
const dateStr = forecasts.SiteRep.DV.Location.Period[j].value;
|
||||||
@ -208,10 +208,10 @@ WeatherProvider.register("ukmetoffice", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Convert wind speed (from mph) if required
|
* Convert wind speed (from mph to m/s or km/h) if required
|
||||||
*/
|
*/
|
||||||
convertWindSpeed(windInMph) {
|
convertWindSpeed(windInMph) {
|
||||||
return this.windUnits === "metric" ? windInMph * 2.23694 : windInMph;
|
return this.windUnits === "metric" ? (this.useKmh ? windInMph * 1.60934 : windInMph / 2.23694) : windInMph;
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -108,7 +108,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||||||
|
|
||||||
// Create a WeatherObject using current weather data (data for the current hour)
|
// Create a WeatherObject using current weather data (data for the current hour)
|
||||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
// Extract the actual forecasts
|
// Extract the actual forecasts
|
||||||
let forecastDataHours = currentWeatherData.features[0].properties.timeSeries;
|
let forecastDataHours = currentWeatherData.features[0].properties.timeSeries;
|
||||||
@ -189,7 +189,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||||||
|
|
||||||
// Go through each day in the forecasts
|
// Go through each day in the forecasts
|
||||||
for (day in forecastDataDays) {
|
for (day in forecastDataDays) {
|
||||||
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
// Get date of forecast
|
// Get date of forecast
|
||||||
let forecastDate = moment.utc(forecastDataDays[day].time);
|
let forecastDate = moment.utc(forecastDataDays[day].time);
|
||||||
@ -254,7 +254,7 @@ WeatherProvider.register("ukmetofficedatahub", {
|
|||||||
return windInMpS;
|
return windInMpS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.windUnits == "kph" || this.config.windUnits == "metric") {
|
if (this.config.windUnits == "kph" || this.config.windUnits == "metric" || this.config.useKmh) {
|
||||||
return windInMpS * 3.6;
|
return windInMpS * 3.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
181
modules/default/weather/providers/weatherbit.js
Normal file
181
modules/default/weather/providers/weatherbit.js
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
/* global WeatherProvider, WeatherObject */
|
||||||
|
|
||||||
|
/* Magic Mirror
|
||||||
|
* Module: Weather
|
||||||
|
* Provider: Weatherbit
|
||||||
|
*
|
||||||
|
* By Andrew Pometti
|
||||||
|
* MIT Licensed
|
||||||
|
*
|
||||||
|
* This class is a provider for Weatherbit, based on Nicholas Hubbard's class for Dark Sky & Vince Peri's class for Weather.gov.
|
||||||
|
*/
|
||||||
|
WeatherProvider.register("weatherbit", {
|
||||||
|
// Set the name of the provider.
|
||||||
|
// Not strictly required, but helps for debugging.
|
||||||
|
providerName: "Weatherbit",
|
||||||
|
|
||||||
|
units: {
|
||||||
|
imperial: "I",
|
||||||
|
metric: "M"
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchedLocation: function () {
|
||||||
|
return this.fetchedLocationName || "";
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchCurrentWeather() {
|
||||||
|
this.fetchData(this.getUrl())
|
||||||
|
.then((data) => {
|
||||||
|
if (!data || !data.data[0] || typeof data.data[0].temp === "undefined") {
|
||||||
|
// No usable data?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentWeather = this.generateWeatherDayFromCurrentWeather(data);
|
||||||
|
this.setCurrentWeather(currentWeather);
|
||||||
|
})
|
||||||
|
.catch(function (request) {
|
||||||
|
Log.error("Could not load data ... ", request);
|
||||||
|
})
|
||||||
|
.finally(() => this.updateAvailable());
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchWeatherForecast() {
|
||||||
|
this.fetchData(this.getUrl())
|
||||||
|
.then((data) => {
|
||||||
|
if (!data || !data.data) {
|
||||||
|
// No usable data?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forecast = this.generateWeatherObjectsFromForecast(data.data);
|
||||||
|
this.setWeatherForecast(forecast);
|
||||||
|
|
||||||
|
this.fetchedLocationName = data.city_name + ", " + data.state_code;
|
||||||
|
})
|
||||||
|
.catch(function (request) {
|
||||||
|
Log.error("Could not load data ... ", request);
|
||||||
|
})
|
||||||
|
.finally(() => this.updateAvailable());
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create a URL from the config and base URL.
|
||||||
|
getUrl() {
|
||||||
|
const units = this.units[this.config.units] || "auto";
|
||||||
|
return `${this.config.apiBase}${this.config.weatherEndpoint}?lat=${this.config.lat}&lon=${this.config.lon}&units=${units}&key=${this.config.apiKey}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Implement WeatherDay generator.
|
||||||
|
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||||
|
//Calculate TZ Offset and invert to convert Sunrise/Sunset times to Local
|
||||||
|
const d = new Date();
|
||||||
|
let tzOffset = d.getTimezoneOffset();
|
||||||
|
tzOffset = tzOffset * -1;
|
||||||
|
|
||||||
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||||
|
|
||||||
|
currentWeather.date = moment(currentWeatherData.data[0].ts, "X");
|
||||||
|
currentWeather.humidity = parseFloat(currentWeatherData.data[0].rh);
|
||||||
|
currentWeather.temperature = parseFloat(currentWeatherData.data[0].temp);
|
||||||
|
currentWeather.windSpeed = parseFloat(currentWeatherData.data[0].wind_spd);
|
||||||
|
currentWeather.windDirection = currentWeatherData.data[0].wind_dir;
|
||||||
|
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.data[0].weather.icon);
|
||||||
|
Log.log("Wx Icon: " + currentWeatherData.data[0].weather.icon);
|
||||||
|
currentWeather.sunrise = moment(currentWeatherData.data[0].sunrise, "HH:mm").add(tzOffset, "m");
|
||||||
|
currentWeather.sunset = moment(currentWeatherData.data[0].sunset, "HH:mm").add(tzOffset, "m");
|
||||||
|
|
||||||
|
this.fetchedLocationName = currentWeatherData.data[0].city_name + ", " + currentWeatherData.data[0].state_code;
|
||||||
|
|
||||||
|
return currentWeather;
|
||||||
|
},
|
||||||
|
|
||||||
|
generateWeatherObjectsFromForecast(forecasts) {
|
||||||
|
const days = [];
|
||||||
|
|
||||||
|
for (const forecast of forecasts) {
|
||||||
|
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||||
|
|
||||||
|
weather.date = moment(forecast.datetime, "YYYY-MM-DD");
|
||||||
|
weather.minTemperature = forecast.min_temp;
|
||||||
|
weather.maxTemperature = forecast.max_temp;
|
||||||
|
weather.precipitation = forecast.precip;
|
||||||
|
weather.weatherType = this.convertWeatherType(forecast.weather.icon);
|
||||||
|
|
||||||
|
days.push(weather);
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Map icons from Dark Sky to our icons.
|
||||||
|
convertWeatherType(weatherType) {
|
||||||
|
const weatherTypes = {
|
||||||
|
t01d: "day-thunderstorm",
|
||||||
|
t01n: "night-alt-thunderstorm",
|
||||||
|
t02d: "day-thunderstorm",
|
||||||
|
t02n: "night-alt-thunderstorm",
|
||||||
|
t03d: "thunderstorm",
|
||||||
|
t03n: "thunderstorm",
|
||||||
|
t04d: "day-thunderstorm",
|
||||||
|
t04n: "night-alt-thunderstorm",
|
||||||
|
t05d: "day-sleet-storm",
|
||||||
|
t05n: "night-alt-sleet-storm",
|
||||||
|
d01d: "day-sprinkle",
|
||||||
|
d01n: "night-alt-sprinkle",
|
||||||
|
d02d: "day-sprinkle",
|
||||||
|
d02n: "night-alt-sprinkle",
|
||||||
|
d03d: "day-shower",
|
||||||
|
d03n: "night-alt-shower",
|
||||||
|
r01d: "day-shower",
|
||||||
|
r01n: "night-alt-shower",
|
||||||
|
r02d: "day-rain",
|
||||||
|
r02n: "night-alt-rain",
|
||||||
|
r03d: "day-rain",
|
||||||
|
r03n: "night-alt-rain",
|
||||||
|
r04d: "day-sprinkle",
|
||||||
|
r04n: "night-alt-sprinkle",
|
||||||
|
r05d: "day-shower",
|
||||||
|
r05n: "night-alt-shower",
|
||||||
|
r06d: "day-shower",
|
||||||
|
r06n: "night-alt-shower",
|
||||||
|
f01d: "day-sleet",
|
||||||
|
f01n: "night-alt-sleet",
|
||||||
|
s01d: "day-snow",
|
||||||
|
s01n: "night-alt-snow",
|
||||||
|
s02d: "day-snow-wind",
|
||||||
|
s02n: "night-alt-snow-wind",
|
||||||
|
s03d: "snowflake-cold",
|
||||||
|
s03n: "snowflake-cold",
|
||||||
|
s04d: "day-rain-mix",
|
||||||
|
s04n: "night-alt-rain-mix",
|
||||||
|
s05d: "day-sleet",
|
||||||
|
s05n: "night-alt-sleet",
|
||||||
|
s06d: "day-snow",
|
||||||
|
s06n: "night-alt-snow",
|
||||||
|
a01d: "day-haze",
|
||||||
|
a01n: "dust",
|
||||||
|
a02d: "smoke",
|
||||||
|
a02n: "smoke",
|
||||||
|
a03d: "day-haze",
|
||||||
|
a03n: "dust",
|
||||||
|
a04d: "dust",
|
||||||
|
a04n: "dust",
|
||||||
|
a05d: "day-fog",
|
||||||
|
a05n: "night-fog",
|
||||||
|
a06d: "fog",
|
||||||
|
a06n: "fog",
|
||||||
|
c01d: "day-sunny",
|
||||||
|
c01n: "night-clear",
|
||||||
|
c02d: "day-sunny-overcast",
|
||||||
|
c02n: "night-alt-partly-cloudy",
|
||||||
|
c03d: "day-cloudy",
|
||||||
|
c03n: "night-alt-cloudy",
|
||||||
|
c04d: "cloudy",
|
||||||
|
c04n: "cloudy",
|
||||||
|
u00d: "rain-mix",
|
||||||
|
u00n: "rain-mix"
|
||||||
|
};
|
||||||
|
|
||||||
|
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||||
|
}
|
||||||
|
});
|
@ -131,11 +131,11 @@ WeatherProvider.register("weathergov", {
|
|||||||
* ... object needs data in units based on config!
|
* ... object needs data in units based on config!
|
||||||
*/
|
*/
|
||||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||||
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
currentWeather.date = moment(currentWeatherData.timestamp);
|
currentWeather.date = moment(currentWeatherData.timestamp);
|
||||||
currentWeather.temperature = this.convertTemp(currentWeatherData.temperature.value);
|
currentWeather.temperature = this.convertTemp(currentWeatherData.temperature.value);
|
||||||
currentWeather.windSpeed = this.covertSpeed(currentWeatherData.windSpeed.value);
|
currentWeather.windSpeed = this.convertSpeed(currentWeatherData.windSpeed.value);
|
||||||
currentWeather.windDirection = currentWeatherData.windDirection.value;
|
currentWeather.windDirection = currentWeatherData.windDirection.value;
|
||||||
currentWeather.minTemperature = this.convertTemp(currentWeatherData.minTemperatureLast24Hours.value);
|
currentWeather.minTemperature = this.convertTemp(currentWeatherData.minTemperatureLast24Hours.value);
|
||||||
currentWeather.maxTemperature = this.convertTemp(currentWeatherData.maxTemperatureLast24Hours.value);
|
currentWeather.maxTemperature = this.convertTemp(currentWeatherData.maxTemperatureLast24Hours.value);
|
||||||
@ -179,7 +179,7 @@ WeatherProvider.register("weathergov", {
|
|||||||
let maxTemp = [];
|
let maxTemp = [];
|
||||||
// variable for date
|
// variable for date
|
||||||
let date = "";
|
let date = "";
|
||||||
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
weather.precipitation = 0;
|
weather.precipitation = 0;
|
||||||
|
|
||||||
for (const forecast of forecasts) {
|
for (const forecast of forecasts) {
|
||||||
@ -191,7 +191,7 @@ WeatherProvider.register("weathergov", {
|
|||||||
// push weather information to days array
|
// push weather information to days array
|
||||||
days.push(weather);
|
days.push(weather);
|
||||||
// create new weather-object
|
// create new weather-object
|
||||||
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
|
||||||
|
|
||||||
minTemp = [];
|
minTemp = [];
|
||||||
maxTemp = [];
|
maxTemp = [];
|
||||||
@ -238,12 +238,16 @@ WeatherProvider.register("weathergov", {
|
|||||||
return temp;
|
return temp;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// conversion to mph
|
// conversion to mph or kmh
|
||||||
covertSpeed(metSec) {
|
convertSpeed(metSec) {
|
||||||
if (this.config.windUnits === "imperial") {
|
if (this.config.windUnits === "imperial") {
|
||||||
return metSec * 2.23694;
|
return metSec * 2.23694;
|
||||||
} else {
|
} else {
|
||||||
return metSec;
|
if (this.config.useKmh) {
|
||||||
|
return metSec * 3.6;
|
||||||
|
} else {
|
||||||
|
return metSec;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// conversion to inches
|
// conversion to inches
|
||||||
|
@ -12,16 +12,14 @@ Module.register("weather", {
|
|||||||
weatherProvider: "openweathermap",
|
weatherProvider: "openweathermap",
|
||||||
roundTemp: false,
|
roundTemp: false,
|
||||||
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
|
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
|
||||||
|
|
||||||
lat: 0,
|
lat: 0,
|
||||||
lon: 0,
|
lon: 0,
|
||||||
location: false,
|
location: false,
|
||||||
locationID: false,
|
locationID: false,
|
||||||
units: config.units,
|
units: config.units,
|
||||||
|
useKmh: false,
|
||||||
tempUnits: config.units,
|
tempUnits: config.units,
|
||||||
windUnits: config.units,
|
windUnits: config.units,
|
||||||
|
|
||||||
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
updateInterval: 10 * 60 * 1000, // every 10 minutes
|
||||||
animationSpeed: 1000,
|
animationSpeed: 1000,
|
||||||
timeFormat: config.timeFormat,
|
timeFormat: config.timeFormat,
|
||||||
@ -41,20 +39,16 @@ Module.register("weather", {
|
|||||||
maxEntries: 5,
|
maxEntries: 5,
|
||||||
fade: true,
|
fade: true,
|
||||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
fadePoint: 0.25, // Start on 1/4th of the list.
|
||||||
|
|
||||||
initialLoadDelay: 0, // 0 seconds delay
|
initialLoadDelay: 0, // 0 seconds delay
|
||||||
retryDelay: 2500,
|
retryDelay: 2500,
|
||||||
|
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
apiSecret: "",
|
apiSecret: "",
|
||||||
apiVersion: "2.5",
|
apiVersion: "2.5",
|
||||||
apiBase: "https://api.openweathermap.org/data/", // TODO: this should not be part of the weather.js file, but should be contained in the openweatherprovider
|
apiBase: "https://api.openweathermap.org/data/", // TODO: this should not be part of the weather.js file, but should be contained in the openweatherprovider
|
||||||
weatherEndpoint: "/weather",
|
weatherEndpoint: "/weather",
|
||||||
|
|
||||||
appendLocationNameToHeader: true,
|
appendLocationNameToHeader: true,
|
||||||
calendarClass: "calendar",
|
calendarClass: "calendar",
|
||||||
tableClass: "small",
|
tableClass: "small",
|
||||||
|
|
||||||
onlyTemp: false,
|
onlyTemp: false,
|
||||||
showPrecipitationAmount: false,
|
showPrecipitationAmount: false,
|
||||||
colored: false,
|
colored: false,
|
||||||
|
@ -10,10 +10,11 @@
|
|||||||
* As soon as we start implementing the forecast, mode properties will be added.
|
* As soon as we start implementing the forecast, mode properties will be added.
|
||||||
*/
|
*/
|
||||||
class WeatherObject {
|
class WeatherObject {
|
||||||
constructor(units, tempUnits, windUnits) {
|
constructor(units, tempUnits, windUnits, useKmh) {
|
||||||
this.units = units;
|
this.units = units;
|
||||||
this.tempUnits = tempUnits;
|
this.tempUnits = tempUnits;
|
||||||
this.windUnits = windUnits;
|
this.windUnits = windUnits;
|
||||||
|
this.useKmh = useKmh;
|
||||||
this.date = null;
|
this.date = null;
|
||||||
this.windSpeed = null;
|
this.windSpeed = null;
|
||||||
this.windDirection = null;
|
this.windDirection = null;
|
||||||
@ -67,7 +68,7 @@ class WeatherObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beaufortWindSpeed() {
|
beaufortWindSpeed() {
|
||||||
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
|
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : this.useKmh ? this.windSpeed : (this.windSpeed * 60 * 60) / 1000;
|
||||||
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
||||||
for (const [index, speed] of speeds.entries()) {
|
for (const [index, speed] of speeds.entries()) {
|
||||||
if (speed > windInKmh) {
|
if (speed > windInKmh) {
|
||||||
@ -77,6 +78,11 @@ class WeatherObject {
|
|||||||
return 12;
|
return 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kmhWindSpeed() {
|
||||||
|
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
|
||||||
|
return windInKmh;
|
||||||
|
}
|
||||||
|
|
||||||
nextSunAction() {
|
nextSunAction() {
|
||||||
return moment().isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
|
return moment().isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
|
||||||
}
|
}
|
||||||
|
@ -371,10 +371,10 @@ Module.register("weatherforecast", {
|
|||||||
var hour;
|
var hour;
|
||||||
if (forecast.dt_txt) {
|
if (forecast.dt_txt) {
|
||||||
day = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
|
day = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
|
||||||
hour = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("H");
|
hour = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").toDate().getHours();
|
||||||
} else {
|
} else {
|
||||||
day = moment(forecast.dt, "X").format("ddd");
|
day = moment(forecast.dt, "X").format("ddd");
|
||||||
hour = moment(forecast.dt, "X").format("H");
|
hour = moment(forecast.dt, "X").toDate().getHours();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (day !== lastDay) {
|
if (day !== lastDay) {
|
||||||
@ -385,7 +385,6 @@ Module.register("weatherforecast", {
|
|||||||
minTemp: this.roundValue(forecast.temp.min),
|
minTemp: this.roundValue(forecast.temp.min),
|
||||||
rain: this.processRain(forecast, forecastList)
|
rain: this.processRain(forecast, forecastList)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.forecast.push(forecastData);
|
this.forecast.push(forecastData);
|
||||||
lastDay = day;
|
lastDay = day;
|
||||||
|
|
||||||
|
1206
package-lock.json
generated
1206
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@ -44,22 +44,22 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"danger": "^10.5.1",
|
"danger": "^10.5.4",
|
||||||
"eslint-config-prettier": "^6.15.0",
|
"eslint-config-prettier": "^7.0.0",
|
||||||
"eslint-plugin-jsdoc": "^30.7.7",
|
"eslint-plugin-jsdoc": "^30.7.8",
|
||||||
"eslint-plugin-prettier": "^3.1.4",
|
"eslint-plugin-prettier": "^3.2.0",
|
||||||
"express-basic-auth": "^1.2.0",
|
"express-basic-auth": "^1.2.0",
|
||||||
"husky": "^4.3.0",
|
"husky": "^4.3.5",
|
||||||
"jsdom": "^16.4.0",
|
"jsdom": "^16.4.0",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"mocha": "^8.2.1",
|
"mocha": "^8.2.1",
|
||||||
"mocha-each": "^2.0.1",
|
"mocha-each": "^2.0.1",
|
||||||
"mocha-logger": "^1.0.7",
|
"mocha-logger": "^1.0.7",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^15.1.0",
|
||||||
"prettier": "^2.1.2",
|
"prettier": "^2.2.1",
|
||||||
"pretty-quick": "^3.1.0",
|
"pretty-quick": "^3.1.0",
|
||||||
"spectron": "^10.0.1",
|
"spectron": "^10.0.1",
|
||||||
"stylelint": "^13.7.2",
|
"stylelint": "^13.8.0",
|
||||||
"stylelint-config-prettier": "^8.0.2",
|
"stylelint-config-prettier": "^8.0.2",
|
||||||
"stylelint-config-standard": "^20.0.0",
|
"stylelint-config-standard": "^20.0.0",
|
||||||
"stylelint-prettier": "^1.1.2"
|
"stylelint-prettier": "^1.1.2"
|
||||||
@ -69,23 +69,23 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"console-stamp": "^0.2.9",
|
"console-stamp": "^3.0.0-rc4.2",
|
||||||
"electron": "^8.5.3",
|
"electron": "^8.5.3",
|
||||||
"eslint": "^7.13.0",
|
"eslint": "^7.15.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-ipfilter": "^1.1.2",
|
"express-ipfilter": "^1.1.2",
|
||||||
"feedme": "^1.2.0",
|
"feedme": "^2.0.2",
|
||||||
"helmet": "^4.2.0",
|
"helmet": "^4.2.0",
|
||||||
"ical": "^0.8.0",
|
"ical": "^0.8.0",
|
||||||
"iconv-lite": "^0.6.2",
|
"iconv-lite": "^0.6.2",
|
||||||
"module-alias": "^2.2.2",
|
"module-alias": "^2.2.2",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"node-ical": "^0.12.3",
|
"node-ical": "^0.12.4",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"rrule": "^2.6.6",
|
"rrule": "^2.6.6",
|
||||||
"rrule-alt": "^2.2.8",
|
"rrule-alt": "^2.2.8",
|
||||||
"simple-git": "^2.21.0",
|
"simple-git": "^2.26.0",
|
||||||
"socket.io": "^2.3.0",
|
"socket.io": "^3.0.4",
|
||||||
"valid-url": "^1.0.9"
|
"valid-url": "^1.0.9"
|
||||||
},
|
},
|
||||||
"_moduleAliases": {
|
"_moduleAliases": {
|
||||||
|
@ -22,11 +22,11 @@ let config = {
|
|||||||
config: {
|
config: {
|
||||||
calendars: [
|
calendars: [
|
||||||
{
|
{
|
||||||
|
maximumEntries: 4,
|
||||||
|
maximumNumberOfDays: 10000,
|
||||||
symbol: "birthday-cake",
|
symbol: "birthday-cake",
|
||||||
fullDaySymbol: "calendar-day",
|
fullDaySymbol: "calendar-day",
|
||||||
recurringSymbol: "undo",
|
recurringSymbol: "undo",
|
||||||
maximumEntries: 4,
|
|
||||||
maximumNumberOfDays: 10000,
|
|
||||||
url: "http://localhost:8080/tests/configs/data/calendar_test_icons.ics"
|
url: "http://localhost:8080/tests/configs/data/calendar_test_icons.ics"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -9,28 +9,28 @@
|
|||||||
|
|
||||||
"WEEK": "{weekNumber} savaitė",
|
"WEEK": "{weekNumber} savaitė",
|
||||||
|
|
||||||
"N": "N",
|
"N": "Š",
|
||||||
"NNE": "NNE",
|
"NNE": "ŠŠR",
|
||||||
"NE": "NE",
|
"NE": "ŠR",
|
||||||
"ENE": "ENE",
|
"ENE": "RŠR",
|
||||||
"E": "E",
|
"E": "R",
|
||||||
"ESE": "ESE",
|
"ESE": "RPR",
|
||||||
"SE": "SE",
|
"SE": "PR",
|
||||||
"SSE": "SSE",
|
"SSE": "PPR",
|
||||||
"S": "S",
|
"S": "P",
|
||||||
"SSW": "SSW",
|
"SSW": "PPV",
|
||||||
"SW": "SW",
|
"SW": "PV",
|
||||||
"WSW": "WSW",
|
"WSW": "VPV",
|
||||||
"W": "W",
|
"W": "V",
|
||||||
"WNW": "WNW",
|
"WNW": "VŠV",
|
||||||
"NW": "NW",
|
"NW": "ŠV",
|
||||||
"NNW": "NNW",
|
"NNW": "ŠŠV",
|
||||||
|
|
||||||
"UPDATE_NOTIFICATION": "Galimas MagicMirror² naujinimas.",
|
"UPDATE_NOTIFICATION": "Galimas MagicMirror² naujinimas.",
|
||||||
"UPDATE_NOTIFICATION_MODULE": "Galimas {MODULE_NAME} naujinimas.",
|
"UPDATE_NOTIFICATION_MODULE": "Galimas {MODULE_NAME} naujinimas.",
|
||||||
"UPDATE_INFO_SINGLE": "Šis įdiegimas atsilieka {COMMIT_COUNT} įsipareigojimu {BRANCH_NAME} šakoje.",
|
"UPDATE_INFO_SINGLE": "Šis įdiegimas atsilieka {COMMIT_COUNT} įsipareigojimu {BRANCH_NAME} šakoje.",
|
||||||
"UPDATE_INFO_MULTIPLE": "Šis įdiegimas atsilieka {COMMIT_COUNT} įsipareigojimais {BRANCH_NAME} šakoje.",
|
"UPDATE_INFO_MULTIPLE": "Šis įdiegimas atsilieka {COMMIT_COUNT} įsipareigojimais {BRANCH_NAME} šakoje.",
|
||||||
|
|
||||||
"FEELS": "Jaučiasi kaip",
|
"FEELS": "Jutiminė temp.",
|
||||||
"PRECIP": "Krituliai"
|
"PRECIP": "Krituliai"
|
||||||
}
|
}
|
||||||
|
37
translations/ps.json
Normal file
37
translations/ps.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"LOADING": "پیلېدل",
|
||||||
|
|
||||||
|
"TODAY": "نن",
|
||||||
|
"TOMORROW": "سبا",
|
||||||
|
"DAYAFTERTOMORROW": "بل سبا",
|
||||||
|
"RUNNING": "روان",
|
||||||
|
"EMPTY": "تش",
|
||||||
|
|
||||||
|
"WEEK": "{weekNumber}. اونۍ",
|
||||||
|
|
||||||
|
"N": "N",
|
||||||
|
"NNE": "NNO",
|
||||||
|
"NE": "NO",
|
||||||
|
"ENE": "ONO",
|
||||||
|
"E": "O",
|
||||||
|
"ESE": "OSO",
|
||||||
|
"SE": "SO",
|
||||||
|
"SSE": "SSO",
|
||||||
|
"S": "S",
|
||||||
|
"SSW": "SSW",
|
||||||
|
"SW": "SW",
|
||||||
|
"WSW": "WSW",
|
||||||
|
"W": "W",
|
||||||
|
"WNW": "WNW",
|
||||||
|
"NW": "NW",
|
||||||
|
"NNW": "NNW",
|
||||||
|
|
||||||
|
"MODULE_CONFIG_CHANGED": "د {MODULE_NAME} بڼی تغیر کړی دی. \n هیله دی چی اسناد و ګوری!",
|
||||||
|
|
||||||
|
"UPDATE_NOTIFICATION": "د MagicMirror² نوې نسخه سته ",
|
||||||
|
"UPDATE_NOTIFICATION_MODULE": "د {MODULE_NAME} نوی نسخه سته",
|
||||||
|
"UPDATE_INFO_SINGLE": "اوسنی برخه {COMMIT_COUNT} د {BRANCH_NAME} څخه وروسته پاته ده",
|
||||||
|
"UPDATE_INFO_MULTIPLE": "اوسنی برخه {COMMIT_COUNT} د {BRANCH_NAME} څخه وروسته پاته ده",
|
||||||
|
|
||||||
|
"FEELS": "حس کېږی"
|
||||||
|
}
|
18
vendor/package-lock.json
generated
vendored
18
vendor/package-lock.json
generated
vendored
@ -4,9 +4,9 @@
|
|||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": {
|
"@fortawesome/fontawesome-free": {
|
||||||
"version": "5.14.0",
|
"version": "5.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz",
|
||||||
"integrity": "sha512-OfdMsF+ZQgdKHP9jUbmDcRrP0eX90XXrsXIdyjLbkmSBzmMXPABB8eobUJtivaupucYaByz6WNe1PI1JuYm3qA=="
|
"integrity": "sha512-OEdH7SyC1suTdhBGW91/zBfR6qaIhThbcN8PUXtXilY4GYnSBbVqOntdHbC1vXwsDnX0Qix2m2+DSU1J51ybOQ=="
|
||||||
},
|
},
|
||||||
"a-sync-waterfall": {
|
"a-sync-waterfall": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@ -119,14 +119,14 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"moment": {
|
"moment": {
|
||||||
"version": "2.28.0",
|
"version": "2.29.1",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||||
"integrity": "sha512-Z5KOjYmnHyd/ukynmFd/WwyXHd7L4J9vTI/nn5Ap9AVUgaAE15VvQ9MOGmJJygEUklupqIrFnor/tjTwRU+tQw=="
|
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
|
||||||
},
|
},
|
||||||
"moment-timezone": {
|
"moment-timezone": {
|
||||||
"version": "0.5.31",
|
"version": "0.5.32",
|
||||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.31.tgz",
|
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.32.tgz",
|
||||||
"integrity": "sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==",
|
"integrity": "sha512-Z8QNyuQHQAmWucp8Knmgei8YNo28aLjJq6Ma+jy1ZSpSk5nyfRT8xgUbSQvD2+2UajISfenndwvFuH3NGS+nvA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"moment": ">= 2.9.0"
|
"moment": ">= 2.9.0"
|
||||||
}
|
}
|
||||||
|
6
vendor/package.json
vendored
6
vendor/package.json
vendored
@ -10,9 +10,9 @@
|
|||||||
"url": "https://github.com/MichMich/MagicMirror/issues"
|
"url": "https://github.com/MichMich/MagicMirror/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||||
"moment": "^2.28.0",
|
"moment": "^2.29.1",
|
||||||
"moment-timezone": "^0.5.31",
|
"moment-timezone": "^0.5.32",
|
||||||
"nunjucks": "^3.2.2",
|
"nunjucks": "^3.2.2",
|
||||||
"suncalc": "^1.8.0",
|
"suncalc": "^1.8.0",
|
||||||
"weathericons": "^2.1.0"
|
"weathericons": "^2.1.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user