Release 2.24.0 (#3141)

Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Karsten Hassel <hassel@gmx.de>
Co-authored-by: Malte Hallström <46646495+SkySails@users.noreply.github.com>
Co-authored-by: Veeck <github@veeck.de>
Co-authored-by: veeck <michael@veeck.de>
Co-authored-by: dWoolridge <dwoolridge@charter.net>
Co-authored-by: Johan <jojjepersson@yahoo.se>
Co-authored-by: Dario Mratovich <dario_mratovich@hotmail.com>
Co-authored-by: Dario Mratovich <dario.mratovich@outlook.com>
Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com>
Co-authored-by: Naveen <172697+naveensrinivasan@users.noreply.github.com>
Co-authored-by: buxxi <buxxi@omfilm.net>
Co-authored-by: Thomas Hirschberger <47733292+Tom-Hirschberger@users.noreply.github.com>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com>
Co-authored-by: Dave Child <dave@addedbytes.com>
Co-authored-by: grenagit <46225780+grenagit@users.noreply.github.com>
Co-authored-by: Grena <grena@grenabox.fr>
Co-authored-by: Magnus Marthinsen <magmar@online.no>
Co-authored-by: Patrick <psieg@users.noreply.github.com>
Co-authored-by: Piotr Rajnisz <56397164+rajniszp@users.noreply.github.com>
Co-authored-by: Suthep Yonphimai <tomzt@users.noreply.github.com>
Co-authored-by: CarJem Generations (Carter Wallace) <cwallacecs@gmail.com>
Co-authored-by: Nicholas Fogal <nfogal.misc@gmail.com>
Co-authored-by: JakeBinney <126349119+JakeBinney@users.noreply.github.com>
Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com>
Co-authored-by: Oscar Björkman <17575446+oscarb@users.noreply.github.com>
Co-authored-by: Ismar Slomic <ismar@slomic.no>
Co-authored-by: Jørgen Veum-Wahlberg <jorgen.wahlberg@amedia.no>
Co-authored-by: Eddie Hung <6740044+eddiehung@users.noreply.github.com>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: bugsounet <bugsounet@bugsounet.fr>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Michael Teeuw 2023-07-01 21:17:31 +02:00 committed by GitHub
parent abe5c08a52
commit e87f50e64a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 2570 additions and 2437 deletions

View File

@ -24,6 +24,7 @@
"rules": {
"eqeqeq": "error",
"import/order": "error",
"no-param-reassign": "error",
"no-prototype-builtins": "off",
"no-throw-literal": "error",
"no-unused-vars": "off",

56
.gitattributes vendored Normal file
View File

@ -0,0 +1,56 @@
# .gitattributes snippet to force users to use same line endings for project.
#
# Handle line endings automatically for files detected as text
# and leave all files detected as binary untouched.
* text=auto
#
# The above will handle all files NOT found below
# https://help.github.com/articles/dealing-with-line-endings/
# https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes
# These files are text and should be normalized (Convert crlf => lf)
*.php text
*.css text
*.scss text
*.js text
*.json text
*.htm text
*.html text
*.xml text
*.txt text
*.ini text
*.inc text
*.pl text
*.rb text
*.py text
*.scm text
*.sql text
.htaccess text
*.sh text
Dockerfile* text
*.yml text
*.yaml text
*.md text
*.markdown text
# These files are binary and should be left untouched
# (binary is a macro for -text -diff)
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.mov binary
*.mp4 binary
*.mp3 binary
*.flv binary
*.fla binary
*.swf binary
*.gz binary
*.zip binary
*.7z binary
*.ttf binary
*.pyc binary

View File

@ -43,7 +43,7 @@ When submitting a new issue, please supply the following information:
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
**Node Version**: Make sure it's version 14 or later (recommended is 16).
**Node Version**: Make sure it's version 16 or later (recommended is 18).
**MagicMirror² Version**: Please let us know which version of MagicMirror² you are running. It can be found in the `package.json` file.

View File

@ -31,7 +31,7 @@ When submitting a new issue, please supply the following information:
**Platform**: Place your platform here... give us your web browser/Electron version _and_ your hardware (Raspberry Pi 2/3/4, Windows, Mac, Linux, System V UNIX).
**Node Version**: Make sure it's version 14 or later (recommended is 16).
**Node Version**: Make sure it's version 16 or later (recommended is 18).
**MagicMirror² Version**: Please let us know which version of MagicMirror² you are running. It can be found in the `package.json` file.

View File

@ -18,7 +18,7 @@ jobs:
timeout-minutes: 30
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
node-version: [16.x, 18.x, 20.x]
steps:
- name: "Checkout code"
uses: actions/checkout@v3
@ -27,11 +27,13 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- name: "Install dependencies and run tests"
- name: "Install dependencies"
run: |
npm run install-mm:dev
- name: "Run tests"
run: |
Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99
npm run install-mm:dev
touch css/custom.css
npm run test:prettier
npm run test:js

View File

@ -19,11 +19,13 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v3
- name: "Install dependencies and run coverage"
- name: "Install dependencies"
run: |
npm ci
- name: "Run coverage"
run: |
Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99
npm ci
touch css/custom.css
npm run test:coverage
- name: "Upload coverage results to codecov"

View File

@ -5,6 +5,48 @@ This project adheres to [Semantic Versioning](https://semver.org/).
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror².
## [2.24.0] - 2023-07-01
Thanks to: @angeldeejay, @bugsounet, @buxxi, @CarJem, @dariom, @DaveChild, @dWoolridge, @eddiehung, @grenagit, @Hirschberger, @ismarslomic, @JakeBinney, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @nfogal, @oscarb, @OWL4C, @psieg, @rajniszp, @retroflex, @SkySails and @tomzt
Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome!
### Added
- Added UV Index to hourly and current Weather, with support for Openmeteo
- Added tests for serveronly
- Set Timezone `Europe/Berlin` in unit tests (needed for new formatTime tests)
- Added no-param-reassign eslint rule and fix warnings
- updatenotification: Added `sendUpdatesNotifications` feature. Broadcast update with `UPDATES` notification to other modules
- updatenotification: allow force scanning with `SCAN_UPDATES` notification from other modules
- Added per-calendar fetchInterval
### Removed
- Removed unneeded (and unwanted) '.' after the year in calendar repeatingCountTitle (#2896, second attempt ...)
### Updated
- Added support for precipitation probability with openmeteo weather-provider
- Update electron to v25.2 and other dependencies
- Use node v20 in github workflow (replacing v14)
- Refactor formatTime into common util function for default modules
- Refactor some calendar methods into own class and added tests for them
- Split install and run commands in github actions
- Changed `fetchInterval` of calendar in `config.js.sample` to 7 days so we not to request example calendar too frequently
- Changed default calendar fetchInterval to one hour
- Changed calendar url in sample config
### Fixed
- Fix envcanada hourly forecast time (#3080)
- Fix electron not running under windows after async changes (#3083)
- Fix style issues after eslint-plugin-jsdoc update
- Fix don't filter out ongoing full day events (#3095)
- Fix date not shown when clock in analog mode (#3100)
- Fix envcanada today percentage-of-precipitation (#3106)
- Fix updatenotification where no branch is checked out but e.g. a version tag (#3130)
## [2.23.0] - 2023-04-04
Thanks to: @angeldeejay, @buxxi, @CarJem, @dariom, @DaveChild, @dWoolridge, @grenagit, @Hirschberger, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @nfogal, @psieg, @rajniszp, @retroflex, @SkySails and @tomzt.
@ -16,17 +58,18 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
- Added increments for hourly forecasts in weather module (#2996)
- Added tests for hourly weather forecast
- Added possibility to ignore MagicMirror repo in updatenotification module
- Added Pirate Weather as new weather provider (#3005)
- Added Pirate Weather as new weather-provider (#3005)
- Added possibility to use your own templates in Alert module
- Added error message if `<modulename>.js` file is missing in module folder to get a hint in the logs (#2403)
- Added possibility to use environment variables in `config.js` (#1756)
- Added option `pastDaysCount` to default calendar module to control of how many days past events should be displayed
- Added thai language to alert module
- Added option `sendNotifications` in clock module (#3056)
- Added tests for some weather utils
### Removed
- Removed darksky weather provider
- Removed darksky weather-provider
- Removed unneeded (and unwanted) '.' after the year in calendar repeatingCountTitle (#2896)
### Updated
@ -43,6 +86,7 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
- Update Eslint config, add new rule and handle issue
- Convert lots of callbacks to async/await
- Revise require imports (#3071 and #3072)
- Use `config.js-old` instead of file with timestamp suffix when backing up config with a `config.template` in use (#3104)
### Fixed
@ -73,12 +117,12 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
- Added new calendar options for colored entries and improved styling (#3033)
- Added test for remoteFile option in compliments module
- Added hourlyWeather functionality to Weather.gov weather provider
- Added hourlyWeather functionality to Weather.gov weather-provider
- Added css class names "today" and "tomorrow" for default calendar
- Added Collaboration.md
- Added new github action for dependency review (#2862)
- Added a WeatherProvider for Open-Meteo
- Added Yr as a weather provider
- Added Yr as a weather-provider
- Added config options "ignoreXOriginHeader" and "ignoreContentSecurityPolicy"
- Added thai language
- Added workflow rule to make sure PRs are based against develop
@ -96,8 +140,8 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
- Updated da translation
- Rework weather module
- Make sure smhi provider api only gets a maximum of 6 digits coordinates (#2955)
- Use fetch instead of XMLHttpRequest in weatherprovider (#2935)
- Reworked how weatherproviders handle units (#2849)
- Use fetch instead of XMLHttpRequest in weather-provider (#2935)
- Reworked how weather-providers handle units (#2849)
- Use unix() method for parsing times, fix suntimes on the way (#2950)
- Refactor conversion functions into utils class (#2958)
- The `cors`-method in `server.js` now supports sending and receiving HTTP headers
@ -107,7 +151,7 @@ Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not al
### Fixed
- Correctly show apparent temperature in SMHI weather provider
- Correctly show apparent temperature in SMHI weather-provider
- Ensure updatenotification module isn't shown when local is _ahead_ of remote
- Handle node_helper errors during startup (#2944)
- Possibility to change FontAwesome class in calendar, so icons like `fab fa-facebook-square` works.
@ -126,7 +170,7 @@ Special thanks to: @BKeyport, @buxxi, @davide125, @khassel, @kolbyjack, @krukle,
- Added possibility to fetch calendars through socket notifications.
- New scripts `install-mm` (and `install-mm:dev`) for simplifying mm installation (now: `npm run install-mm`) and adding params `--no-audit --no-fund --no-update-notifier` for less noise.
- New `showTimeToday` option in calendar module shows time for current-day events even if `timeFormat` is `"relative"`.
- Added hourly forecasts, apparent temperature & custom location name to SMHI weather provider.
- Added hourly forecasts, apparent temperature & custom location name to SMHI weather-provider.
- Added new electron tests for calendar and moved some compliments tests from `e2e` to `electron` because of date mocking, removed mock stuff from compliments module.
### Removed
@ -182,7 +226,7 @@ Special thanks to the following contributors: @10bias, @CFenner, @JHWelch, @k1rd
- Added test for new weather forecast `absoluteDates` property.
- The modules get a class hidden added/removed if they get hidden/shown which will also toggle pointer-events.
- Added new config option `showTitleAsUrl` to newsfeed module. If set, the displayed title is a link to the article which is useful when running in a browser and you want to read this article.
- Added internal cors proxy to get weather providers working without public proxies (fixes #2714). The new url `http(s)://address:port/cors?url=https://whatever-to-proxy` can be used in other modules too.
- Added internal cors proxy to get weather-providers working without public proxies (fixes #2714). The new url `http(s)://address:port/cors?url=https://whatever-to-proxy` can be used in other modules too.
- Added a WeatherProvider for Weatherflow.
- Added new env var `ELECTRON_DISABLE_GPU` which disable gpu under electron if set (fixes #2831).
- Added missing Czech translations.
@ -276,7 +320,7 @@ Special thanks to the following contributors: @apiontek, @eouia, @jupadin, @khas
- Updated jsdocs and print warnings during testing too.
- Updated weathergov provider to try fetching not just current, but also foreacst, when API URLs available.
- Refactored clock layout.
- Refactored methods from weatherproviders into weatherobject (isDaytime, updateSunTime).
- Refactored methods from weather-providers into weatherobject (isDaytime, updateSunTime).
- Use of `logger.js` in jest tests.
- Run prettier over all relevant files.
- Move tests needing electron in new category `electron`, use `server only` mode in `e2e` tests.
@ -374,7 +418,7 @@ Special thanks to the following contributors: @EdgardosReis, @MystaraTheGreat, @
- Code cleanup for FEELS like and added {DEGREE} placeholder for FEELSLIKE for each language.
- Converted newsfeed module to use templates.
- Updated documentation and help screen about invalid config files.
- Moving weather provider specific code and configuration into each provider and making hourly part of the interface.
- Moving weather-provider specific code and configuration into each provider and making hourly part of the interface.
- Bump electron to v11 and enable contextIsolation.
- Don't update the DOM when a module is not displayed.
- Cleaned up jsdoc and tests.
@ -456,7 +500,7 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
- Rename Greek translation to correct ISO 639-1 alpha-2 code (gr > el). (#2155)
- 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 windspeed conversion error in ukmetoffice weather provider. (#2189)
- Fix windspeed conversion 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)
@ -685,7 +729,7 @@ Special thanks to @sdetweil for all his great contributions!
- Use Feels Like temp from feed if present
- Optionally display probability of precipitation (PoP) in current weather (UK Met Office data)
- Automatically try to fix eslint errors by passing `--fix` option to it
- Added sunrise and sunset times to weathergov weather provider [#1705](https://github.com/MichMich/MagicMirror/issues/1705)
- Added sunrise and sunset times to weathergov weather-provider [#1705](https://github.com/MichMich/MagicMirror/issues/1705)
- Added "useLocationAsHeader" to display "location" in `config.js` as header when location name is not returned
- Added to `newsfeed.js`: in order to design the news article better with css, three more class-names were introduced: newsfeed-desc, newsfeed-desc, newsfeed-desc
@ -696,7 +740,7 @@ Special thanks to @sdetweil for all his great contributions!
- Updated `ical.js` to solve various calendar issues.
- Updated weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
- Only update clock once per minute when seconds aren't shown
- Updated weatherprovider documentation.
- Updated weather-provider documentation.
### Fixed
@ -778,7 +822,7 @@ Fixed `package.json` version number.
- Added fade, fadePoint and maxNumberOfDays properties to the forecast mode [#1516](https://github.com/MichMich/MagicMirror/issues/1516)
- Fixed Loading string and decimalSymbol string replace [#1538](https://github.com/MichMich/MagicMirror/issues/1538)
- Show Snow amounts in new weather module [#1545](https://github.com/MichMich/MagicMirror/issues/1545)
- Added weather.gov as a new weather provider for US locations
- Added weather.gov as a new weather-provider for US locations
## [2.6.0] - 2019-01-01

View File

@ -11,7 +11,6 @@
/**
* Get command line parameters
* Assumes that a cmdline parameter is defined with `--key [value]`
*
* @param {string} key key to look for at the command line
* @param {string} defaultValue value if no key is given at the command line
* @returns {string} the value of the parameter
@ -33,7 +32,6 @@
/**
* Gets the config from the specified server url
*
* @param {string} url location where the server is running.
* @returns {Promise} the config
*/
@ -63,7 +61,6 @@
/**
* Print a message to the console in case of errors
*
* @param {string} message error message to print
* @param {number} code error code for the exit call
*/

View File

@ -55,8 +55,9 @@ let config = {
config: {
calendars: [
{
fetchInterval: 7 * 24 * 60 * 60 * 1000,
symbol: "calendar-check",
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics"
url: "https://ics.calendarlabs.com/76/mm3137/US_Holidays.ics"
}
]
}

View File

@ -6,23 +6,24 @@ module.exports = async () => {
projects: [
{
displayName: "unit",
globalSetup: "<rootDir>/tests/unit/helpers/global-setup.js",
moduleNameMapper: {
logger: "<rootDir>/js/logger.js"
},
testMatch: ["**/tests/unit/**/*.[jt]s?(x)"],
testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks"]
testPathIgnorePatterns: ["<rootDir>/tests/unit/mocks", "<rootDir>/tests/unit/helpers"]
},
{
displayName: "electron",
testMatch: ["**/tests/electron/**/*.[jt]s?(x)"],
testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers/"]
testPathIgnorePatterns: ["<rootDir>/tests/electron/helpers"]
},
{
displayName: "e2e",
setupFilesAfterEnv: ["<rootDir>/tests/e2e/helpers/mock-console.js"],
testMatch: ["**/tests/e2e/**/*.[jt]s?(x)"],
modulePaths: ["<rootDir>/js/"],
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers/", "<rootDir>/tests/e2e/mocks"]
testPathIgnorePatterns: ["<rootDir>/tests/e2e/helpers", "<rootDir>/tests/e2e/mocks"]
}
],
collectCoverageFrom: ["./clientonly/**/*.js", "./js/**/*.js", "./modules/default/**/*.js", "./serveronly/**/*.js"],

View File

@ -44,7 +44,6 @@ process.on("uncaughtException", function (err) {
/**
* The core app.
*
* @class
*/
function App() {
@ -53,7 +52,6 @@ function App() {
/**
* Loads the config file. Combines it with the defaults and returns the config
*
* @async
* @returns {Promise<object>} the loaded config or the defaults if something goes wrong
*/
@ -78,7 +76,7 @@ function App() {
// save current config.js
try {
if (fs.existsSync(configFilename)) {
fs.copyFileSync(configFilename, `${configFilename}_${Date.now()}`);
fs.copyFileSync(configFilename, `${configFilename}-old`);
}
} catch (err) {
Log.warn(`Could not copy ${configFilename}: ${err.message}`);
@ -135,7 +133,6 @@ function App() {
/**
* Checks the config for deprecated options and throws a warning in the logs
* if it encounters one option from the deprecated.js list
*
* @param {object} userConfig The user config
*/
function checkDeprecatedOptions(userConfig) {
@ -150,7 +147,6 @@ function App() {
/**
* Loads a specific module.
*
* @param {string} module The name of the module (including subpath).
*/
function loadModule(module) {
@ -204,36 +200,21 @@ function App() {
/**
* Loads all modules.
*
* @param {string[]} modules All modules to be loaded
* @param {Module[]} modules All modules to be loaded
* @returns {Promise} A promise that is resolved when all modules been loaded
*/
async function loadModules(modules) {
return new Promise((resolve) => {
Log.log("Loading module helpers ...");
Log.log("Loading module helpers ...");
/**
*
*/
function loadNextModule() {
if (modules.length > 0) {
const nextModule = modules[0];
loadModule(nextModule);
modules = modules.slice(1);
loadNextModule();
} else {
// All modules are loaded
Log.log("All module helpers loaded.");
resolve();
}
}
for (let module of modules) {
await loadModule(module);
}
loadNextModule();
});
Log.log("All module helpers loaded.");
}
/**
* Compare two semantic version numbers and return the difference.
*
* @param {string} a Version number a.
* @param {string} b Version number b.
* @returns {number} A positive number if a is larger than b, a negative
@ -259,7 +240,6 @@ function App() {
* Start the core app.
*
* It loads the config, then it loads all modules.
*
* @async
* @returns {Promise<object>} the config used
*/
@ -274,6 +254,7 @@ function App() {
modules.push(module.module);
}
}
await loadModules(modules);
httpServer = new Server(config);
@ -312,7 +293,6 @@ function App() {
* exists.
*
* Added to fix #1056
*
* @returns {Promise} A promise that is resolved when all node_helpers and
* the http server has been closed
*/

View File

@ -18,7 +18,6 @@ const Utils = require(`${rootPath}/js/utils.js`);
/**
* Returns a string with path of configuration file.
* Check if set by environment variable MM_CONFIG_FILE
*
* @returns {string} path and filename of the config file
*/
function getConfigFile() {

View File

@ -82,7 +82,6 @@
/**
* Define the clone method for later use. Helper Method.
*
* @param {object} obj Object to be cloned
* @returns {object} the cloned object
*/

View File

@ -126,13 +126,6 @@ function createWindow() {
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on("ready", function () {
Log.log("Launching application.");
createWindow();
});
// Quit when all windows are closed.
app.on("window-all-closed", function () {
if (process.env.JEST_WORKER_ID !== undefined) {
@ -178,5 +171,11 @@ app.on("certificate-error", (event, webContents, url, error, certificate, callba
// Start the core application if server is run on localhost
// This starts all node helpers and starts the webserver.
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {
core.start().then((c) => (config = c));
core.start().then((c) => {
config = c;
app.whenReady().then(() => {
Log.log("Launching application.");
createWindow();
});
});
}

View File

@ -4,7 +4,6 @@
*
* Attention: After some discussion we always return the third party
* implementation until the node implementation is stable and more tested
*
* @see https://github.com/MichMich/MagicMirror/pull/2952
* @see https://github.com/MichMich/MagicMirror/issues/2649
* @param {string} url to be fetched

View File

@ -52,7 +52,6 @@ const Loader = (function () {
/**
* Retrieve list of all modules.
*
* @returns {object[]} module data as configured in config
*/
const getAllModules = function () {
@ -61,7 +60,6 @@ const Loader = (function () {
/**
* Generate array with module information including module paths.
*
* @returns {object[]} Module information.
*/
const getModuleData = function () {
@ -103,7 +101,6 @@ const Loader = (function () {
/**
* Load modules via ajax request and create module objects.
*
* @param {object} module Information about the module we want to load.
* @returns {Promise<void>} resolved when module is loaded
*/
@ -131,7 +128,6 @@ const Loader = (function () {
/**
* Bootstrap modules by setting the module data and loading the scripts & styles.
*
* @param {object} module Information about the module we want to load.
* @param {Module} mObj Modules instance.
*/
@ -153,7 +149,6 @@ const Loader = (function () {
/**
* Load a script or stylesheet by adding it to the dom.
*
* @param {string} fileName Path of the file we want to load.
* @returns {Promise} resolved when the file is loaded
*/
@ -229,7 +224,6 @@ const Loader = (function () {
/**
* Load a file (script or stylesheet).
* Prevent double loading and search for files in the vendor folder.
*
* @param {string} fileName Path of the file we want to load.
* @param {Module} module The module that calls the loadFile function.
* @returns {Promise} resolved when the file is loaded

View File

@ -68,7 +68,6 @@ const MM = (function () {
/**
* Select the wrapper dom object for a specific position.
*
* @param {string} position The name of the position.
* @returns {HTMLElement | void} the wrapper element
*/
@ -85,7 +84,6 @@ const MM = (function () {
/**
* Send a notification to all modules.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
* @param {Module} sender The module that sent the notification.
@ -102,7 +100,6 @@ const MM = (function () {
/**
* Update the dom for a specific module.
*
* @param {Module} module The module that needs an update.
* @param {number} [speed] The (optional) number of microseconds for the animation.
* @returns {Promise} Resolved when the dom is fully updated.
@ -129,7 +126,6 @@ const MM = (function () {
/**
* Update the dom with the specified content
*
* @param {Module} module The module that needs an update.
* @param {number} [speed] The (optional) number of microseconds for the animation.
* @param {string} newHeader The new header that is generated.
@ -167,7 +163,6 @@ const MM = (function () {
/**
* Check if the content has changed.
*
* @param {Module} module The module to check.
* @param {string} newHeader The new header that is generated.
* @param {HTMLElement} newContent The new content that is generated.
@ -198,7 +193,6 @@ const MM = (function () {
/**
* Update the content of a module on screen.
*
* @param {Module} module The module to check.
* @param {string} newHeader The new header that is generated.
* @param {HTMLElement} newContent The new content that is generated.
@ -224,15 +218,12 @@ const MM = (function () {
/**
* Hide the module.
*
* @param {Module} module The module to hide.
* @param {number} speed The speed of the hide animation.
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the hide method.
*/
const hideModule = function (module, speed, callback, options) {
options = options || {};
const hideModule = function (module, speed, callback, options = {}) {
// set lockString if set in options.
if (options.lockString) {
// Log.log("Has lockstring: " + options.lockString);
@ -271,15 +262,12 @@ const MM = (function () {
/**
* Show the module.
*
* @param {Module} module The module to show.
* @param {number} speed The speed of the show animation.
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the show method.
*/
const showModule = function (module, speed, callback, options) {
options = options || {};
const showModule = function (module, speed, callback, options = {}) {
// remove lockString if set in options.
if (options.lockString) {
const index = module.lockStrings.indexOf(options.lockString);
@ -380,13 +368,11 @@ const MM = (function () {
/**
* Adds special selectors on a collection of modules.
*
* @param {Module[]} modules Array of modules.
*/
const setSelectionMethodsForModules = function (modules) {
/**
* Filter modules with the specified classes.
*
* @param {string|string[]} className one or multiple classnames (array or space divided).
* @returns {Module[]} Filtered collection of modules.
*/
@ -396,7 +382,6 @@ const MM = (function () {
/**
* Filter modules without the specified classes.
*
* @param {string|string[]} className one or multiple classnames (array or space divided).
* @returns {Module[]} Filtered collection of modules.
*/
@ -406,7 +391,6 @@ const MM = (function () {
/**
* Filters a collection of modules based on classname(s).
*
* @param {string|string[]} className one or multiple classnames (array or space divided).
* @param {boolean} include if the filter should include or exclude the modules with the specific classes.
* @returns {Module[]} Filtered collection of modules.
@ -435,7 +419,6 @@ const MM = (function () {
/**
* Removes a module instance from the collection.
*
* @param {object} module The module instance to remove from the collection.
* @returns {Module[]} Filtered collection of modules.
*/
@ -450,7 +433,6 @@ const MM = (function () {
/**
* Walks thru a collection of modules and executes the callback with the module as an argument.
*
* @param {Function} callback The function to execute with the module as an argument.
*/
const enumerate = function (callback) {
@ -491,7 +473,6 @@ const MM = (function () {
/**
* Gets called when all modules are started.
*
* @param {Module[]} moduleObjects All module instances.
*/
modulesStarted: function (moduleObjects) {
@ -506,7 +487,6 @@ const MM = (function () {
/**
* Send a notification to all modules.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
* @param {Module} sender The module that sent the notification.
@ -533,7 +513,6 @@ const MM = (function () {
/**
* Update the dom for a specific module.
*
* @param {Module} module The module that needs an update.
* @param {number} [speed] The number of microseconds for the animation.
*/
@ -554,7 +533,6 @@ const MM = (function () {
/**
* Returns a collection of all modules currently active.
*
* @returns {Module[]} A collection of all modules currently active.
*/
getModules: function () {
@ -564,7 +542,6 @@ const MM = (function () {
/**
* Hide the module.
*
* @param {Module} module The module to hide.
* @param {number} speed The speed of the hide animation.
* @param {Function} callback Called when the animation is done.
@ -577,7 +554,6 @@ const MM = (function () {
/**
* Show the module.
*
* @param {Module} module The module to show.
* @param {number} speed The speed of the show animation.
* @param {Function} callback Called when the animation is done.

View File

@ -46,7 +46,6 @@ const Module = Class.extend({
/**
* Returns a list of scripts the module requires to be loaded.
*
* @returns {string[]} An array with filenames.
*/
getScripts: function () {
@ -55,7 +54,6 @@ const Module = Class.extend({
/**
* Returns a list of stylesheets the module requires to be loaded.
*
* @returns {string[]} An array with filenames.
*/
getStyles: function () {
@ -66,7 +64,6 @@ const Module = Class.extend({
* Returns a map of translation files the module requires to be loaded.
*
* return Map<String, String> -
*
* @returns {*} A map with langKeys and filenames.
*/
getTranslations: function () {
@ -77,7 +74,6 @@ const Module = Class.extend({
* Generates the dom which needs to be displayed. This method is called by the MagicMirror² core.
* This method can to be subclassed if the module wants to display info on the mirror.
* Alternatively, the getTemplate method could be subclassed.
*
* @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
*/
getDom: function () {
@ -111,7 +107,6 @@ const Module = Class.extend({
* Generates the header string which needs to be displayed if a user has a header configured for this module.
* This method is called by the MagicMirror² core, but only if the user has configured a default header for the module.
* This method needs to be subclassed if the module wants to display modified headers on the mirror.
*
* @returns {string} The header to display above the header.
*/
getHeader: function () {
@ -123,7 +118,6 @@ const Module = Class.extend({
* This method needs to be subclassed if the module wants to use a template.
* It can either return a template sting, or a template filename.
* If the string ends with '.html' it's considered a file from within the module's folder.
*
* @returns {string} The template string of filename.
*/
getTemplate: function () {
@ -133,7 +127,6 @@ const Module = Class.extend({
/**
* Returns the data to be used in the template.
* This method needs to be subclassed if the module wants to use a custom data.
*
* @returns {object} The data for the template
*/
getTemplateData: function () {
@ -142,7 +135,6 @@ const Module = Class.extend({
/**
* Called by the MagicMirror² core when a notification arrives.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
* @param {Module} sender The module that sent the notification.
@ -158,7 +150,6 @@ const Module = Class.extend({
/**
* Returns the nunjucks environment for the current module.
* The environment is checked in the _nunjucksEnvironment instance variable.
*
* @returns {object} The Nunjucks Environment
*/
nunjucksEnvironment: function () {
@ -180,7 +171,6 @@ const Module = Class.extend({
/**
* Called when a socket notification arrives.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
*/
@ -208,7 +198,6 @@ const Module = Class.extend({
/**
* Set the module data.
*
* @param {object} data The module data
*/
setData: function (data) {
@ -222,7 +211,6 @@ const Module = Class.extend({
/**
* Set the module config and combine it with the module defaults.
*
* @param {object} config The combined module config.
* @param {boolean} deep Merge module config in deep.
*/
@ -233,7 +221,6 @@ const Module = Class.extend({
/**
* Returns a socket object. If it doesn't exist, it's created.
* It also registers the notification callback.
*
* @returns {MMSocket} a socket object
*/
socket: function () {
@ -250,7 +237,6 @@ const Module = Class.extend({
/**
* Retrieve the path to a module file.
*
* @param {string} file Filename
* @returns {string} the file path
*/
@ -260,7 +246,6 @@ const Module = Class.extend({
/**
* Load all required stylesheets by requesting the MM object to load the files.
*
* @returns {Promise<void>}
*/
loadStyles: function () {
@ -269,7 +254,6 @@ const Module = Class.extend({
/**
* Load all required scripts by requesting the MM object to load the files.
*
* @returns {Promise<void>}
*/
loadScripts: function () {
@ -278,7 +262,6 @@ const Module = Class.extend({
/**
* Helper method to load all dependencies.
*
* @param {string} funcName Function name to call to get scripts or styles.
* @returns {Promise<void>}
*/
@ -301,6 +284,7 @@ const Module = Class.extend({
/**
* Load all translations.
* @returns {Promise<void>}
*/
loadTranslations: async function () {
const translations = this.getTranslations() || {};
@ -329,7 +313,6 @@ const Module = Class.extend({
/**
* Request the translation for a given key with optional variables and default value.
*
* @param {string} key The key of the string to translate
* @param {string|object} [defaultValueOrVariables] The default value or variables for translating.
* @param {string} [defaultValue] The default value with variables.
@ -344,7 +327,6 @@ const Module = Class.extend({
/**
* Request an (animated) update of the module.
*
* @param {number} [speed] The speed of the animation.
*/
updateDom: function (speed) {
@ -353,7 +335,6 @@ const Module = Class.extend({
/**
* Send a notification to all modules.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
*/
@ -363,7 +344,6 @@ const Module = Class.extend({
/**
* Send a socket notification to the node helper.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
*/
@ -373,55 +353,55 @@ const Module = Class.extend({
/**
* Hide this module.
*
* @param {number} speed The speed of the hide animation.
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the hide method.
*/
hide: function (speed, callback, options) {
if (typeof callback === "object") {
options = callback;
callback = function () {};
}
hide: function (speed, callback, options = {}) {
let usedCallback = callback || function () {};
let usedOptions = options;
callback = callback || function () {};
options = options || {};
if (typeof callback === "object") {
Log.error("Parameter mismatch in module.hide: callback is not an optional parameter!");
usedOptions = callback;
usedCallback = function () {};
}
MM.hideModule(
this,
speed,
() => {
this.suspend();
callback();
usedCallback();
},
options
usedOptions
);
},
/**
* Show this module.
*
* @param {number} speed The speed of the show animation.
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the show method.
*/
show: function (speed, callback, options) {
if (typeof callback === "object") {
options = callback;
callback = function () {};
}
let usedCallback = callback || function () {};
let usedOptions = options;
callback = callback || function () {};
options = options || {};
if (typeof callback === "object") {
Log.error("Parameter mismatch in module.show: callback is not an optional parameter!");
usedOptions = callback;
usedCallback = function () {};
}
MM.showModule(
this,
speed,
() => {
this.resume();
callback();
usedCallback();
},
options
usedOptions
);
}
});
@ -445,7 +425,6 @@ const Module = Class.extend({
* -------
*
* Todo: idea of Mich determinate what do you want to merge or not
*
* @param {object} result the initial object
* @returns {object} the merged config
*/
@ -507,7 +486,6 @@ window.Module = Module;
/**
* Compare two semantic version numbers and return the difference.
*
* @param {string} a Version number a.
* @param {string} b Version number b.
* @returns {number} A positive number if a is larger than b, a negative

View File

@ -32,7 +32,6 @@ const NodeHelper = Class.extend({
/**
* This method is called when a socket notification arrives.
*
* @param {string} notification The identifier of the notification.
* @param {*} payload The payload of the notification.
*/
@ -42,7 +41,6 @@ const NodeHelper = Class.extend({
/**
* Set the module name.
*
* @param {string} name Module name.
*/
setName(name) {
@ -51,7 +49,6 @@ const NodeHelper = Class.extend({
/**
* Set the module path.
*
* @param {string} path Module path.
*/
setPath(path) {
@ -123,7 +120,6 @@ NodeHelper.checkFetchStatus = function (response) {
/**
* Look at the specified error and return an appropriate error type, that
* can be translated to a detailed error message
*
* @param {Error} error the error from fetching something
* @returns {string} the string of the detailed error message in the translations
*/

View File

@ -19,7 +19,6 @@ const { cors, getConfig, getHtml, getVersion } = require("./server_functions");
/**
* Server
*
* @param {object} config The MM config
* @class
*/
@ -31,7 +30,6 @@ function Server(config) {
/**
* Opens the server for incoming connections
*
* @returns {Promise} A promise that is resolved when the server listens to connections
*/
this.open = function () {
@ -106,7 +104,6 @@ function Server(config) {
/**
* Closes the server and destroys all lingering connections to it.
*
* @returns {Promise} A promise that resolves when server has successfully shut down
*/
this.close = function () {

View File

@ -5,7 +5,6 @@ const fetch = require("./fetch");
/**
* Gets the config.
*
* @param {Request} req - the request
* @param {Response} res - the result
*/
@ -19,7 +18,6 @@ function getConfig(req, res) {
* Example input request url: /cors?sendheaders=header1:value1,header2:value2&expectedheaders=header1,header2&url=http://www.test.com/path?param1=value1
*
* Only the url-param of the input request url is required. It must be the last parameter.
*
* @param {Request} req - the request
* @param {Response} res - the result
*/
@ -57,7 +55,6 @@ async function cors(req, res) {
/**
* Gets headers and values to attach to the web request.
*
* @param {string} url - The url containing the headers and values to send.
* @returns {object} An object specifying name and value of the headers.
*/
@ -79,7 +76,6 @@ function getHeadersToSend(url) {
/**
* Gets the headers expected from the response.
*
* @param {string} url - The url containing the expected headers from the response.
* @returns {string[]} headers - The name of the expected headers.
*/
@ -97,7 +93,6 @@ function geExpectedRecievedHeaders(url) {
/**
* Gets the HTML to display the magic mirror.
*
* @param {Request} req - the request
* @param {Response} res - the result
*/
@ -116,7 +111,6 @@ function getHtml(req, res) {
/**
* Gets the MagicMirror version.
*
* @param {Request} req - the request
* @param {Response} res - the result
*/

View File

@ -44,10 +44,7 @@ const MMSocket = function (moduleName) {
notificationCallback = callback;
};
this.sendNotification = (notification, payload) => {
if (typeof payload === "undefined") {
payload = {};
}
this.sendNotification = (notification, payload = {}) => {
this.socket.emit(notification, payload);
};
};

View File

@ -9,13 +9,12 @@
const Translator = (function () {
/**
* Load a JSON file via XHR.
*
* @param {string} file Path of the file we want to load.
* @returns {Promise<object>} the translations in the specified file
*/
async function loadJSON(file) {
const xhr = new XMLHttpRequest();
return new Promise(function (resolve, reject) {
return new Promise(function (resolve) {
xhr.overrideMimeType("application/json");
xhr.open("GET", file, true);
xhr.onreadystatechange = function () {
@ -43,21 +42,17 @@ const Translator = (function () {
/**
* Load a translation for a given key for a given module.
*
* @param {Module} module The module to load the translation for.
* @param {string} key The key of the text to translate.
* @param {object} variables The variables to use within the translation template (optional)
* @returns {string} the translated key
*/
translate: function (module, key, variables) {
variables = variables || {}; // Empty object by default
translate: function (module, key, variables = {}) {
/**
* Combines template and variables like:
* template: "Please wait for {timeToWait} before continuing with {work}."
* variables: {timeToWait: "2 hours", work: "painting"}
* to: "Please wait for 2 hours before continuing with painting."
*
* @param {string} template Text with placeholder
* @param {object} variables Variables for the placeholder
* @returns {string} the template filled with the variables
@ -66,10 +61,11 @@ const Translator = (function () {
if (Object.prototype.toString.call(template) !== "[object String]") {
return template;
}
let templateToUse = template;
if (variables.fallback && !template.match(new RegExp("{.+}"))) {
template = variables.fallback;
templateToUse = variables.fallback;
}
return template.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
return templateToUse.replace(new RegExp("{([^}]+)}", "g"), function (_unused, varName) {
return varName in variables ? variables[varName] : `{${varName}}`;
});
}
@ -99,7 +95,6 @@ const Translator = (function () {
/**
* Load a translation file (json) and remember the data.
*
* @param {Module} module The module to load the translation file for.
* @param {string} file Path of the file we want to load.
* @param {boolean} isFallback Flag to indicate fallback translations.
@ -118,7 +113,6 @@ const Translator = (function () {
/**
* Load the core translations.
*
* @param {string} lang The language identifier of the core language.
*/
loadCoreTranslations: async function (lang) {

View File

@ -9,13 +9,11 @@
*
* Copyright 2014, Codrops
* https://tympanus.net/codrops/
*
* @param {object} window The window object
*/
(function (window) {
/**
* Extend one object with another one
*
* @param {object} a The object to extend
* @param {object} b The object which extends the other, overwrites existing keys
* @returns {object} The merged object
@ -31,7 +29,6 @@
/**
* NotificationFx constructor
*
* @param {object} options The configuration options
* @class
*/
@ -124,7 +121,6 @@
/**
* Dismiss the notification
*
* @param {boolean} [close] call the onClose callback at the end
*/
NotificationFx.prototype.dismiss = function (close = true) {

View File

@ -1,4 +1,4 @@
/* global cloneObject */
/* global CalendarUtils, cloneObject */
/* MagicMirror²
* Module: Calendar
@ -21,11 +21,11 @@ Module.register("calendar", {
defaultRepeatingCountTitle: "",
maxTitleLength: 25,
maxLocationTitleLength: 25,
wrapEvents: false, // wrap events to multiple lines breaking at maxTitleLength
wrapEvents: false, // Wrap events to multiple lines breaking at maxTitleLength
wrapLocationEvents: false,
maxTitleLines: 3,
maxEventTitleLines: 3,
fetchInterval: 5 * 60 * 1000, // Update every 5 minutes.
fetchInterval: 60 * 60 * 1000, // Update every hour
animationSpeed: 2000,
fade: true,
urgency: 7,
@ -79,7 +79,7 @@ Module.register("calendar", {
// Define required scripts.
getScripts: function () {
return ["moment.js"];
return ["calendarutils.js", "moment.js"];
},
// Define required translations.
@ -108,7 +108,7 @@ Module.register("calendar", {
}
// Set locale.
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
moment.updateLocale(config.language, CalendarUtils.getLocaleSpecification(config.timeFormat));
// clear data holder before start
this.calendarData = {};
@ -313,7 +313,7 @@ Module.register("calendar", {
const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
yearDiff = thisYear - event.firstYear;
repeatingCountTitle = `, ${yearDiff}. ${repeatingCountTitle}`;
repeatingCountTitle = `, ${yearDiff} ${repeatingCountTitle}`;
}
}
@ -337,7 +337,8 @@ Module.register("calendar", {
}
}
titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
const transformedTitle = CalendarUtils.titleTransform(event.title, this.config.titleReplace);
titleWrapper.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxTitleLength, this.config.wrapEvents, this.config.maxTitleLines) + repeatingCountTitle;
const titleClass = this.titleClassForUrl(event.url);
@ -362,7 +363,7 @@ Module.register("calendar", {
// Add endDate to dataheaders if showEnd is enabled
if (this.config.showEnd) {
timeWrapper.innerHTML += ` - ${this.capFirst(moment(event.endDate, "x").format("LT"))}`;
timeWrapper.innerHTML += ` - ${CalendarUtils.capFirst(moment(event.endDate, "x").format("LT"))}`;
}
eventWrapper.appendChild(timeWrapper);
@ -378,20 +379,20 @@ Module.register("calendar", {
if (this.config.timeFormat === "absolute") {
// Use dateFormat
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.dateFormat));
// Add end time if showEnd
if (this.config.showEnd) {
timeWrapper.innerHTML += "-";
timeWrapper.innerHTML += this.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
timeWrapper.innerHTML += CalendarUtils.capFirst(moment(event.endDate, "x").format(this.config.dateEndFormat));
}
// For full day events we use the fullDayEventDateFormat
if (event.fullDayEvent) {
//subtract one second so that fullDayEvents end at 23:59:59, and not at 0:00:00 one the next day
event.endDate -= ONE_SECOND;
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").format(this.config.fullDayEventDateFormat));
} else if (this.config.getRelative > 0 && event.startDate < now) {
// Ongoing and getRelative is set
timeWrapper.innerHTML = this.capFirst(
timeWrapper.innerHTML = CalendarUtils.capFirst(
this.translate("RUNNING", {
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
@ -399,19 +400,19 @@ Module.register("calendar", {
);
} else if (this.config.urgency > 0 && event.startDate - now < this.config.urgency * ONE_DAY) {
// Within urgency days
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
timeWrapper.innerHTML = CalendarUtils.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"));
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
} else if (event.yesterday) {
timeWrapper.innerHTML = this.capFirst(this.translate("YESTERDAY"));
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
}
}
}
@ -420,9 +421,9 @@ Module.register("calendar", {
if (event.startDate >= now || (event.fullDayEvent && event.today)) {
// Use relative time
if (!this.config.hideTime && !event.fullDayEvent) {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
timeWrapper.innerHTML = CalendarUtils.capFirst(moment(event.startDate, "x").calendar(null, { sameElse: this.config.dateFormat }));
} else {
timeWrapper.innerHTML = this.capFirst(
timeWrapper.innerHTML = CalendarUtils.capFirst(
moment(event.startDate, "x").calendar(null, {
sameDay: this.config.showTimeToday ? "LT" : `[${this.translate("TODAY")}]`,
nextDay: `[${this.translate("TOMORROW")}]`,
@ -434,27 +435,27 @@ Module.register("calendar", {
if (event.fullDayEvent) {
// Full days events within the next two days
if (event.today) {
timeWrapper.innerHTML = this.capFirst(this.translate("TODAY"));
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TODAY"));
} else if (event.dayBeforeYesterday) {
if (this.translate("DAYBEFOREYESTERDAY") !== "DAYBEFOREYESTERDAY") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYBEFOREYESTERDAY"));
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYBEFOREYESTERDAY"));
}
} else if (event.yesterday) {
timeWrapper.innerHTML = this.capFirst(this.translate("YESTERDAY"));
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("YESTERDAY"));
} else if (event.startDate - now < ONE_DAY && event.startDate - now > 0) {
timeWrapper.innerHTML = this.capFirst(this.translate("TOMORROW"));
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("TOMORROW"));
} else if (event.startDate - now < 2 * ONE_DAY && event.startDate - now > 0) {
if (this.translate("DAYAFTERTOMORROW") !== "DAYAFTERTOMORROW") {
timeWrapper.innerHTML = this.capFirst(this.translate("DAYAFTERTOMORROW"));
timeWrapper.innerHTML = CalendarUtils.capFirst(this.translate("DAYAFTERTOMORROW"));
}
}
} else if (event.startDate - now < this.config.getRelative * ONE_HOUR) {
// 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 = CalendarUtils.capFirst(moment(event.startDate, "x").fromNow());
}
} else {
// Ongoing event
timeWrapper.innerHTML = this.capFirst(
timeWrapper.innerHTML = CalendarUtils.capFirst(
this.translate("RUNNING", {
fallback: `${this.translate("RUNNING")} {timeUntilEnd}`,
timeUntilEnd: moment(event.endDate, "x").fromNow(true)
@ -503,7 +504,9 @@ Module.register("calendar", {
const descCell = document.createElement("td");
descCell.className = "location";
descCell.colSpan = "2";
descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines);
const transformedTitle = CalendarUtils.titleTransform(event.location, this.config.locationTitleReplace);
descCell.innerHTML = CalendarUtils.shorten(transformedTitle, this.config.maxLocationTitleLength, this.config.wrapLocationEvents, this.config.maxEventTitleLines);
locationRow.appendChild(descCell);
wrapper.appendChild(locationRow);
@ -519,31 +522,8 @@ Module.register("calendar", {
return wrapper;
},
/**
* This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
* corresponding timeformat to be used in the calendar display. If no number is given (or otherwise invalid input)
* it will a localeSpecification object with the system locale time format.
*
* @param {number} timeFormat Specifies either 12 or 24 hour time format
* @returns {moment.LocaleSpecification} formatted time
*/
getLocaleSpecification: function (timeFormat) {
switch (timeFormat) {
case 12: {
return { longDateFormat: { LT: "h:mm A" } };
}
case 24: {
return { longDateFormat: { LT: "HH:mm" } };
}
default: {
return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
}
}
},
/**
* Checks if this config contains the calendar url.
*
* @param {string} url The calendar url
* @returns {boolean} True if the calendar config contains the url, False otherwise
*/
@ -559,7 +539,6 @@ Module.register("calendar", {
/**
* Creates the sorted list of all events.
*
* @param {boolean} limitNumberOfEntries Whether to filter returned events for display.
* @returns {object[]} Array with events.
*/
@ -692,7 +671,6 @@ Module.register("calendar", {
/**
* Requests node helper to add calendar url.
*
* @param {string} url The calendar url to add
* @param {object} auth The authentication method and credentials
* @param {object} calendarConfig The config of the specific calendar
@ -705,7 +683,7 @@ Module.register("calendar", {
maximumEntries: calendarConfig.maximumEntries || this.config.maximumEntries,
maximumNumberOfDays: calendarConfig.maximumNumberOfDays || this.config.maximumNumberOfDays,
pastDaysCount: calendarConfig.pastDaysCount || this.config.pastDaysCount,
fetchInterval: this.config.fetchInterval,
fetchInterval: calendarConfig.fetchInterval || this.config.fetchInterval,
symbolClass: calendarConfig.symbolClass,
titleClass: calendarConfig.titleClass,
timeClass: calendarConfig.timeClass,
@ -717,7 +695,6 @@ Module.register("calendar", {
/**
* Retrieves the symbols for a specific event.
*
* @param {object} event Event to look for.
* @returns {string[]} The symbols
*/
@ -758,7 +735,6 @@ Module.register("calendar", {
/**
* Retrieves the symbolClass for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {string} The class to be used for the symbols of the calendar
*/
@ -768,7 +744,6 @@ Module.register("calendar", {
/**
* Retrieves the titleClass for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {string} The class to be used for the title of the calendar
*/
@ -778,7 +753,6 @@ Module.register("calendar", {
/**
* Retrieves the timeClass for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {string} The class to be used for the time of the calendar
*/
@ -788,7 +762,6 @@ Module.register("calendar", {
/**
* Retrieves the calendar name for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {string} The name of the calendar
*/
@ -798,7 +771,6 @@ Module.register("calendar", {
/**
* Retrieves the color for a specific calendar url.
*
* @param {string} url The calendar url
* @param {boolean} isBg Determines if we fetch the bgColor or not
* @returns {string} The color
@ -809,7 +781,6 @@ Module.register("calendar", {
/**
* Retrieves the count title for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {string} The title
*/
@ -819,7 +790,6 @@ Module.register("calendar", {
/**
* Retrieves the maximum entry count for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {number} The maximum entry count
*/
@ -829,7 +799,6 @@ Module.register("calendar", {
/**
* Retrieves the maximum count of past days which events of should be displayed for a specific calendar url.
*
* @param {string} url The calendar url
* @returns {number} The maximum past days count
*/
@ -839,7 +808,6 @@ Module.register("calendar", {
/**
* Helper method to retrieve the property for a specific calendar url.
*
* @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
@ -870,98 +838,6 @@ Module.register("calendar", {
return !!this.getCalendarProperty(url, property, undefined);
},
/**
* Shortens a string if it's longer than maxLength and add a ellipsis to the end
*
* @param {string} string Text string to shorten
* @param {number} maxLength The max length of the string
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
* @returns {string} The shortened string
*/
shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
if (typeof string !== "string") {
return "";
}
if (wrapEvents === true) {
const words = string.split(" ");
let temp = "";
let currentLine = "";
let line = 0;
for (let i = 0; i < words.length; i++) {
const word = words[i];
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
// max - 1 to account for a space
currentLine += `${word} `;
} else {
line++;
if (line > maxTitleLines - 1) {
if (i < words.length) {
currentLine += "…";
}
break;
}
if (currentLine.length > 0) {
temp += `${currentLine}<br>${word} `;
} else {
temp += `${word}<br>`;
}
currentLine = "";
}
}
return (temp + currentLine).trim();
} else {
if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
return `${string.trim().slice(0, maxLength)}`;
} else {
return string.trim();
}
}
},
/**
* Capitalize the first letter of a string
*
* @param {string} string The string to capitalize
* @returns {string} The capitalized string
*/
capFirst: function (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
/**
* Transforms the title of an event for usage.
* Replaces parts of the text as defined in config.titleReplace.
* Shortens title based on config.maxTitleLength and config.wrapEvents
*
* @param {string} title The title to transform.
* @param {object} titleReplace Pairs of strings to be replaced in the title
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
* @param {number} maxTitleLength The max length of the string
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
* @returns {string} The transformed title.
*/
titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
for (let needle in titleReplace) {
const replacement = titleReplace[needle];
const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
if (regParts) {
// the parsed pattern is a regexp.
needle = new RegExp(regParts[1], regParts[2]);
}
title = title.replace(needle, replacement);
}
title = this.shorten(title, maxTitleLength, wrapEvents, maxTitleLines);
return title;
},
/**
* Broadcasts the events to all other modules for reuse.
* The all events available in one array, sorted on startdate.

View File

@ -11,7 +11,7 @@ const ical = require("node-ical");
const fetch = require("fetch");
const Log = require("logger");
const NodeHelper = require("node_helper");
const CalendarUtils = require("./calendarutils");
const CalendarFetcherUtils = require("./calendarfetcherutils");
/**
*
@ -72,7 +72,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
try {
data = ical.parseICS(responseData);
Log.debug(`parsed data=${JSON.stringify(data)}`);
events = CalendarUtils.filterEvents(data, {
events = CalendarFetcherUtils.filterEvents(data, {
excludedEvents,
includePastEvents,
maximumEntries,
@ -121,7 +121,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
/**
* Sets the on success callback
*
* @param {Function} callback The on success callback.
*/
this.onReceive = function (callback) {
@ -130,7 +129,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
/**
* Sets the on error callback
*
* @param {Function} callback The on error callback.
*/
this.onError = function (callback) {
@ -139,7 +137,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
/**
* Returns the url of this fetcher.
*
* @returns {string} The url of this fetcher.
*/
this.url = function () {
@ -148,7 +145,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
/**
* Returns current available events for this fetcher.
*
* @returns {object[]} The current available events for this fetcher.
*/
this.events = function () {

View File

@ -0,0 +1,605 @@
/* MagicMirror²
* Calendar Fetcher Util Methods
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
/**
* @external Moment
*/
const path = require("path");
const moment = require("moment");
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
const Log = require("../../../js/logger");
const CalendarFetcherUtils = {
/**
* Calculate the time correction, either dst/std or full day in cases where
* utc time is day before plus offset
* @param {object} event the event which needs adjustment
* @param {Date} date the date on which this event happens
* @returns {number} the necessary adjustment in hours
*/
calculateTimezoneAdjustment: function (event, date) {
let adjustHours = 0;
// if a timezone was specified
if (!event.start.tz) {
Log.debug(" if no tz, guess based on now");
event.start.tz = moment.tz.guess();
}
Log.debug(`initial tz=${event.start.tz}`);
// if there is a start date specified
if (event.start.tz) {
// if this is a windows timezone
if (event.start.tz.includes(" ")) {
// use the lookup table to get theIANA name as moment and date don't know MS timezones
let tz = CalendarFetcherUtils.getIanaTZFromMS(event.start.tz);
Log.debug(`corrected TZ=${tz}`);
// watch out for unregistered windows timezone names
// if we had a successful lookup
if (tz) {
// change the timezone to the IANA name
event.start.tz = tz;
// Log.debug("corrected timezone="+event.start.tz)
}
}
Log.debug(`corrected tz=${event.start.tz}`);
let current_offset = 0; // offset from TZ string or calculated
let mm = 0; // date with tz or offset
let start_offset = 0; // utc offset of created with tz
// if there is still an offset, lookup failed, use it
if (event.start.tz.startsWith("(")) {
const regex = /[+|-]\d*:\d*/;
const start_offsetString = event.start.tz.match(regex).toString().split(":");
let start_offset = parseInt(start_offsetString[0]);
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
adjustHours = start_offset;
Log.debug(`defined offset=${start_offset} hours`);
current_offset = start_offset;
event.start.tz = "";
Log.debug(`ical offset=${current_offset} date=${date}`);
mm = moment(date);
let x = parseInt(moment(new Date()).utcOffset());
Log.debug(`net mins=${current_offset * 60 - x}`);
mm = mm.add(x - current_offset * 60, "minutes");
adjustHours = (current_offset * 60 - x) / 60;
event.start = mm.toDate();
Log.debug(`adjusted date=${event.start}`);
} else {
// get the start time in that timezone
let es = moment(event.start);
// check for start date prior to start of daylight changing date
if (es.format("YYYY") < 2007) {
es.set("year", 2013); // if so, use a closer date
}
Log.debug(`start date/time=${es.toDate()}`);
start_offset = moment.tz(es, event.start.tz).utcOffset();
Log.debug(`start offset=${start_offset}`);
Log.debug(`start date/time w tz =${moment.tz(moment(event.start), event.start.tz).toDate()}`);
// get the specified date in that timezone
mm = moment.tz(moment(date), event.start.tz);
Log.debug(`event date=${mm.toDate()}`);
current_offset = mm.utcOffset();
}
Log.debug(`event offset=${current_offset} hour=${mm.format("H")} event date=${mm.toDate()}`);
// if the offset is greater than 0, east of london
if (current_offset !== start_offset) {
// big offset
Log.debug("offset");
let h = parseInt(mm.format("H"));
// check if the event time is less than the offset
if (h > 0 && h < Math.abs(current_offset) / 60) {
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
// we need to fix that
//adjustHours = 24;
// Log.debug("adjusting date")
}
//-300 > -240
//if (Math.abs(current_offset) > Math.abs(start_offset)){
if (current_offset > start_offset) {
adjustHours -= 1;
Log.debug("adjust down 1 hour dst change");
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
} else if (current_offset < start_offset) {
adjustHours += 1;
Log.debug("adjust up 1 hour dst change");
}
}
}
Log.debug(`adjustHours=${adjustHours}`);
return adjustHours;
},
/**
* Filter the events from ical according to the given config
* @param {object} data the calendar data from ical
* @param {object} config The configuration object
* @returns {string[]} the filtered events
*/
filterEvents: function (data, config) {
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
const limitFunction = function (date, i) {
return true;
};
const eventDate = function (event, time) {
return CalendarFetcherUtils.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]) => {
Log.debug("Processing entry...");
const now = new Date();
const today = moment().startOf("day").toDate();
const future = moment().startOf("day").add(config.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;
if (config.includePastEvents) {
past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
}
// FIXME: Ugly fix to solve the facebook birthday issue.
// Otherwise, the recurring events only show the birthday for next year.
let isFacebookBirthday = false;
if (typeof event.uid !== "undefined") {
if (event.uid.indexOf("@facebook.com") !== -1) {
isFacebookBirthday = true;
}
}
if (event.type === "VEVENT") {
Log.debug(`Event:\n${JSON.stringify(event)}`);
let startDate = eventDate(event, "start");
let endDate;
if (typeof event.end !== "undefined") {
endDate = eventDate(event, "end");
} else if (typeof event.duration !== "undefined") {
endDate = startDate.clone().add(moment.duration(event.duration));
} else {
if (!isFacebookBirthday) {
// make copy of start date, separate storage area
endDate = moment(startDate.format("x"), "x");
} else {
endDate = moment(startDate).add(1, "days");
}
}
Log.debug(`start: ${startDate.toDate()}`);
Log.debug(`end:: ${endDate.toDate()}`);
// Calculate the duration of the event for use with recurring events.
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
Log.debug(`duration: ${duration}`);
// FIXME: Since the parsed json object from node-ical comes with time information
// this check could be removed (?)
if (event.start.length === 8) {
startDate = startDate.startOf("day");
}
const title = CalendarFetcherUtils.getTitleFromEvent(event);
Log.debug(`title: ${title}`);
let excluded = false,
dateFilter = null;
for (let f in config.excludedEvents) {
let filter = config.excludedEvents[f],
testTitle = title.toLowerCase(),
until = null,
useRegex = false,
regexFlags = "g";
if (filter instanceof Object) {
if (typeof filter.until !== "undefined") {
until = filter.until;
}
if (typeof filter.regex !== "undefined") {
useRegex = filter.regex;
}
// If additional advanced filtering is added in, this section
// must remain last as we overwrite the filter object with the
// filterBy string
if (filter.caseSensitive) {
filter = filter.filterBy;
testTitle = title;
} else if (useRegex) {
filter = filter.filterBy;
testTitle = title;
regexFlags += "i";
} else {
filter = filter.filterBy.toLowerCase();
}
} else {
filter = filter.toLowerCase();
}
if (CalendarFetcherUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
if (until) {
dateFilter = until;
} else {
excluded = true;
}
break;
}
}
if (excluded) {
return;
}
const location = event.location || false;
const geo = event.geo || false;
const description = event.description || false;
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
const rule = event.rrule;
let addedEvents = 0;
const pastMoment = moment(past);
const futureMoment = moment(future);
// can cause problems with e.g. birthdays before 1900
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
rule.origOptions.dtstart.setYear(1900);
rule.options.dtstart.setYear(1900);
}
// For recurring events, get the set of start dates that fall within the range
// of dates we're looking for.
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
let pastLocal = 0;
let futureLocal = 0;
if (CalendarFetcherUtils.isFullDayEvent(event)) {
Log.debug("fullday");
// if full day event, only use the date part of the ranges
pastLocal = pastMoment.toDate();
futureLocal = futureMoment.toDate();
Log.debug(`pastLocal: ${pastLocal}`);
Log.debug(`futureLocal: ${futureLocal}`);
} else {
// if we want past events
if (config.includePastEvents) {
// use the calculated past time for the between from
pastLocal = pastMoment.toDate();
} else {
// otherwise use NOW.. cause we shouldn't use any before now
pastLocal = moment().toDate(); //now
}
futureLocal = futureMoment.toDate(); // future
}
Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
// The "dates" array contains the set of dates within our desired date range range that are valid
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
// had its date changed from outside the range to inside the range. For the time being,
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
// because the logic below will filter out any recurrences that don't actually belong within
// our display range.
// Would be great if there was a better way to handle this.
Log.debug(`event.recurrences: ${event.recurrences}`);
if (event.recurrences !== undefined) {
for (let r in event.recurrences) {
// Only add dates that weren't already in the range we added from the rrule so that
// we don"t double-add those events.
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
dates.push(new Date(r));
}
}
}
// Loop through the set of date entries to see which recurrences should be added to our event list.
for (let d in dates) {
let date = dates[d];
// Remove the time information of each date by using its substring, using the following method:
// .toISOString().substring(0,10).
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
const dateKey = date.toISOString().substring(0, 10);
let curEvent = event;
let showRecurrence = true;
// Get the offset of today where we are processing
// This will be the correction, we need to apply.
let nowOffset = new Date().getTimezoneOffset();
// For full day events, the time might be off from RRULE/Luxon problem
// Get time zone offset of the rule calculated event
let dateoffset = date.getTimezoneOffset();
// Reduce the time by the following offset.
Log.debug(` recurring date is ${date} offset is ${dateoffset}`);
let dh = moment(date).format("HH");
Log.debug(` recurring date is ${date} offset is ${dateoffset / 60} Hour is ${dh}`);
if (CalendarFetcherUtils.isFullDayEvent(event)) {
Log.debug("Fullday");
// If the offset is negative (east of GMT), where the problem is
if (dateoffset < 0) {
if (dh < Math.abs(dateoffset / 60)) {
// if the rrule byweekday WAS explicitly set , correct it
// reduce the time by the offset
if (curEvent.rrule.origOptions.byweekday !== undefined) {
// Apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
}
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug(`new recurring date1 fulldate is ${date}`);
}
} else {
// if the timezones are the same, correct date if needed
//if (event.start.tz === moment.tz.guess()) {
// if the date hour is less than the offset
if (24 - dh <= Math.abs(dateoffset / 60)) {
// if the rrule byweekday WAS explicitly set , correct it
if (curEvent.rrule.origOptions.byweekday !== undefined) {
// apply the correction to the date/time back to right day
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
}
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug(`new recurring date2 fulldate is ${date}`);
}
//}
}
} else {
// not full day, but luxon can still screw up the date on the rule processing
// we need to correct the date to get back to the right event for
if (dateoffset < 0) {
// if the date hour is less than the offset
if (dh <= Math.abs(dateoffset / 60)) {
// if the rrule byweekday WAS explicitly set , correct it
if (curEvent.rrule.origOptions.byweekday !== undefined) {
// Reduce the time by t:
// Apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
}
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug(`new recurring date1 is ${date}`);
}
} else {
// if the timezones are the same, correct date if needed
//if (event.start.tz === moment.tz.guess()) {
// if the date hour is less than the offset
if (24 - dh <= Math.abs(dateoffset / 60)) {
// if the rrule byweekday WAS explicitly set , correct it
if (curEvent.rrule.origOptions.byweekday !== undefined) {
// apply the correction to the date/time back to right day
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
}
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug(`new recurring date2 is ${date}`);
}
//}
}
}
startDate = moment(date);
Log.debug(`Corrected startDate: ${startDate.toDate()}`);
let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, date);
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
curEvent = curEvent.recurrences[dateKey];
startDate = moment(curEvent.start);
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
}
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
// This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false;
}
Log.debug(`duration: ${duration}`);
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (startDate.format("x") === endDate.format("x")) {
endDate = endDate.endOf("day");
}
const recurrenceTitle = CalendarFetcherUtils.getTitleFromEvent(curEvent);
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
// it to the event list.
if (endDate.isBefore(past) || startDate.isAfter(future)) {
showRecurrence = false;
}
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
showRecurrence = false;
}
if (showRecurrence === true) {
Log.debug(`saving event: ${description}`);
addedEvents++;
newEvents.push({
title: recurrenceTitle,
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
fullDayEvent: CalendarFetcherUtils.isFullDayEvent(event),
recurringEvent: true,
class: event.class,
firstYear: event.start.getFullYear(),
location: location,
geo: geo,
description: description
});
}
}
// End recurring event parsing.
} else {
// Single event.
const fullDayEvent = isFacebookBirthday ? true : CalendarFetcherUtils.isFullDayEvent(event);
// Log.debug("full day event")
// 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")) {
endDate = endDate.endOf("day");
}
if (config.includePastEvents) {
// Past event is too far in the past, so skip.
if (endDate < past) {
return;
}
} else {
// It's not a fullday event, and it is in the past, so skip.
if (!fullDayEvent && endDate < new Date()) {
return;
}
// It's a fullday event, and it is before today, So skip.
if (fullDayEvent && endDate <= today) {
return;
}
}
// It exceeds the maximumNumberOfDays limit, so skip.
if (startDate > future) {
return;
}
if (CalendarFetcherUtils.timeFilterApplies(now, endDate, dateFilter)) {
return;
}
// get correction for date saving and dst change between now and then
let adjustDays = CalendarFetcherUtils.calculateTimezoneAdjustment(event, startDate.toDate());
// Every thing is good. Add it to the list.
newEvents.push({
title: title,
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
fullDayEvent: fullDayEvent,
class: event.class,
location: location,
geo: geo,
description: description
});
}
}
});
newEvents.sort(function (a, b) {
return a.startDate - b.startDate;
});
return newEvents;
},
/**
* Lookup iana tz from windows
* @param {string} msTZName the timezone name to lookup
* @returns {string|null} the iana name or null of none is found
*/
getIanaTZFromMS: function (msTZName) {
// Get hash entry
const he = zoneTable[msTZName];
// If found return iana name, else null
return he ? he.iana[0] : null;
},
/**
* Gets the title from the event.
* @param {object} event The event object to check.
* @returns {string} The title of the event, or "Event" if no title is found.
*/
getTitleFromEvent: function (event) {
let title = "Event";
if (event.summary) {
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
} else if (event.description) {
title = event.description;
}
return title;
},
/**
* Checks if an event is a fullday event.
* @param {object} event The event object to check.
* @returns {boolean} True if the event is a fullday event, false otherwise
*/
isFullDayEvent: function (event) {
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
return true;
}
const start = event.start || 0;
const startDate = new Date(start);
const end = event.end || 0;
if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
// Is 24 hours, and starts on the middle of the night.
return true;
}
return false;
},
/**
* Determines if the user defined time filter should apply
* @param {Date} now Date object using previously created object for consistency
* @param {Moment} endDate Moment object representing the event end date
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
* @returns {boolean} True if the event should be filtered out, false otherwise
*/
timeFilterApplies: function (now, endDate, filter) {
if (filter) {
const until = filter.split(" "),
value = parseInt(until[0]),
increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js
filterUntil = moment(endDate.format()).subtract(value, increment);
return now < filterUntil.format("x");
}
return false;
},
/**
* Determines if the user defined title filter should apply
* @param {string} title the title of the event
* @param {string} filter the string to look for, can be a regex also
* @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
* @param {string} regexFlags flags that should be applied to the regex
* @returns {boolean} True if the title should be filtered out, false otherwise
*/
titleFilterApplies: function (title, filter, useRegex, regexFlags) {
if (useRegex) {
let regexFilter = filter;
// Assume if leading slash, there is also trailing slash
if (filter[0] === "/") {
// Strip leading and trailing slashes
regexFilter = filter.substr(1).slice(0, -1);
}
return new RegExp(regexFilter, regexFlags).test(title);
} else {
return title.includes(filter);
}
}
};
if (typeof module !== "undefined") {
module.exports = CalendarFetcherUtils;
}

View File

@ -1,610 +1,114 @@
/* MagicMirror²
* Calendar Util Methods
*
* By Michael Teeuw https://michaelteeuw.nl
* By Rejas
* MIT Licensed.
*/
/**
* @external Moment
*/
const path = require("path");
const moment = require("moment");
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
const Log = require("../../../js/logger");
const CalendarUtils = {
/**
* Calculate the time correction, either dst/std or full day in cases where
* utc time is day before plus offset
*
* @param {object} event the event which needs adjustement
* @param {Date} date the date on which this event happens
* @returns {number} the necessary adjustment in hours
* Capitalize the first letter of a string
* @param {string} string The string to capitalize
* @returns {string} The capitalized string
*/
calculateTimezoneAdjustment: function (event, date) {
let adjustHours = 0;
// if a timezone was specified
if (!event.start.tz) {
Log.debug(" if no tz, guess based on now");
event.start.tz = moment.tz.guess();
}
Log.debug(`initial tz=${event.start.tz}`);
// if there is a start date specified
if (event.start.tz) {
// if this is a windows timezone
if (event.start.tz.includes(" ")) {
// use the lookup table to get theIANA name as moment and date don't know MS timezones
let tz = CalendarUtils.getIanaTZFromMS(event.start.tz);
Log.debug(`corrected TZ=${tz}`);
// watch out for unregistered windows timezone names
// if we had a successful lookup
if (tz) {
// change the timezone to the IANA name
event.start.tz = tz;
// Log.debug("corrected timezone="+event.start.tz)
}
}
Log.debug(`corrected tz=${event.start.tz}`);
let current_offset = 0; // offset from TZ string or calculated
let mm = 0; // date with tz or offset
let start_offset = 0; // utc offset of created with tz
// if there is still an offset, lookup failed, use it
if (event.start.tz.startsWith("(")) {
const regex = /[+|-]\d*:\d*/;
const start_offsetString = event.start.tz.match(regex).toString().split(":");
let start_offset = parseInt(start_offsetString[0]);
start_offset *= event.start.tz[1] === "-" ? -1 : 1;
adjustHours = start_offset;
Log.debug(`defined offset=${start_offset} hours`);
current_offset = start_offset;
event.start.tz = "";
Log.debug(`ical offset=${current_offset} date=${date}`);
mm = moment(date);
let x = parseInt(moment(new Date()).utcOffset());
Log.debug(`net mins=${current_offset * 60 - x}`);
mm = mm.add(x - current_offset * 60, "minutes");
adjustHours = (current_offset * 60 - x) / 60;
event.start = mm.toDate();
Log.debug(`adjusted date=${event.start}`);
} else {
// get the start time in that timezone
let es = moment(event.start);
// check for start date prior to start of daylight changing date
if (es.format("YYYY") < 2007) {
es.set("year", 2013); // if so, use a closer date
}
Log.debug(`start date/time=${es.toDate()}`);
start_offset = moment.tz(es, event.start.tz).utcOffset();
Log.debug(`start offset=${start_offset}`);
Log.debug(`start date/time w tz =${moment.tz(moment(event.start), event.start.tz).toDate()}`);
// get the specified date in that timezone
mm = moment.tz(moment(date), event.start.tz);
Log.debug(`event date=${mm.toDate()}`);
current_offset = mm.utcOffset();
}
Log.debug(`event offset=${current_offset} hour=${mm.format("H")} event date=${mm.toDate()}`);
// if the offset is greater than 0, east of london
if (current_offset !== start_offset) {
// big offset
Log.debug("offset");
let h = parseInt(mm.format("H"));
// check if the event time is less than the offset
if (h > 0 && h < Math.abs(current_offset) / 60) {
// if so, rrule created a wrong date (utc day, oops, with utc yesterday adjusted time)
// we need to fix that
//adjustHours = 24;
// Log.debug("adjusting date")
}
//-300 > -240
//if (Math.abs(current_offset) > Math.abs(start_offset)){
if (current_offset > start_offset) {
adjustHours -= 1;
Log.debug("adjust down 1 hour dst change");
//} else if (Math.abs(current_offset) < Math.abs(start_offset)) {
} else if (current_offset < start_offset) {
adjustHours += 1;
Log.debug("adjust up 1 hour dst change");
}
}
}
Log.debug(`adjustHours=${adjustHours}`);
return adjustHours;
capFirst: function (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
/**
* Filter the events from ical according to the given config
*
* @param {object} data the calendar data from ical
* @param {object} config The configuration object
* @returns {string[]} the filtered events
* This function accepts a number (either 12 or 24) and returns a moment.js LocaleSpecification with the
* corresponding time-format to be used in the calendar display. If no number is given (or otherwise invalid input)
* it will a localeSpecification object with the system locale time format.
* @param {number} timeFormat Specifies either 12 or 24-hour time format
* @returns {moment.LocaleSpecification} formatted time
*/
filterEvents: function (data, config) {
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
const limitFunction = function (date, i) {
return true;
};
const eventDate = function (event, time) {
return CalendarUtils.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]) => {
Log.debug("Processing entry...");
const now = new Date();
const today = moment().startOf("day").toDate();
const future = moment().startOf("day").add(config.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;
if (config.includePastEvents) {
past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
getLocaleSpecification: function (timeFormat) {
switch (timeFormat) {
case 12: {
return { longDateFormat: { LT: "h:mm A" } };
}
// FIXME: Ugly fix to solve the facebook birthday issue.
// Otherwise, the recurring events only show the birthday for next year.
let isFacebookBirthday = false;
if (typeof event.uid !== "undefined") {
if (event.uid.indexOf("@facebook.com") !== -1) {
isFacebookBirthday = true;
}
case 24: {
return { longDateFormat: { LT: "HH:mm" } };
}
default: {
return { longDateFormat: { LT: moment.localeData().longDateFormat("LT") } };
}
}
},
if (event.type === "VEVENT") {
Log.debug(`Event:\n${JSON.stringify(event)}`);
let startDate = eventDate(event, "start");
let endDate;
/**
* Shortens a string if it's longer than maxLength and add an ellipsis to the end
* @param {string} string Text string to shorten
* @param {number} maxLength The max length of the string
* @param {boolean} wrapEvents Wrap the text after the line has reached maxLength
* @param {number} maxTitleLines The max number of vertical lines before cutting event title
* @returns {string} The shortened string
*/
shorten: function (string, maxLength, wrapEvents, maxTitleLines) {
if (typeof string !== "string") {
return "";
}
if (typeof event.end !== "undefined") {
endDate = eventDate(event, "end");
} else if (typeof event.duration !== "undefined") {
endDate = startDate.clone().add(moment.duration(event.duration));
if (wrapEvents === true) {
const words = string.split(" ");
let temp = "";
let currentLine = "";
let line = 0;
for (let i = 0; i < words.length; i++) {
const word = words[i];
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
// max - 1 to account for a space
currentLine += `${word} `;
} else {
if (!isFacebookBirthday) {
// make copy of start date, separate storage area
endDate = moment(startDate.format("x"), "x");
} else {
endDate = moment(startDate).add(1, "days");
}
}
Log.debug(`start: ${startDate.toDate()}`);
Log.debug(`end:: ${endDate.toDate()}`);
// Calculate the duration of the event for use with recurring events.
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
Log.debug(`duration: ${duration}`);
// FIXME: Since the parsed json object from node-ical comes with time information
// this check could be removed (?)
if (event.start.length === 8) {
startDate = startDate.startOf("day");
}
const title = CalendarUtils.getTitleFromEvent(event);
Log.debug(`title: ${title}`);
let excluded = false,
dateFilter = null;
for (let f in config.excludedEvents) {
let filter = config.excludedEvents[f],
testTitle = title.toLowerCase(),
until = null,
useRegex = false,
regexFlags = "g";
if (filter instanceof Object) {
if (typeof filter.until !== "undefined") {
until = filter.until;
}
if (typeof filter.regex !== "undefined") {
useRegex = filter.regex;
}
// If additional advanced filtering is added in, this section
// must remain last as we overwrite the filter object with the
// filterBy string
if (filter.caseSensitive) {
filter = filter.filterBy;
testTitle = title;
} else if (useRegex) {
filter = filter.filterBy;
testTitle = title;
regexFlags += "i";
} else {
filter = filter.filterBy.toLowerCase();
}
} else {
filter = filter.toLowerCase();
}
if (CalendarUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
if (until) {
dateFilter = until;
} else {
excluded = true;
line++;
if (line > maxTitleLines - 1) {
if (i < words.length) {
currentLine += "…";
}
break;
}
}
if (excluded) {
return;
}
const location = event.location || false;
const geo = event.geo || false;
const description = event.description || false;
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
const rule = event.rrule;
let addedEvents = 0;
const pastMoment = moment(past);
const futureMoment = moment(future);
// can cause problems with e.g. birthdays before 1900
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
rule.origOptions.dtstart.setYear(1900);
rule.options.dtstart.setYear(1900);
}
// For recurring events, get the set of start dates that fall within the range
// of dates we're looking for.
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
let pastLocal = 0;
let futureLocal = 0;
if (CalendarUtils.isFullDayEvent(event)) {
Log.debug("fullday");
// if full day event, only use the date part of the ranges
pastLocal = pastMoment.toDate();
futureLocal = futureMoment.toDate();
Log.debug(`pastLocal: ${pastLocal}`);
Log.debug(`futureLocal: ${futureLocal}`);
if (currentLine.length > 0) {
temp += `${currentLine}<br>${word} `;
} else {
// if we want past events
if (config.includePastEvents) {
// use the calculated past time for the between from
pastLocal = pastMoment.toDate();
} else {
// otherwise use NOW.. cause we shouldn't use any before now
pastLocal = moment().toDate(); //now
}
futureLocal = futureMoment.toDate(); // future
temp += `${word}<br>`;
}
Log.debug(`Search for recurring events between: ${pastLocal} and ${futureLocal}`);
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
Log.debug(`Title: ${event.summary}, with dates: ${JSON.stringify(dates)}`);
// The "dates" array contains the set of dates within our desired date range range that are valid
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
// had its date changed from outside the range to inside the range. For the time being,
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
// because the logic below will filter out any recurrences that don't actually belong within
// our display range.
// Would be great if there was a better way to handle this.
Log.debug(`event.recurrences: ${event.recurrences}`);
if (event.recurrences !== undefined) {
for (let r in event.recurrences) {
// Only add dates that weren't already in the range we added from the rrule so that
// we don"t double-add those events.
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
dates.push(new Date(r));
}
}
}
// Loop through the set of date entries to see which recurrences should be added to our event list.
for (let d in dates) {
let date = dates[d];
// Remove the time information of each date by using its substring, using the following method:
// .toISOString().substring(0,10).
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
const dateKey = date.toISOString().substring(0, 10);
let curEvent = event;
let showRecurrence = true;
// Get the offset of today where we are processing
// This will be the correction, we need to apply.
let nowOffset = new Date().getTimezoneOffset();
// For full day events, the time might be off from RRULE/Luxon problem
// Get time zone offset of the rule calculated event
let dateoffset = date.getTimezoneOffset();
// Reduce the time by the following offset.
Log.debug(` recurring date is ${date} offset is ${dateoffset}`);
let dh = moment(date).format("HH");
Log.debug(` recurring date is ${date} offset is ${dateoffset / 60} Hour is ${dh}`);
if (CalendarUtils.isFullDayEvent(event)) {
Log.debug("Fullday");
// If the offset is negative (east of GMT), where the problem is
if (dateoffset < 0) {
if (dh < Math.abs(dateoffset / 60)) {
// if the rrule byweekday WAS explicitly set , correct it
// reduce the time by the offset
if (curEvent.rrule.origOptions.byweekday !== undefined) {
// Apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
}
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug(`new recurring date1 fulldate is ${date}`);
}
} else {
// if the timezones are the same, correct date if needed
//if (event.start.tz === moment.tz.guess()) {
// if the date hour is less than the offset
if (24 - dh <= Math.abs(dateoffset / 60)) {
// if the rrule byweekday WAS explicitly set , correct it
if (curEvent.rrule.origOptions.byweekday !== undefined) {
// apply the correction to the date/time back to right day
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
}
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug(`new recurring date2 fulldate is ${date}`);
}
//}
}
} else {
// not full day, but luxon can still screw up the date on the rule processing
// we need to correct the date to get back to the right event for
if (dateoffset < 0) {
// if the date hour is less than the offset
if (dh <= Math.abs(dateoffset / 60)) {
// if the rrule byweekday WAS explicitly set , correct it
if (curEvent.rrule.origOptions.byweekday !== undefined) {
// Reduce the time by t:
// Apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(24 * 60) * 60000);
}
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug(`new recurring date1 is ${date}`);
}
} else {
// if the timezones are the same, correct date if needed
//if (event.start.tz === moment.tz.guess()) {
// if the date hour is less than the offset
if (24 - dh <= Math.abs(dateoffset / 60)) {
// if the rrule byweekday WAS explicitly set , correct it
if (curEvent.rrule.origOptions.byweekday !== undefined) {
// apply the correction to the date/time back to right day
date = new Date(date.getTime() + Math.abs(24 * 60) * 60000);
}
// the duration was calculated way back at the top before we could correct the start time..
// fix it for this event entry
//duration = 24 * 60 * 60 * 1000;
Log.debug(`new recurring date2 is ${date}`);
}
//}
}
}
startDate = moment(date);
Log.debug(`Corrected startDate: ${startDate.toDate()}`);
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
curEvent = curEvent.recurrences[dateKey];
startDate = moment(curEvent.start);
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
}
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
// This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false;
}
Log.debug(`duration: ${duration}`);
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (startDate.format("x") === endDate.format("x")) {
endDate = endDate.endOf("day");
}
const recurrenceTitle = CalendarUtils.getTitleFromEvent(curEvent);
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
// it to the event list.
if (endDate.isBefore(past) || startDate.isAfter(future)) {
showRecurrence = false;
}
if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
showRecurrence = false;
}
if (showRecurrence === true) {
Log.debug(`saving event: ${description}`);
addedEvents++;
newEvents.push({
title: recurrenceTitle,
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
fullDayEvent: CalendarUtils.isFullDayEvent(event),
recurringEvent: true,
class: event.class,
firstYear: event.start.getFullYear(),
location: location,
geo: geo,
description: description
});
}
}
// End recurring event parsing.
} else {
// Single event.
const fullDayEvent = isFacebookBirthday ? true : CalendarUtils.isFullDayEvent(event);
// Log.debug("full day event")
if (config.includePastEvents) {
// Past event is too far in the past, so skip.
if (endDate < past) {
return;
}
} else {
// It's not a fullday event, and it is in the past, so skip.
if (!fullDayEvent && endDate < new Date()) {
return;
}
// It's a fullday event, and it is before today, So skip.
if (fullDayEvent && endDate <= today) {
return;
}
}
// It exceeds the maximumNumberOfDays limit, so skip.
if (startDate > future) {
return;
}
if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
return;
}
// 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")) {
endDate = endDate.endOf("day");
}
// get correction for date saving and dst change between now and then
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, startDate.toDate());
// Every thing is good. Add it to the list.
newEvents.push({
title: title,
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
fullDayEvent: fullDayEvent,
class: event.class,
location: location,
geo: geo,
description: description
});
currentLine = "";
}
}
});
newEvents.sort(function (a, b) {
return a.startDate - b.startDate;
});
return newEvents;
},
/**
* Lookup iana tz from windows
*
* @param {string} msTZName the timezone name to lookup
* @returns {string|null} the iana name or null of none is found
*/
getIanaTZFromMS: function (msTZName) {
// Get hash entry
const he = zoneTable[msTZName];
// If found return iana name, else null
return he ? he.iana[0] : null;
},
/**
* Gets the title from the event.
*
* @param {object} event The event object to check.
* @returns {string} The title of the event, or "Event" if no title is found.
*/
getTitleFromEvent: function (event) {
let title = "Event";
if (event.summary) {
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
} else if (event.description) {
title = event.description;
}
return title;
},
/**
* Checks if an event is a fullday event.
*
* @param {object} event The event object to check.
* @returns {boolean} True if the event is a fullday event, false otherwise
*/
isFullDayEvent: function (event) {
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
return true;
}
const start = event.start || 0;
const startDate = new Date(start);
const end = event.end || 0;
if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
// Is 24 hours, and starts on the middle of the night.
return true;
}
return false;
},
/**
* Determines if the user defined time filter should apply
*
* @param {Date} now Date object using previously created object for consistency
* @param {Moment} endDate Moment object representing the event end date
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
* @returns {boolean} True if the event should be filtered out, false otherwise
*/
timeFilterApplies: function (now, endDate, filter) {
if (filter) {
const until = filter.split(" "),
value = parseInt(until[0]),
increment = until[1].slice(-1) === "s" ? until[1] : `${until[1]}s`, // Massage the data for moment js
filterUntil = moment(endDate.format()).subtract(value, increment);
return now < filterUntil.format("x");
}
return false;
},
/**
* Determines if the user defined title filter should apply
*
* @param {string} title the title of the event
* @param {string} filter the string to look for, can be a regex also
* @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
* @param {string} regexFlags flags that should be applied to the regex
* @returns {boolean} True if the title should be filtered out, false otherwise
*/
titleFilterApplies: function (title, filter, useRegex, regexFlags) {
if (useRegex) {
// Assume if leading slash, there is also trailing slash
if (filter[0] === "/") {
// Strip leading and trailing slashes
filter = filter.substr(1).slice(0, -1);
}
filter = new RegExp(filter, regexFlags);
return filter.test(title);
return (temp + currentLine).trim();
} else {
return title.includes(filter);
if (maxLength && typeof maxLength === "number" && string.length > maxLength) {
return `${string.trim().slice(0, maxLength)}`;
} else {
return string.trim();
}
}
},
/**
* Transforms the title of an event for usage.
* Replaces parts of the text as defined in config.titleReplace.
* Shortens title based on config.maxTitleLength and config.wrapEvents
* @param {string} title The title to transform.
* @param {object} titleReplace Pairs of strings to be replaced in the title
* @returns {string} The transformed title.
*/
titleTransform: function (title, titleReplace) {
let transformedTitle = title;
for (let needle in titleReplace) {
const replacement = titleReplace[needle];
const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
if (regParts) {
// the parsed pattern is a regexp.
needle = new RegExp(regParts[1], regParts[2]);
}
transformedTitle = transformedTitle.replace(needle, replacement);
}
return transformedTitle;
}
};

View File

@ -33,7 +33,6 @@ module.exports = NodeHelper.create({
/**
* Creates a fetcher for a new url if it doesn't exist yet.
* Otherwise it reuses the existing one.
*
* @param {string} url The url of the calendar
* @param {number} fetchInterval How often does the calendar needs to be fetched in ms
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.

View File

@ -1,4 +1,4 @@
/* global SunCalc */
/* global SunCalc, formatTime */
/* MagicMirror²
* Module: Clock
@ -169,21 +169,6 @@ Module.register("clock", {
digitalWrapper.appendChild(timeWrapper);
}
/**
* Format the time according to the config
*
* @param {object} config The config of the module
* @param {object} time time to format
* @returns {string} The formatted time string
*/
function formatTime(config, time) {
let formatString = `${hourSymbol}:mm`;
if (config.showPeriod && config.timeFormat !== 24) {
formatString += config.showPeriodUpper ? "A" : "a";
}
return moment(time).format(formatString);
}
/****************************************************************
* Create wrappers for Sun Times, only if specified in config
*/
@ -296,9 +281,14 @@ Module.register("clock", {
*/
if (this.config.displayType === "analog") {
// Display only an analog clock
if (this.config.analogShowDate === "top") {
if (this.config.showDate) {
// Add date to the analog clock
dateWrapper.innerHTML = now.format(this.config.dateFormat);
wrapper.appendChild(dateWrapper);
}
if (this.config.analogShowDate === "bottom") {
wrapper.classList.add("clock-grid-bottom");
} else if (this.config.analogShowDate === "bottom") {
} else if (this.config.analogShowDate === "top") {
wrapper.classList.add("clock-grid-top");
}
wrapper.appendChild(analogWrapper);

View File

@ -52,7 +52,6 @@ Module.register("compliments", {
/**
* Generate a random index for a list of compliments.
*
* @param {string[]} compliments Array with compliments.
* @returns {number} a random index of given array
*/
@ -78,7 +77,6 @@ Module.register("compliments", {
/**
* Retrieve an array of compliments for the time of the day.
*
* @returns {string[]} array with compliments for the time of the day.
*/
complimentArray: function () {
@ -115,7 +113,6 @@ Module.register("compliments", {
/**
* Retrieve a file from the local filesystem
*
* @returns {Promise} Resolved when the file is loaded
*/
loadComplimentFile: async function () {
@ -127,7 +124,6 @@ Module.register("compliments", {
/**
* Retrieve a random compliment.
*
* @returns {string} a compliment
*/
getRandomCompliment: function () {

View File

@ -179,7 +179,6 @@ Module.register("newsfeed", {
/**
* Generate an ordered list of items for this configured module.
*
* @param {object} feeds An object with feeds returned by the node helper.
*/
generateFeed: function (feeds) {
@ -272,7 +271,6 @@ Module.register("newsfeed", {
/**
* Check if this module is configured to show this feed.
*
* @param {string} feedUrl Url of the feed to check.
* @returns {boolean} True if it is subscribed, false otherwise
*/
@ -287,7 +285,6 @@ Module.register("newsfeed", {
/**
* Returns title for the specific feed url.
*
* @param {string} feedUrl Url of the feed
* @returns {string} The title of the feed
*/

View File

@ -14,7 +14,6 @@ const NodeHelper = require("node_helper");
/**
* Responsible for requesting an update on the set interval and broadcasting the data.
*
* @param {string} url URL of the news feed.
* @param {number} reloadInterval Reload interval in milliseconds.
* @param {string} encoding Encoding of the feed.
@ -25,12 +24,13 @@ const NodeHelper = require("node_helper");
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings, useCorsProxy) {
let reloadTimer = null;
let items = [];
let reloadIntervalMS = reloadInterval;
let fetchFailedCallback = function () {};
let itemsReceivedCallback = function () {};
if (reloadInterval < 1000) {
reloadInterval = 1000;
if (reloadIntervalMS < 1000) {
reloadIntervalMS = 1000;
}
/* private methods */
@ -89,9 +89,9 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
try {
// 86400000 = 24 hours is mentioned in the docs as maximum value:
const ttlms = Math.min(minutes * 60 * 1000, 86400000);
if (ttlms > reloadInterval) {
reloadInterval = ttlms;
Log.info(`Newsfeed-Fetcher: reloadInterval set to ttl=${reloadInterval} for url ${url}`);
if (ttlms > reloadIntervalMS) {
reloadIntervalMS = ttlms;
Log.info(`Newsfeed-Fetcher: reloadInterval set to ttl=${reloadIntervalMS} for url ${url}`);
}
} catch (error) {
Log.warn(`Newsfeed-Fetcher: feed ttl is no valid integer=${minutes} for url ${url}`);
@ -129,19 +129,18 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
clearTimeout(reloadTimer);
reloadTimer = setTimeout(function () {
fetchNews();
}, reloadInterval);
}, reloadIntervalMS);
};
/* public methods */
/**
* Update the reload interval, but only if we need to increase the speed.
*
* @param {number} interval Interval for the update in milliseconds.
*/
this.setReloadInterval = function (interval) {
if (interval > 1000 && interval < reloadInterval) {
reloadInterval = interval;
if (interval > 1000 && interval < reloadIntervalMS) {
reloadIntervalMS = interval;
}
};

View File

@ -26,7 +26,6 @@ module.exports = NodeHelper.create({
/**
* Creates a fetcher for a new feed if it doesn't exist yet.
* Otherwise it reuses the existing one.
*
* @param {object} feed The feed object
* @param {object} config The configuration object
*/

View File

@ -9,6 +9,7 @@ const BASE_DIR = path.normalize(`${__dirname}/../../../`);
class GitHelper {
constructor() {
this.gitRepos = [];
this.gitResultList = [];
}
getRefRegex(branch) {
@ -93,18 +94,21 @@ class GitHelper {
// ## develop...origin/develop
// ## master...origin/master [behind 8]
// ## master...origin/master [ahead 8, behind 1]
// ## HEAD (no branch)
status = status.match(/## (.*)\.\.\.([^ ]*)(?: .*behind (\d+))?/);
// examples for status:
// [ '## develop...origin/develop', 'develop', 'origin/develop' ]
// [ '## master...origin/master [behind 8]', 'master', 'origin/master', '8' ]
// [ '## master...origin/master [ahead 8, behind 1]', 'master', 'origin/master', '1' ]
gitInfo.current = status[1];
gitInfo.tracking = status[2];
if (status) {
gitInfo.current = status[1];
gitInfo.tracking = status[2];
if (status[3]) {
// git fetch was already called before so `git status -sb` delivers already the behind number
gitInfo.behind = parseInt(status[3]);
gitInfo.isBehindInStatus = true;
if (status[3]) {
// git fetch was already called before so `git status -sb` delivers already the behind number
gitInfo.behind = parseInt(status[3]);
gitInfo.isBehindInStatus = true;
}
}
return gitInfo;
@ -113,7 +117,7 @@ class GitHelper {
async getRepoInfo(repo) {
const gitInfo = await this.getStatusInfo(repo);
if (!gitInfo) {
if (!gitInfo || !gitInfo.current) {
return;
}
@ -171,21 +175,38 @@ class GitHelper {
}
async getRepos() {
const gitResultList = [];
this.gitResultList = [];
for (const repo of this.gitRepos) {
try {
const gitInfo = await this.getRepoInfo(repo);
if (gitInfo) {
gitResultList.push(gitInfo);
this.gitResultList.push(gitInfo);
}
} catch (e) {
Log.error(`Failed to retrieve repo info for ${repo.module}: ${e}`);
}
}
return gitResultList;
return this.gitResultList;
}
async checkUpdates() {
var updates = [];
const allRepos = await this.gitResultList.map((module) => {
return new Promise((resolve) => {
if (module.behind > 0 && module.module !== "MagicMirror") {
Log.info(`Update found for module: ${module.module}`);
updates.push(module);
}
resolve(module);
});
});
await Promise.all(allRepos);
return updates;
}
}

View File

@ -25,15 +25,25 @@ module.exports = NodeHelper.create({
},
async socketNotificationReceived(notification, payload) {
if (notification === "CONFIG") {
this.config = payload;
} else if (notification === "MODULES") {
// if this is the 1st time thru the update check process
if (!this.updateProcessStarted) {
this.updateProcessStarted = true;
await this.configureModules(payload);
await this.performFetch();
}
switch (notification) {
case "CONFIG":
this.config = payload;
break;
case "MODULES":
// if this is the 1st time thru the update check process
if (!this.updateProcessStarted) {
this.updateProcessStarted = true;
await this.configureModules(payload);
await this.performFetch();
}
break;
case "SCAN_UPDATES":
// 1st time of check allows to force new scan
if (this.updateProcessStarted) {
clearTimeout(this.updateTimer);
await this.performFetch();
}
break;
}
},
@ -44,6 +54,11 @@ module.exports = NodeHelper.create({
this.sendSocketNotification("STATUS", repo);
}
if (this.config.sendUpdatesNotifications) {
const updates = await this.gitHelper.checkUpdates();
if (updates.length) this.sendSocketNotification("UPDATES", updates);
}
this.scheduleNextFetch(this.config.updateInterval);
},

View File

@ -8,7 +8,8 @@ Module.register("updatenotification", {
defaults: {
updateInterval: 10 * 60 * 1000, // every 10 minutes
refreshInterval: 24 * 60 * 60 * 1000, // one day
ignoreModules: []
ignoreModules: [],
sendUpdatesNotifications: false
},
suspended: false,
@ -33,15 +34,25 @@ Module.register("updatenotification", {
},
notificationReceived(notification) {
if (notification === "DOM_OBJECTS_CREATED") {
this.sendSocketNotification("CONFIG", this.config);
this.sendSocketNotification("MODULES", Object.keys(Module.definitions));
switch (notification) {
case "DOM_OBJECTS_CREATED":
this.sendSocketNotification("CONFIG", this.config);
this.sendSocketNotification("MODULES", Object.keys(Module.definitions));
break;
case "SCAN_UPDATES":
this.sendSocketNotification("SCAN_UPDATES");
break;
}
},
socketNotificationReceived(notification, payload) {
if (notification === "STATUS") {
this.updateUI(payload);
switch (notification) {
case "STATUS":
this.updateUI(payload);
break;
case "UPDATES":
this.sendNotification("UPDATES", payload);
break;
}
},

View File

@ -1,6 +1,5 @@
/**
* A function to make HTTP requests via the server to avoid CORS-errors.
*
* @param {string} url the url to fetch from
* @param {string} type what contenttype to expect in the response, can be "json" or "xml"
* @param {boolean} useCorsProxy A flag to indicate
@ -10,12 +9,14 @@
*/
async function performWebRequest(url, type = "json", useCorsProxy = false, requestHeaders = undefined, expectedResponseHeaders = undefined) {
const request = {};
let requestUrl;
if (useCorsProxy) {
url = getCorsUrl(url, requestHeaders, expectedResponseHeaders);
requestUrl = getCorsUrl(url, requestHeaders, expectedResponseHeaders);
} else {
requestUrl = url;
request.headers = getHeadersToSend(requestHeaders);
}
const response = await fetch(url, request);
const response = await fetch(requestUrl, request);
const data = await response.text();
if (type === "xml") {
@ -33,7 +34,6 @@ async function performWebRequest(url, type = "json", useCorsProxy = false, reque
/**
* Gets a URL that will be used when calling the CORS-method on the server.
*
* @param {string} url the url to fetch from
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
@ -64,7 +64,6 @@ const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders) {
/**
* Gets the part of the CORS URL that represents the HTTP headers to send.
*
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @returns {string} to be used as request-headers component in CORS URL.
*/
@ -85,7 +84,6 @@ const getRequestHeaderString = function (requestHeaders) {
/**
* Gets headers and values to attach to the web request.
*
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @returns {object} An object specifying name and value of the headers.
*/
@ -102,7 +100,6 @@ const getHeadersToSend = (requestHeaders) => {
/**
* Gets the part of the CORS URL that represents the expected HTTP headers to receive.
*
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
*/
@ -123,7 +120,6 @@ const getExpectedResponseHeadersString = function (expectedResponseHeaders) {
/**
* Gets the values for the expected headers from the response.
*
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @param {Response} response the HTTP response
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
@ -141,7 +137,36 @@ const getHeadersFromResponse = (expectedResponseHeaders, response) => {
return responseHeaders;
};
/**
* Format the time according to the config
* @param {object} config The config of the module
* @param {object} time time to format
* @returns {string} The formatted time string
*/
const formatTime = (config, time) => {
let date = moment(time);
if (config.timezone) {
date = date.tz(config.timezone);
}
if (config.timeFormat !== 24) {
if (config.showPeriod) {
if (config.showPeriodUpper) {
return date.format("h:mm A");
} else {
return date.format("h:mm a");
}
} else {
return date.format("h:mm");
}
}
return date.format("HH:mm");
};
if (typeof module !== "undefined")
module.exports = {
performWebRequest
performWebRequest,
formatTime
};

View File

@ -28,6 +28,12 @@
{% 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 light">
@ -61,12 +67,12 @@
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
</span><br/>
{% endif %}
{% if config.showPrecipitationAmount and current.precipitationAmount %}
{% 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 %}
{% if config.showPrecipitationProbability and current.precipitationProbability %}
<span class="dimmed">
<span class="precipitationLeadText">{{ "PRECIP_POP" | translate }}</span> {{ current.precipitationProbability }}%
</span>

View File

@ -32,6 +32,12 @@
{{ 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 %}

View File

@ -10,6 +10,14 @@
<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.showPrecipitationAmount %}
<td class="align-right bright precipitation-amount">
{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}

View File

@ -384,7 +384,7 @@ WeatherProvider.register("envcanada", {
const foreTime = moment(hourGroup[stepHour].getAttribute("dateTimeUTC"), "YYYYMMDDhhmmss");
const currTime = foreTime.add(hourOffset, "hours");
weather.date = moment.unix(currTime);
weather.date = moment(currTime);
// Capture the temperature
@ -505,9 +505,9 @@ WeatherProvider.register("envcanada", {
}
// Check Today element for POP
if (foreGroup[today].querySelector("abbreviatedForecast pop").textContent > 0) {
weather.precipitationProbability = foreGroup[today].querySelector("abbreviatedForecast pop").textContent;
const precipPOP = foreGroup[today].querySelector("abbreviatedForecast pop").textContent * 1.0;
if (precipPOP > 0) {
weather.precipitationProbability = precipPOP;
}
},

View File

@ -76,6 +76,10 @@ WeatherProvider.register("openmeteo", {
"et0_fao_evapotranspiration",
// Total precipitation (rain, showers, snow) sum of the preceding hour
"precipitation",
// Precipitation Probability
"precipitation_probability",
// UV index
"uv_index",
// Snowfall amount of the preceding hour in centimeters. For the water equivalent in millimeter, divide by 7. E.g. 7 cm snow = 10 mm precipitation water equivalent
"snowfall",
// Rain from large scale weather systems of the preceding hour in millimeter
@ -130,6 +134,8 @@ WeatherProvider.register("openmeteo", {
"winddirection_10m_dominant",
// The sum of solar radiation on a given day in Megajoules
"shortwave_radiation_sum",
//UV Index
"uv_index_max",
// Daily sum of ET₀ Reference Evapotranspiration of a well watered grass field
"et0_fao_evapotranspiration"
],
@ -194,7 +200,6 @@ WeatherProvider.register("openmeteo", {
/**
* Overrides method for setting config to check if endpoint is correct for hourly
*
* @param {object} config The configuration object
*/
setConfig(config) {
@ -382,6 +387,8 @@ WeatherProvider.register("openmeteo", {
currentWeather.rain = parseFloat(weather.hourly[h].rain);
currentWeather.snow = parseFloat(weather.hourly[h].snowfall * 10);
currentWeather.precipitationAmount = parseFloat(weather.hourly[h].precipitation);
currentWeather.precipitationProbability = parseFloat(weather.hourly[h].precipitation_probability);
currentWeather.uv_index = parseFloat(weather.hourly[h].uv_index);
return currentWeather;
},
@ -405,6 +412,8 @@ WeatherProvider.register("openmeteo", {
currentWeather.rain = parseFloat(weather.rain_sum);
currentWeather.snow = parseFloat(weather.snowfall_sum * 10);
currentWeather.precipitationAmount = parseFloat(weather.precipitation_sum);
currentWeather.precipitationProbability = parseFloat(weather.precipitation_probability);
currentWeather.uv_index = parseFloat(weather.uv_index_max);
days.push(currentWeather);
});
@ -438,6 +447,8 @@ WeatherProvider.register("openmeteo", {
currentWeather.rain = parseFloat(weather.rain);
currentWeather.snow = parseFloat(weather.snowfall * 10);
currentWeather.precipitationAmount = parseFloat(weather.precipitation);
currentWeather.precipitationProbability = parseFloat(weather.precipitation_probability);
currentWeather.uv_index = parseFloat(weather.uv_index);
hours.push(currentWeather);
});

View File

@ -90,7 +90,6 @@ WeatherProvider.register("openweathermap", {
/**
* Overrides method for setting config to check if endpoint is correct for hourly
*
* @param {object} config The configuration object
*/
setConfig(config) {
@ -435,6 +434,7 @@ WeatherProvider.register("openweathermap", {
} else if (this.firstEvent && this.firstEvent.location) {
params += `q=${this.firstEvent.location}`;
} else {
// TODO hide doesnt exist!
this.hide(this.config.animationSpeed, { lockString: this.identifier });
return;
}

View File

@ -69,7 +69,6 @@ WeatherProvider.register("smhi", {
/**
* Overrides method for setting config with checks for the precipitationValue being unset or invalid
*
* @param {object} config The configuration object
*/
setConfig(config) {
@ -82,7 +81,6 @@ WeatherProvider.register("smhi", {
/**
* 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 {object[]} times Array of time objects
* @returns {object} The weatherdata closest to the current time
*/
@ -100,7 +98,6 @@ WeatherProvider.register("smhi", {
/**
* Get the forecast url for the configured coordinates
*
* @returns {string} the url for the specified coordinates
*/
getURL() {
@ -115,7 +112,6 @@ WeatherProvider.register("smhi", {
/**
* Calculates the apparent temperature based on known atmospheric data.
*
* @param {object} weatherData Weatherdata to use for the calculation
* @returns {number} The apparent temperature
*/
@ -132,7 +128,6 @@ WeatherProvider.register("smhi", {
* 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 {object} weatherData Weatherdata to convert
* @param {object} coordinates Coordinates of the locations of the weather
* @returns {WeatherObject} The converted weatherdata at the specified location
@ -178,7 +173,6 @@ WeatherProvider.register("smhi", {
/**
* Takes all the data points and converts it to one WeatherObject per day.
*
* @param {object[]} allWeatherData Array of weatherdata
* @param {object} coordinates Coordinates of the locations of the weather
* @param {string} groupBy The interval to use for grouping the data (day, hour)
@ -230,7 +224,6 @@ WeatherProvider.register("smhi", {
/**
* Resolve coordinates from the response data (probably preferably to use
* this if it's not matching the config values exactly)
*
* @param {object} data Response data from the weather service
* @returns {{lon, lat}} the lat/long coordinates of the data
*/
@ -241,7 +234,6 @@ WeatherProvider.register("smhi", {
/**
* 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 {object[]} data Response data from the weather service
* @returns {object[]} Given data with filled gaps
*/
@ -263,7 +255,6 @@ 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
@ -276,7 +267,6 @@ WeatherProvider.register("smhi", {
* Map the icon value from SMHI to an icon that MagicMirror² understands.
* Uses different icons depending on if its daytime or nighttime.
* SMHI's description of what the numeric value means is the comment after the case.
*
* @param {number} input The SMHI icon value
* @param {boolean} isDayTime True if the icon should be for daytime, false for nighttime
* @returns {string} The icon name for the MagicMirror

View File

@ -189,7 +189,6 @@ WeatherProvider.register("ukmetoffice", {
/**
* Generates an url with api parameters based on the config.
*
* @param {string} forecastType daily or 3hourly forecast
* @returns {string} url
*/

View File

@ -65,7 +65,6 @@ WeatherProvider.register("weatherbit", {
/**
* Overrides method for setting config to check if endpoint is correct for hourly
*
* @param {object} config The configuration object
*/
setConfig(config) {

View File

@ -110,13 +110,15 @@ WeatherProvider.register("yr", {
this.getWeatherDataFromYr(weatherData?.downloadedAt)
.then((weatherData) => {
Log.debug("Got weather data from yr.");
let data;
if (weatherData) {
this.cacheWeatherData(weatherData);
data = weatherData;
} else {
//Undefined if unchanged
weatherData = this.getWeatherDataFromCache();
data = this.getWeatherDataFromCache();
}
resolve(weatherData);
resolve(data);
})
.catch((err) => {
Log.error(err);
@ -266,14 +268,14 @@ WeatherProvider.register("yr", {
this.getStellarDataFromYr(today, 2)
.then((stellarData) => {
if (stellarData) {
stellarData = {
const data = {
today: stellarData
};
stellarData.tomorrow = Object.assign({}, stellarData.today);
stellarData.today.date = today;
stellarData.tomorrow.date = tomorrow;
this.cacheStellarData(stellarData);
resolve(stellarData);
data.tomorrow = Object.assign({}, data.today);
data.today.date = today;
data.tomorrow.date = tomorrow;
this.cacheStellarData(data);
resolve(data);
} else {
Log.error(`Something went wrong when fetching stellar data. Responses: ${stellarData}`);
reject(stellarData);

View File

@ -30,7 +30,8 @@
}
.weather .precipitation-amount,
.weather .precipitation-prob {
.weather .precipitation-prob,
.weather .uv-index {
padding-left: 20px;
padding-right: 0;
}

View File

@ -1,4 +1,4 @@
/* global WeatherProvider, WeatherUtils */
/* global WeatherProvider, WeatherUtils, formatTime */
/* MagicMirror²
* Module: Weather
@ -27,6 +27,7 @@ Module.register("weather", {
showPeriodUpper: false,
showPrecipitationAmount: false,
showPrecipitationProbability: false,
showUVIndex: false,
showSun: true,
showWindDirection: true,
showWindDirectionAsArrow: false,
@ -211,50 +212,37 @@ Module.register("weather", {
this.nunjucksEnvironment().addFilter(
"formatTime",
function (date) {
date = moment(date);
if (this.config.timeFormat !== 24) {
if (this.config.showPeriod) {
if (this.config.showPeriodUpper) {
return date.format("h:mm A");
} else {
return date.format("h:mm a");
}
} else {
return date.format("h:mm");
}
}
return date.format("HH:mm");
return formatTime(this.config, date);
}.bind(this)
);
this.nunjucksEnvironment().addFilter(
"unit",
function (value, type, valueUnit) {
let formattedValue;
if (type === "temperature") {
value = `${this.roundValue(WeatherUtils.convertTemp(value, this.config.tempUnits))}°`;
formattedValue = `${this.roundValue(WeatherUtils.convertTemp(value, this.config.tempUnits))}°`;
if (this.config.degreeLabel) {
if (this.config.tempUnits === "metric") {
value += "C";
formattedValue += "C";
} else if (this.config.tempUnits === "imperial") {
value += "F";
formattedValue += "F";
} else {
value += "K";
formattedValue += "K";
}
}
} else if (type === "precip") {
if (value === null || isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
value = "";
formattedValue = "";
} else {
value = WeatherUtils.convertPrecipitationUnit(value, valueUnit, this.config.units);
formattedValue = WeatherUtils.convertPrecipitationUnit(value, valueUnit, this.config.units);
}
} else if (type === "humidity") {
value += "%";
formattedValue = `${value}%`;
} else if (type === "wind") {
value = WeatherUtils.convertWind(value, this.config.windUnits);
formattedValue = WeatherUtils.convertWind(value, this.config.windUnits);
}
return value;
return formattedValue;
}.bind(this)
);

View File

@ -75,7 +75,6 @@ class WeatherObject {
/**
* Determines if the sun sets or rises next. Uses the current time and not
* the date from the weather-forecast.
*
* @param {Moment} date an optional date where you want to get the next
* action for. Useful only in tests, defaults to the current time.
* @returns {string} "sunset" or "sunrise"
@ -93,7 +92,6 @@ class WeatherObject {
/**
* Checks if the weatherObject is at dayTime.
*
* @returns {boolean} true if it is at dayTime
*/
isDayTime() {
@ -105,7 +103,6 @@ class WeatherObject {
* Update the sunrise / sunset time depending on the location. This can be
* used if your provider doesn't provide that data by itself. Then SunCalc
* is used here to calculate them according to the location.
*
* @param {number} lat latitude
* @param {number} lon longitude
*/
@ -121,7 +118,6 @@ class WeatherObject {
*
* Before being handed to other modules, mutable values must be cloned safely.
* Especially 'moment' object is not immutable, so original 'date', 'sunrise', 'sunset' could be corrupted or changed by other modules.
*
* @returns {object} plained object clone of original weatherObject
*/
simpleClone() {

View File

@ -113,7 +113,6 @@ const WeatherProvider = Class.extend({
/**
* A convenience function to make requests.
*
* @param {string} url the url to fetch from
* @param {string} type what contenttype to expect in the response, can be "json" or "xml"
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
@ -138,7 +137,6 @@ WeatherProvider.providers = [];
/**
* Static method to register a new weather provider.
*
* @param {string} providerIdentifier The name of the weather provider
* @param {object} providerDetails The details of the weather provider
*/
@ -148,23 +146,22 @@ WeatherProvider.register = function (providerIdentifier, providerDetails) {
/**
* Static method to initialize a new weather provider.
*
* @param {string} providerIdentifier The name of the weather provider
* @param {object} delegate The weather module
* @returns {object} The new weather provider
*/
WeatherProvider.initialize = function (providerIdentifier, delegate) {
providerIdentifier = providerIdentifier.toLowerCase();
const pi = providerIdentifier.toLowerCase();
const provider = new WeatherProvider.providers[providerIdentifier]();
const provider = new WeatherProvider.providers[pi]();
const config = Object.assign({}, provider.defaults, delegate.config);
provider.delegate = delegate;
provider.setConfig(config);
provider.providerIdentifier = providerIdentifier;
provider.providerIdentifier = pi;
if (!provider.providerName) {
provider.providerName = providerIdentifier;
provider.providerName = pi;
}
return provider;

View File

@ -7,7 +7,6 @@
const WeatherUtils = {
/**
* Convert wind (from m/s) to beaufort scale
*
* @param {number} speedInMS the windspeed you want to convert
* @returns {number} the speed in beaufort
*/
@ -25,30 +24,30 @@ const WeatherUtils = {
/**
* Convert a value in a given unit to a string with a converted
* value and a postfix matching the output unit system.
*
* @param {number} value - The value to convert.
* @param {string} valueUnit - The unit the values has. Default is mm.
* @param {string} outputUnit - The unit system (imperial/metric) the return value should have.
* @returns {string} - A string with tha value and a unit postfix.
*/
convertPrecipitationUnit(value, valueUnit, outputUnit) {
if (outputUnit === "imperial") {
if (valueUnit && valueUnit.toLowerCase() === "cm") value = value * 0.3937007874;
else value = value * 0.03937007874;
valueUnit = "in";
} else {
valueUnit = valueUnit ? valueUnit : "mm";
}
if (valueUnit === "%") return `${value.toFixed(0)} ${valueUnit}`;
return `${value.toFixed(2)} ${valueUnit}`;
let convertedValue = value;
let conversionUnit = valueUnit;
if (outputUnit === "imperial") {
if (valueUnit && valueUnit.toLowerCase() === "cm") convertedValue = convertedValue * 0.3937007874;
else convertedValue = convertedValue * 0.03937007874;
conversionUnit = "in";
} else {
conversionUnit = valueUnit ? valueUnit : "mm";
}
return `${convertedValue.toFixed(2)} ${conversionUnit}`;
},
/**
* 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 {string} unit can be 'imperial' or 'metric'
* @returns {number} the converted temperature
@ -59,7 +58,6 @@ const WeatherUtils = {
/**
* Convert wind speed into another unit.
*
* @param {number} windInMS the windspeed in meter/sec you want to convert
* @param {string} unit can be 'beaufort', 'kmh', 'knots, 'imperial' (mph)
* or 'metric' (mps)

2276
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "magicmirror",
"version": "2.23.0",
"version": "2.24.0",
"description": "The open source modular smart mirror platform.",
"main": "js/electron.js",
"scripts": {
@ -51,43 +51,43 @@
"devDependencies": {
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-jsdoc": "^40.1.0",
"eslint-plugin-jest": "^27.2.2",
"eslint-plugin-jsdoc": "^46.4.2",
"eslint-plugin-prettier": "^4.2.1",
"express-basic-auth": "^1.2.1",
"husky": "^8.0.3",
"jest": "^29.5.0",
"jsdom": "^21.1.1",
"jsdom": "^22.1.0",
"lodash": "^4.17.21",
"playwright": "^1.32.1",
"prettier": "^2.8.7",
"playwright": "^1.35.1",
"prettier": "^2.8.8",
"pretty-quick": "^3.1.3",
"sinon": "^15.0.2",
"stylelint": "^15.3.0",
"stylelint-config-standard": "^31.0.0",
"sinon": "^15.2.0",
"stylelint": "^15.9.0",
"stylelint-config-standard": "^33.0.0",
"stylelint-prettier": "^3.0.0",
"suncalc": "^1.9.0"
},
"optionalDependencies": {
"electron": "^22.3.4"
"electron": "^25.2.0"
},
"dependencies": {
"colors": "^1.4.0",
"console-stamp": "^3.1.1",
"digest-fetch": "^2.0.1",
"digest-fetch": "^2.0.3",
"envsub": "^4.1.0",
"eslint": "^8.36.0",
"eslint": "^8.43.0",
"express": "^4.18.2",
"express-ipfilter": "^1.3.1",
"feedme": "^2.0.2",
"helmet": "^6.0.1",
"helmet": "^7.0.0",
"iconv-lite": "^0.6.3",
"luxon": "^1.28.1",
"module-alias": "^2.2.2",
"module-alias": "^2.2.3",
"moment": "^2.29.4",
"node-fetch": "^2.6.9",
"node-ical": "^0.16.0",
"socket.io": "^4.6.1"
"node-fetch": "^2.6.12",
"node-ical": "^0.16.1",
"socket.io": "^4.7.1"
},
"_moduleAliases": {
"node_helper": "js/node_helper.js",
@ -95,6 +95,6 @@
"fetch": "js/fetch.js"
},
"engines": {
"node": ">=14"
"node": ">=16"
}
}

View File

@ -9,7 +9,8 @@ let config = {
position: "middle_center",
config: {
displayType: "analog",
analogFace: "face-006"
analogFace: "face-006",
showDate: false
}
}
]

View File

@ -0,0 +1,25 @@
/* MagicMirror² Test config for default clock module
*
* By Johan Hammar
* MIT Licensed.
*/
let config = {
timeFormat: 12,
modules: [
{
module: "clock",
position: "middle_center",
config: {
showTime: true,
showDate: true,
displayType: "analog"
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@ -3,7 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
let config = require(`${process.cwd()}/tests/configs/default.js`).configFactory({
port: ${MM_PORT}
});

View File

@ -1,10 +1,17 @@
/**
* Suppresses errors concerning web server already shut down.
*
* @param {string} err The error message.
*/
const mockError = (err) => {
if (err.includes("ECONNREFUSED") || err.includes("ECONNRESET") || err.includes("socket hang up") || err.includes("exports is not defined") || err.includes("write EPIPE")) {
if (
err.includes("ECONNREFUSED") ||
err.includes("ECONNRESET") ||
err.includes("socket hang up") ||
err.includes("exports is not defined") ||
err.includes("write EPIPE") ||
err.includes("AggregateError") ||
err.includes("ERR_SOCKET_CONNECTION_TIMEOUT")
) {
jest.fn();
} else {
console.dir(err);

View File

@ -107,4 +107,18 @@ describe("Clock module", () => {
expect(elem).not.toBe(null);
});
});
describe("with analog clock face and date enabled", () => {
beforeAll(async () => {
await helpers.startApplication("tests/configs/modules/clock/clock_showDateAnalog.js");
await helpers.getDocument();
});
it("should show the analog clock face and the date", async () => {
const elemClock = helpers.waitForElement(".clock-circle");
await expect(elemClock).not.toBe(null);
const elemDate = helpers.waitForElement(".clock .date");
await expect(elemDate).not.toBe(null);
});
});
});

View File

@ -3,7 +3,6 @@ const helpers = require("../helpers/global-setup");
describe("Compliments module", () => {
/**
* move similar tests in function doTest
*
* @param {Array} complimentsArray The array of compliments.
*/
const doTest = async (complimentsArray) => {
@ -25,7 +24,7 @@ describe("Compliments module", () => {
await helpers.getDocument();
});
it("Show anytime because if configure empty parts of day compliments and set anytime compliments", async () => {
it("shows anytime because if configure empty parts of day compliments and set anytime compliments", async () => {
await doTest(["Anytime here"]);
});
});
@ -36,7 +35,7 @@ describe("Compliments module", () => {
await helpers.getDocument();
});
it("Show anytime compliments", async () => {
it("shows anytime compliments", async () => {
await doTest(["Anytime here"]);
});
});

View File

@ -9,13 +9,13 @@ describe("Check configuration without modules", () => {
await helpers.stopApplication();
});
it("Show the message MagicMirror² title", async () => {
it("shows the message MagicMirror² title", async () => {
const elem = await helpers.waitForElement("#module_1_helloworld .module-content");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("MagicMirror²");
});
it("Show the url of michael's website", async () => {
it("shows the url of michael's website", async () => {
const elem = await helpers.waitForElement("#module_5_helloworld .module-content");
expect(elem).not.toBe(null);
expect(elem.textContent).toContain("www.michaelteeuw.nl");

View File

@ -0,0 +1,28 @@
const helpers = require("./helpers/global-setup");
const delay = (time) => {
return new Promise((resolve) => setTimeout(resolve, time));
};
describe("App environment", () => {
let serverProcess;
beforeAll(async () => {
process.env.MM_CONFIG_FILE = "tests/configs/default.js";
serverProcess = await require("child_process").spawn("npm", ["run", "server"], { env: process.env, detached: true });
// we have to wait until the server is startet
await delay(2000);
});
afterAll(async () => {
await process.kill(-serverProcess.pid);
});
it("get request from http://localhost:8080 should return 200", async () => {
const res = await helpers.fetch("http://localhost:8080");
expect(res.status).toBe(200);
});
it("get request from http://localhost:8080/nothing should return 404", async () => {
const res = await helpers.fetch("http://localhost:8080/nothing");
expect(res.status).toBe(404);
});
});

View File

@ -3,7 +3,6 @@ const helpers = require("../helpers/global-setup");
describe("Calendar module", () => {
/**
* move similar tests in function doTest
*
* @param {string} cssClass css selector
*/
const doTest = async (cssClass) => {

View File

@ -3,7 +3,6 @@ const helpers = require("../helpers/global-setup");
describe("Compliments module", () => {
/**
* move similar tests in function doTest
*
* @param {Array} complimentsArray The array of compliments.
*/
const doTest = async (complimentsArray) => {
@ -36,7 +35,7 @@ describe("Compliments module", () => {
describe("Feature date in compliments module", () => {
describe("Set date and empty compliments for anytime, morning, evening and afternoon", () => {
it("Show happy new year compliment on new years day", async () => {
it("shows happy new year compliment on new years day", async () => {
await helpers.startApplication("tests/configs/modules/compliments/compliments_date.js", "01 Jan 2022 10:00:00 GMT");
await doTest(["Happy new year!"]);
});

View File

@ -0,0 +1,3 @@
module.exports = async () => {
process.env.TZ = "Europe/Berlin";
};

View File

@ -0,0 +1,53 @@
global.moment = require("moment-timezone");
const CalendarFetcherUtils = require("../../../../../modules/default/calendar/calendarfetcherutils");
describe("Calendar fetcher utils test", () => {
const defaultConfig = {
excludedEvents: [],
includePastEvents: false,
maximumEntries: 10,
maximumNumberOfDays: 365
};
describe("filterEvents", () => {
it("should return only ongoing and upcoming non full day events", () => {
const minusOneHour = moment().subtract(1, "hours").toDate();
const minusTwoHours = moment().subtract(2, "hours").toDate();
const plusOneHour = moment().add(1, "hours").toDate();
const plusTwoHours = moment().add(2, "hours").toDate();
const filteredEvents = CalendarFetcherUtils.filterEvents(
{
pastEvent: { type: "VEVENT", start: minusTwoHours, end: minusOneHour, summary: "pastEvent" },
ongoingEvent: { type: "VEVENT", start: minusOneHour, end: plusOneHour, summary: "ongoingEvent" },
upcomingEvent: { type: "VEVENT", start: plusOneHour, end: plusTwoHours, summary: "upcomingEvent" }
},
defaultConfig
);
expect(filteredEvents.length).toEqual(2);
expect(filteredEvents[0].title).toBe("ongoingEvent");
expect(filteredEvents[1].title).toBe("upcomingEvent");
});
it("should return only ongoing and upcoming full day events", () => {
const yesterday = moment().subtract(1, "days").startOf("day").toDate();
const today = moment().startOf("day").toDate();
const tomorrow = moment().add(1, "days").startOf("day").toDate();
const filteredEvents = CalendarFetcherUtils.filterEvents(
{
pastEvent: { type: "VEVENT", start: yesterday, end: yesterday, summary: "pastEvent" },
ongoingEvent: { type: "VEVENT", start: today, end: today, summary: "ongoingEvent" },
upcomingEvent: { type: "VEVENT", start: tomorrow, end: tomorrow, summary: "upcomingEvent" }
},
defaultConfig
);
expect(filteredEvents.length).toEqual(2);
expect(filteredEvents[0].title).toBe("ongoingEvent");
expect(filteredEvents[1].title).toBe("upcomingEvent");
});
});
});

View File

@ -1,18 +1,8 @@
global.moment = require("moment");
describe("Functions into modules/default/calendar/calendar.js", () => {
// Fake for use by calendar.js
Module = {};
Module.definitions = {};
Module.register = (name, moduleDefinition) => {
Module.definitions[name] = moduleDefinition;
};
beforeAll(() => {
// load calendar.js
require("../../../modules/default/calendar/calendar");
});
const CalendarUtils = require("../../../../../modules/default/calendar/calendarutils");
describe("Calendar utils tests", () => {
describe("capFirst", () => {
const words = {
rodrigo: "Rodrigo",
@ -24,68 +14,88 @@ describe("Functions into modules/default/calendar/calendar.js", () => {
Object.keys(words).forEach((word) => {
it(`for '${word}' should return '${words[word]}'`, () => {
expect(Module.definitions.calendar.capFirst(word)).toBe(words[word]);
expect(CalendarUtils.capFirst(word)).toBe(words[word]);
});
});
it("should not capitalize other letters", () => {
expect(CalendarUtils.capFirst("event")).not.toBe("EVent");
});
});
describe("getLocaleSpecification", () => {
it("should return a valid moment.LocaleSpecification for a 12-hour format", () => {
expect(Module.definitions.calendar.getLocaleSpecification(12)).toEqual({ longDateFormat: { LT: "h:mm A" } });
expect(CalendarUtils.getLocaleSpecification(12)).toEqual({ longDateFormat: { LT: "h:mm A" } });
});
it("should return a valid moment.LocaleSpecification for a 24-hour format", () => {
expect(Module.definitions.calendar.getLocaleSpecification(24)).toEqual({ longDateFormat: { LT: "HH:mm" } });
expect(CalendarUtils.getLocaleSpecification(24)).toEqual({ longDateFormat: { LT: "HH:mm" } });
});
it("should return the current system locale when called without timeFormat number", () => {
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: moment.localeData().longDateFormat("LT") } });
expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: moment.localeData().longDateFormat("LT") } });
});
it("should return a 12-hour longDateFormat when using the 'en' locale", () => {
const localeBackup = moment.locale();
moment.locale("en");
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
moment.locale(localeBackup);
});
it("should return a 12-hour longDateFormat when using the 'au' locale", () => {
const localeBackup = moment.locale();
moment.locale("au");
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
moment.locale(localeBackup);
});
it("should return a 12-hour longDateFormat when using the 'eg' locale", () => {
const localeBackup = moment.locale();
moment.locale("eg");
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "h:mm A" } });
moment.locale(localeBackup);
});
it("should return a 24-hour longDateFormat when using the 'nl' locale", () => {
const localeBackup = moment.locale();
moment.locale("nl");
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup);
});
it("should return a 24-hour longDateFormat when using the 'fr' locale", () => {
const localeBackup = moment.locale();
moment.locale("fr");
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup);
});
it("should return a 24-hour longDateFormat when using the 'uk' locale", () => {
const localeBackup = moment.locale();
moment.locale("uk");
expect(Module.definitions.calendar.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
expect(CalendarUtils.getLocaleSpecification()).toEqual({ longDateFormat: { LT: "HH:mm" } });
moment.locale(localeBackup);
});
});
describe("shorten", () => {
it("should not shorten if short enough", () => {
expect(CalendarUtils.shorten("Event 1", 10, false, 1)).toBe("Event 1");
});
it("should shorten into one line", () => {
expect(CalendarUtils.shorten("Example event at 12 o clock", 10, true, 1)).toBe("Example …");
});
it("should shorten into three lines", () => {
expect(CalendarUtils.shorten("Example event at 12 o clock", 10, true, 3)).toBe("Example <br>event at 12 o <br>clock");
});
it("should not shorten into three lines if wrap is false", () => {
expect(CalendarUtils.shorten("Example event at 12 o clock", 10, false, 3)).toBe("Example ev…");
});
const strings = {
" String with whitespace at the beginning that needs trimming": { length: 16, return: "String with whit…" },
"long string that needs shortening": { length: 16, return: "long string that…" },
@ -95,34 +105,44 @@ describe("Functions into modules/default/calendar/calendar.js", () => {
Object.keys(strings).forEach((string) => {
it(`for '${string}' should return '${strings[string].return}'`, () => {
expect(Module.definitions.calendar.shorten(string, strings[string].length)).toBe(strings[string].return);
expect(CalendarUtils.shorten(string, strings[string].length)).toBe(strings[string].return);
});
});
it("should return an empty string if shorten is called with a non-string", () => {
expect(Module.definitions.calendar.shorten(100)).toBe("");
expect(CalendarUtils.shorten(100)).toBe("");
});
it("should not shorten the string if shorten is called with a non-number maxLength", () => {
expect(Module.definitions.calendar.shorten("This is a test string", "This is not a number")).toBe("This is a test string");
expect(CalendarUtils.shorten("This is a test string", "This is not a number")).toBe("This is a test string");
});
it("should wrap the string instead of shorten it if shorten is called with wrapEvents = true (with maxLength defined as 20)", () => {
expect(Module.definitions.calendar.shorten("This is a wrapEvent test. Should wrap the string instead of shorten it if called with wrapEvent = true", 20, true)).toBe(
expect(CalendarUtils.shorten("This is a wrapEvent test. Should wrap the string instead of shorten it if called with wrapEvent = true", 20, true)).toBe(
"This is a <br>wrapEvent test. Should wrap <br>the string instead of <br>shorten it if called with <br>wrapEvent = true"
);
});
it("should wrap the string instead of shorten it if shorten is called with wrapEvents = true (without maxLength defined, default 25)", () => {
expect(Module.definitions.calendar.shorten("This is a wrapEvent test. Should wrap the string instead of shorten it if called with wrapEvent = true", undefined, true)).toBe(
expect(CalendarUtils.shorten("This is a wrapEvent test. Should wrap the string instead of shorten it if called with wrapEvent = true", undefined, true)).toBe(
"This is a wrapEvent <br>test. Should wrap the string <br>instead of shorten it if called <br>with wrapEvent = true"
);
});
it("should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", () => {
expect(Module.definitions.calendar.shorten("This is a wrapEvent and maxTitleLines test. Should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", undefined, true, 2)).toBe(
expect(CalendarUtils.shorten("This is a wrapEvent and maxTitleLines test. Should wrap and shorten the string in the second line if called with wrapEvents = true and maxTitleLines = 2", undefined, true, 2)).toBe(
"This is a wrapEvent and <br>maxTitleLines test. Should wrap and …"
);
});
});
describe("titleTransform and shorten combined", () => {
it("should replace the birthday and wrap nicely", () => {
const transformedTitle = CalendarUtils.titleTransform("Michael Teeuw's birthday", {
"De verjaardag van ": "",
"'s birthday": ""
});
expect(CalendarUtils.shorten(transformedTitle, 10, true, 2)).toBe("Michael <br>Teeuw");
});
});
});

View File

@ -1,23 +1,22 @@
const { performWebRequest } = require("../../../../modules/default/utils");
global.moment = require("moment-timezone");
const { performWebRequest, formatTime } = require("../../../../modules/default/utils");
const nodeVersion = process.version.match(/^v(\d+)\.*/)[1];
describe("Utils tests", () => {
describe("The performWebRequest-method", () => {
describe("Default modules utils tests", () => {
describe("performWebRequest", () => {
if (nodeVersion > 18) {
const locationHost = "localhost:8080";
const locationProtocol = "http";
let fetchResponse;
let fetchMock;
let url;
let urlToCall;
beforeEach(() => {
fetchResponse = new Response();
global.fetch = jest.fn(() => Promise.resolve(fetchResponse));
fetchMock = global.fetch;
url = "www.test.com";
});
describe("When using cors proxy", () => {
@ -29,24 +28,23 @@ describe("Utils tests", () => {
});
test("Calls correct URL once", async () => {
const urlToCall = "http://www.test.com/path?param1=value1";
url = urlToCall;
urlToCall = "http://www.test.com/path?param1=value1";
await performWebRequest(url, "json", true);
await performWebRequest(urlToCall, "json", true);
expect(fetchMock.mock.calls.length).toBe(1);
expect(fetchMock.mock.calls[0][0]).toBe(`${locationProtocol}//${locationHost}/cors?url=${urlToCall}`);
});
test("Sends correct headers", async () => {
const urlToCall = "http://www.test.com/path?param1=value1";
url = urlToCall;
urlToCall = "http://www.test.com/path?param1=value1";
const headers = [
{ name: "header1", value: "value1" },
{ name: "header2", value: "value2" }
];
await performWebRequest(url, "json", true, headers);
await performWebRequest(urlToCall, "json", true, headers);
expect(fetchMock.mock.calls.length).toBe(1);
expect(fetchMock.mock.calls[0][0]).toBe(`${locationProtocol}//${locationHost}/cors?sendheaders=header1:value1,header2:value2&url=${urlToCall}`);
@ -55,24 +53,22 @@ describe("Utils tests", () => {
describe("When not using cors proxy", () => {
test("Calls correct URL once", async () => {
const urlToCall = "http://www.test.com/path?param1=value1";
url = urlToCall;
urlToCall = "http://www.test.com/path?param1=value1";
await performWebRequest(url);
await performWebRequest(urlToCall);
expect(fetchMock.mock.calls.length).toBe(1);
expect(fetchMock.mock.calls[0][0]).toBe(urlToCall);
});
test("Sends correct headers", async () => {
const urlToCall = "http://www.test.com/path?param1=value1";
url = urlToCall;
urlToCall = "http://www.test.com/path?param1=value1";
const headers = [
{ name: "header1", value: "value1" },
{ name: "header2", value: "value2" }
];
await performWebRequest(url, "json", false, headers);
await performWebRequest(urlToCall, "json", false, headers);
const expectedHeaders = { headers: { header1: "value1", header2: "value2" } };
expect(fetchMock.mock.calls.length).toBe(1);
@ -82,23 +78,27 @@ describe("Utils tests", () => {
describe("When receiving json format", () => {
test("Returns undefined when no data is received", async () => {
const response = await performWebRequest(url);
urlToCall = "www.test.com";
const response = await performWebRequest(urlToCall);
expect(response).toBe(undefined);
});
test("Returns object when data is received", async () => {
urlToCall = "www.test.com";
fetchResponse = new Response('{"body": "some content"}');
const response = await performWebRequest(url);
const response = await performWebRequest(urlToCall);
expect(response.body).toBe("some content");
});
test("Returns expected headers when data is received", async () => {
urlToCall = "www.test.com";
fetchResponse = new Response('{"body": "some content"}', { headers: { header1: "value1", header2: "value2" } });
const response = await performWebRequest(url, "json", false, undefined, ["header1"]);
const response = await performWebRequest(urlToCall, "json", false, undefined, ["header1"]);
expect(response.headers.length).toBe(1);
expect(response.headers[0].name).toBe("header1");
@ -109,4 +109,64 @@ describe("Utils tests", () => {
test("Always ok, need one test", () => {});
}
});
describe("formatTime", () => {
const time = new Date();
beforeEach(async () => {
time.setHours(13, 13);
});
it("should convert correctly according to the config", () => {
expect(
formatTime(
{
timeFormat: 24
},
time
)
).toBe("13:13");
expect(
formatTime(
{
showPeriod: true,
showPeriodUpper: true,
timeFormat: 12
},
time
)
).toBe("1:13 PM");
expect(
formatTime(
{
showPeriod: true,
showPeriodUpper: false,
timeFormat: 12
},
time
)
).toBe("1:13 pm");
expect(
formatTime(
{
showPeriod: false,
timeFormat: 12
},
time
)
).toBe("1:13");
});
it("should convert correctly into another timezone", () => {
expect(
formatTime(
{
timeFormat: 24,
timezone: "America/Toronto"
},
time
)
).toBe("07:13");
});
});
});

View File

@ -1,5 +1,4 @@
const WeatherObject = require("../../../modules/default/weather/weatherobject");
const WeatherUtils = require("../../../modules/default/weather/weatherutils");
const WeatherObject = require("../../../../../modules/default/weather/weatherobject");
global.moment = require("moment-timezone");
global.SunCalc = require("suncalc");
@ -47,38 +46,3 @@ describe("WeatherObject", () => {
moment.tz.setDefault(originalTimeZone);
});
});
describe("WeatherUtils", () => {
it("should convert windspeed correctly from mps to beaufort", () => {
expect(Math.round(WeatherUtils.convertWind(5, "beaufort"))).toBe(3);
expect(Math.round(WeatherUtils.convertWind(300, "beaufort"))).toBe(12);
});
it("should convert windspeed correctly from mps to kmh", () => {
expect(Math.round(WeatherUtils.convertWind(11.75, "kmh"))).toBe(42);
});
it("should convert windspeed correctly from mps to knots", () => {
expect(Math.round(WeatherUtils.convertWind(10, "knots"))).toBe(19);
});
it("should convert windspeed correctly from mph to mps", () => {
expect(Math.round(WeatherUtils.convertWindToMetric(93.951324266285))).toBe(42);
});
it("should convert windspeed correctly from kmh to mps", () => {
expect(Math.round(WeatherUtils.convertWindToMs(151.2))).toBe(42);
});
it("should convert wind direction correctly from cardinal to value", () => {
expect(WeatherUtils.convertWindDirection("SSE")).toBe(157);
});
it("should return a calculated feelsLike info", () => {
expect(WeatherUtils.calculateFeelsLike(0, 20, 40)).toBe(-9.444444444444445);
});
it("should return a calculated feelsLike info", () => {
expect(WeatherUtils.calculateFeelsLike(30, 0, 60)).toBe(32.8320322777777);
});
});

View File

@ -1,45 +1,99 @@
const weather = require("../../../../../modules/default/weather/weatherutils");
const WeatherUtils = require("../../../../../modules/default/weather/weatherutils");
describe("Weather utils tests", () => {
describe("convertPrecipitationUnit tests", () => {
it("Should keep value and unit if outputUnit is undefined", () => {
describe("windspeed conversion", () => {
it("should convert windspeed correctly from mps to beaufort", () => {
expect(Math.round(WeatherUtils.convertWind(5, "beaufort"))).toBe(3);
expect(Math.round(WeatherUtils.convertWind(300, "beaufort"))).toBe(12);
});
it("should convert windspeed correctly from mps to mps", () => {
expect(WeatherUtils.convertWind(11.75, "FOOBAR")).toBe(11.75);
});
it("should convert windspeed correctly from mps to kmh", () => {
expect(Math.round(WeatherUtils.convertWind(11.75, "kmh"))).toBe(42);
});
it("should convert windspeed correctly from mps to knots", () => {
expect(Math.round(WeatherUtils.convertWind(10, "knots"))).toBe(19);
});
it("should convert windspeed correctly from mph to mps", () => {
expect(Math.round(WeatherUtils.convertWindToMetric(93.951324266285))).toBe(42);
});
it("should convert windspeed correctly from kmh to mps", () => {
expect(Math.round(WeatherUtils.convertWindToMs(151.2))).toBe(42);
});
});
describe("wind direction conversion", () => {
it("should convert wind direction correctly from cardinal to value", () => {
expect(WeatherUtils.convertWindDirection("SSE")).toBe(157);
});
});
describe("feelsLike calculation", () => {
it("should return a calculated feelsLike info", () => {
expect(WeatherUtils.calculateFeelsLike(0, 20, 40)).toBe(-9.444444444444445);
});
it("should return a calculated feelsLike info", () => {
expect(WeatherUtils.calculateFeelsLike(30, 0, 60)).toBe(32.8320322777777);
});
});
describe("precipitationUnit conversion", () => {
it("should keep value and unit if outputUnit is undefined", () => {
const values = [1, 2];
const units = ["mm", "cm"];
for (let i = 0; i < values.length; i++) {
var result = weather.convertPrecipitationUnit(values[i], units[i], undefined);
const result = weather.convertPrecipitationUnit(values[i], units[i], undefined);
expect(result).toBe(`${values[i].toFixed(2)} ${units[i]}`);
}
});
it("Should keep value and unit if outputUnit is metric", () => {
it("should keep value and unit if outputUnit is metric", () => {
const values = [1, 2];
const units = ["mm", "cm"];
for (let i = 0; i < values.length; i++) {
var result = weather.convertPrecipitationUnit(values[i], units[i], "metric");
const result = weather.convertPrecipitationUnit(values[i], units[i], "metric");
expect(result).toBe(`${values[i].toFixed(2)} ${units[i]}`);
}
});
it("Should use mm unit if input unit is undefined", () => {
it("should use mm unit if input unit is undefined", () => {
const values = [1, 2];
for (let i = 0; i < values.length; i++) {
var result = weather.convertPrecipitationUnit(values[i], undefined, "metric");
const result = weather.convertPrecipitationUnit(values[i], undefined, "metric");
expect(result).toBe(`${values[i].toFixed(2)} mm`);
}
});
it("Should convert value and unit if outputUnit is imperial", () => {
it("should convert value and unit if outputUnit is imperial", () => {
const values = [1, 2];
const units = ["mm", "cm"];
const expectedValues = [0.04, 0.79];
for (let i = 0; i < values.length; i++) {
var result = weather.convertPrecipitationUnit(values[i], units[i], "imperial");
const result = weather.convertPrecipitationUnit(values[i], units[i], "imperial");
expect(result).toBe(`${expectedValues[i]} in`);
}
});
it("should round percentage values regardless of output units", () => {
const values = [0.1, 2.22, 9.999];
const output = [undefined, "imperial", "metric"];
const result = ["0 %", "2 %", "10 %"];
for (let i = 0; i < values.length; i++) {
expect(weather.convertPrecipitationUnit(values[i], "%", output[i])).toBe(result[i]);
}
});
});
});

43
th.json
View File

@ -1,43 +0,0 @@
{
"LOADING": "กำลังโหลด …",
"TODAY": "วันนี้",
"TOMORROW": "พรุ่งนี้",
"RUNNING": "สิ้นสุดใน",
"EMPTY": "ไม่มีกิจกรรมที่กำลังจะมาถึง",
"WEEK": "สัปดาห์ที่ {weekNumber}",
"N": "น",
"NNE": "น.ต.อ.น.",
"NE": "ต.อ.น.",
"ENE": "ต.อ.ต.อ.น.",
"E": "ต.อ.",
"ESE": "ต.อ.ต.อ.ต.",
"SE": "ต.อ.ต.",
"SSE": "ต.ต.อ.ต.",
"S": "ต.",
"SSW": "ต.ต.ต.ต.",
"SW": "ต.ต.ต.",
"WSW": "ต.ต.ต.ต.ต.",
"W": "ต.ต.",
"WNW": "ต.ต.ต.ต.น.",
"NW": "ต.ต.น.",
"NNW": "น.ต.ต.น.",
"PRECIP_POP": "ความแม่นยำ",
"PRECIP_AMOUNT": "ปริมาณน้ำฝน",
"MODULE_CONFIG_CHANGED": "ตัวเลือกการกำหนดค่าสำหรับโมดูล {MODULE_NAME} มีการเปลี่ยนแปลง\nโปรดตรวจสอบเอกสารประกอบ",
"MODULE_CONFIG_ERROR": "เกิดข้อผิดพลาดในโมดูล {MODULE_NAME} {ERROR}",
"MODULE_ERROR_MALFORMED_URL": "URL ผิดรูปแบบ",
"MODULE_ERROR_NO_CONNECTION": "ไม่มีการเชื่อมต่ออินเทอร์เน็ต.",
"MODULE_ERROR_UNAUTHORIZED": "การอนุญาตล้มเหลว",
"MODULE_ERROR_UNSPECIFIED": "ตรวจสอบบันทึกสำหรับรายละเอียดเพิ่มเติม",
"NEWSFEED_NO_ITEMS": "ไม่มีข่าวในขณะนี้",
"UPDATE_NOTIFICATION": "MagicMirror² มีการอัปเดต",
"UPDATE_NOTIFICATION_MODULE": "มีการอัปเดตสำหรับโมดูล {MODULE_NAME}",
"UPDATE_INFO_SINGLE": "การติดตั้งปัจจุบันถูกคอมมิท {COMMIT_COUNT} รายการในสาขา {BRANCH_NAME}",
"UPDATE_INFO_MULTIPLE": "การติดตั้งปัจจุบันคือ {COMMIT_COUNT} เป็นคอมมิทที่อยู่เบื้องหลังในสาขา {BRANCH_NAME}"
}

42
vendor/package-lock.json generated vendored
View File

@ -7,18 +7,18 @@
"name": "magicmirror-vendors",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.3.0",
"@fortawesome/fontawesome-free": "^6.4.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.41",
"nunjucks": "^3.2.3",
"moment-timezone": "^0.5.43",
"nunjucks": "^3.2.4",
"suncalc": "^1.9.0",
"weathericons": "^2.1.0"
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.3.0.tgz",
"integrity": "sha512-qVtd5i1Cc7cdrqnTWqTObKQHjPWAiRwjUPaXObaeNPcy7+WKxJumGBx66rfSFgK6LNpIasVKkEgW8oyf0tmPLA==",
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz",
"integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
@ -51,9 +51,9 @@
}
},
"node_modules/moment-timezone": {
"version": "0.5.41",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.41.tgz",
"integrity": "sha512-e0jGNZDOHfBXJGz8vR/sIMXvBIGJJcqFjmlg9lmE+5KX1U7/RZNMswfD8nKnNCnQdKTIj50IaRKwl1fvMLyyRg==",
"version": "0.5.43",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz",
"integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==",
"dependencies": {
"moment": "^2.29.4"
},
@ -62,9 +62,9 @@
}
},
"node_modules/nunjucks": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.3.tgz",
"integrity": "sha512-psb6xjLj47+fE76JdZwskvwG4MYsQKXUtMsPh6U0YMvmyjRtKRFcxnlXGWglNybtNTNVmGdp94K62/+NjF5FDQ==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz",
"integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==",
"dependencies": {
"a-sync-waterfall": "^1.0.0",
"asap": "^2.0.3",
@ -98,9 +98,9 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.3.0.tgz",
"integrity": "sha512-qVtd5i1Cc7cdrqnTWqTObKQHjPWAiRwjUPaXObaeNPcy7+WKxJumGBx66rfSFgK6LNpIasVKkEgW8oyf0tmPLA=="
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz",
"integrity": "sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ=="
},
"a-sync-waterfall": {
"version": "1.0.1",
@ -123,17 +123,17 @@
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"moment-timezone": {
"version": "0.5.41",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.41.tgz",
"integrity": "sha512-e0jGNZDOHfBXJGz8vR/sIMXvBIGJJcqFjmlg9lmE+5KX1U7/RZNMswfD8nKnNCnQdKTIj50IaRKwl1fvMLyyRg==",
"version": "0.5.43",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz",
"integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==",
"requires": {
"moment": "^2.29.4"
}
},
"nunjucks": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.3.tgz",
"integrity": "sha512-psb6xjLj47+fE76JdZwskvwG4MYsQKXUtMsPh6U0YMvmyjRtKRFcxnlXGWglNybtNTNVmGdp94K62/+NjF5FDQ==",
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz",
"integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==",
"requires": {
"a-sync-waterfall": "^1.0.0",
"asap": "^2.0.3",

6
vendor/package.json vendored
View File

@ -10,10 +10,10 @@
"url": "https://github.com/MichMich/MagicMirror/issues"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.3.0",
"@fortawesome/fontawesome-free": "^6.4.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.41",
"nunjucks": "^3.2.3",
"moment-timezone": "^0.5.43",
"nunjucks": "^3.2.4",
"suncalc": "^1.9.0",
"weathericons": "^2.1.0"
}