Merge pull request #2510 from MichMich/develop

Release v2.15.0
This commit is contained in:
Michael Teeuw
2021-04-01 14:23:02 +02:00
committed by GitHub
161 changed files with 6461 additions and 6625 deletions

View File

@@ -25,13 +25,14 @@ To run StyleLint, use `npm run lint:style`.
Please only submit reproducible issues. Please only submit reproducible issues.
If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt) If you're not sure if it's a real bug or if it's just you, please open a topic on the forum: [https://forum.magicmirror.builders/category/15/bug-hunt](https://forum.magicmirror.builders/category/15/bug-hunt)
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting) Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
When submitting a new issue, please supply the following information: 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, Windows, Mac, Linux, System V UNIX). **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 0.12.13 or later. **Node Version**: Make sure it's version 10 or later.
**MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2). **MagicMirror Version**: Now that the versions have split, tell us if you are using the PHP version (v1) or the newer JavaScript version (v2).

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: MichMich
custom: ['https://magicmirror.builders/#donate']

View File

@@ -6,6 +6,8 @@ If you're not sure if it's a real bug or if it's just you, please open a topic o
Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting) Problems installing or configuring your MagicMirror? Check out: [https://forum.magicmirror.builders/category/10/troubleshooting](https://forum.magicmirror.builders/category/10/troubleshooting)
A common problem is that your config file could be invalid. Please run in your MagicMirror directory: `npm run config:check` and see if it reports an error.
## I found a bug in the MagicMirror installer ## I found a bug in the MagicMirror installer
If you are facing an issue or found a bug while trying to install MagicMirror via the installer please report it in the respective GitHub repository: If you are facing an issue or found a bug while trying to install MagicMirror via the installer please report it in the respective GitHub repository:
@@ -23,9 +25,9 @@ If you are facing an issue or found a bug while running MagicMirror inside a Doc
Please make sure to only submit reproducible issues. You can safely remove everything above the dividing line. Please make sure to only submit reproducible issues. You can safely remove everything above the dividing line.
When submitting a new issue, please supply the following information: 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, Windows, Mac, Linux, System V UNIX). **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 8 or later. **Node Version**: Make sure it's version 10 or later.
**MagicMirror Version**: Please let us now which version of MagicMirror you are running. It can be found in the `package.log` file. **MagicMirror Version**: Please let us now which version of MagicMirror you are running. It can be found in the `package.log` file.

View File

@@ -1,13 +1,28 @@
> Please send your pull requests the develop branch. Hello and thank you for wanting to contribute to the MagicMirror project
> Don't forget to add the change to CHANGELOG.md.
**Please make sure that you have followed these 4 rules before submitting your Pull Request:**
> 1) Base your pull requests against the `develop` branch.
>
>
> 2) Include these infos in the description:
> * Does the pull request solve a **related** issue?
> * If so, can you reference the issue like this `Fixes #<issue_number>`?
> * What does the pull request accomplish? Use a list if needed.
> * If it includes major visual changes please add screenshots.
>
>
> 3) Please run `npm run lint:prettier` before submitting so that
> style issues are fixed.
>
>
> 4) Don't forget to add an entry about your changes to
> the CHANGELOG.md file.
**Note**: Sometimes the development moves very fast. It is highly **Note**: Sometimes the development moves very fast. It is highly
recommended that you update your branch of `develop` before creating a recommended that you update your branch of `develop` before creating a
pull request to send us your changes. This makes everyone's lives pull request to send us your changes. This makes everyone's lives
easier (including yours) and helps us out on the development team. easier (including yours) and helps us out on the development team.
Thanks!
- Does the pull request solve a **related** issue? Thanks again and have a nice day!
- If so, can you reference the issue?
- What does the pull request accomplish? Use a list if needed.
- If it includes major visual changes please add screenshots.

View File

@@ -0,0 +1,24 @@
# This workflow runs the automated test and uploads the coverage results to codecov.io
name: "Run Codecov Tests"
on:
push:
branches: [ master, develop ]
pull_request:
branches: [ master, develop ]
jobs:
run-and-upload-coverage-report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: |
Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99
npm ci
npm run test:coverage
- uses: codecov/codecov-action@v1
with:
file: ./coverage/lcov.info
fail_ci_if_error: true

View File

@@ -1,10 +1,12 @@
# This workflow enforces the update of a changelog file on every pull request
name: "Enforce Changelog" name: "Enforce Changelog"
on: on:
pull_request: pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
jobs: jobs:
# Enforces the update of a changelog file on every pull request
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

@@ -1,7 +1,7 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Automated Tests name: "Run Automated Tests"
on: on:
push: push:
@@ -11,13 +11,10 @@ on:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node-version: [10.x, 12.x, 14.x] node-version: [10.x, 12.x, 14.x]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}

View File

@@ -5,6 +5,70 @@ 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² ❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror²
## [2.15.0] - 2021-04-01
Special thanks to the following contributors: @EdgardosReis, @MystaraTheGreat, @TheDuffman85, @ashishtank, @buxxi, @codac, @fewieden, @khassel, @klaernie, @qu1que, @rejas, @sdetweil & @thomasrockhu.
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
### Added
- Added Galician language.
- Added GitHub workflows for automated testing and changelog enforcement.
- Added CodeCov badge to Readme.
- Added CURRENTWEATHER_TYPE notification to currentweather and weather module, use it in compliments module.
- Added `start:dev` command to the npm scripts for starting electron with devTools open.
- Added logging when using deprecated modules weatherforecast or currentweather.
- Added Portuguese translations for "MODULE_CONFIG_CHANGED" and "PRECIP".
- Respect parameter ColoredSymbolOnly also for custom events.
- Added a new parameter to hide time portion on relative times.
- `module.show` has now the option for a callback on error.
- Added locale to sample config file.
- Added support for self-signed certificates for the default calendar module (#466).
- Added hiddenOnStartup flag to module config (#2475).
### Updated
- Updated markdown files for github.
- Cleaned up old code on server side.
- Convert `-0` to `0` when displaying temperature.
- 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.
- Bump electron to v11 and enable contextIsolation.
- Don't update the DOM when a module is not displayed.
- Cleaned up jsdoc and tests.
- Exposed logger as node module for easier access for 3rd party modules.
- Replaced deprecated `request` package with `node-fetch` and `digest-fetch`.
- Refactored calendar fetcher.
- Cleaned up newsfeed module.
- Cleaned up translations and translator code.
### Removed
- Removed danger.js library.
- Removed `ical` which was substituted by `node-ical` in release `v2.13.0`. Module developers must install this dependency themselves in the module folder if needed.
- Removed valid-url library.
### Fixed
- Added default log levels to stop calendar log spamming.
- Fix socket.io cors errors, see [breaking change since socket.io v3](https://socket.io/docs/v3/handling-cors/).
- Fix Issue with weather forecast icons due to fixed day start and end time (#2221).
- Fix empty directory for each module's main javascript file in the inspector.
- Fix Issue with weather forecast icons unit tests with different timezones (#2221).
- Fix issue with unencoded characters in translated strings when using nunjuck template (`Loading &hellip;` as an example).
- Fix socket.io backward compatibility with socket v2 clients.
- Fix 3rd party module language loading if language is English.
- Fix e2e tests after spectron update.
- Fix updatenotification creating zombie processes by setting a timeout for the git process.
- Fix weather module openweathermap not loading if lat and lon set without onecall.
- Fix calendar daylight savings offset calculation if recurring start date before 2007.
- Fix calendar time/date adjustment when time with GMT offset is different day (#2488).
- Fix calendar daylight savings offset calculation if recurring FULL DAY start date before 2007 (#2483).
- Fix newsreaders template, for wrong test for nowrap in 2 places (should be if not).
## [2.14.0] - 2021-01-01 ## [2.14.0] - 2021-01-01
Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank, @bluemanos, @flopp999, @jakemulley, @jakobsarwary1, @marvai-vgtu, @mirontoli, @rejas, @sdetweil, @Snille & @Sub028. Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank, @bluemanos, @flopp999, @jakemulley, @jakobsarwary1, @marvai-vgtu, @mirontoli, @rejas, @sdetweil, @Snille & @Sub028.
@@ -15,17 +79,15 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
- Added new log level "debug" to the logger. - Added new log level "debug" to the logger.
- Added new parameter "useKmh" to weather module for displaying wind speed as kmh. - Added new parameter "useKmh" to weather module for displaying wind speed as kmh.
- Chuvash translation. - Added Chuvash translation.
- Added Weatherbit as a provider to Weather module. - Added Weatherbit as a provider to Weather module.
- Added SMHI as a provider to Weather module. - Added SMHI as a provider to Weather module.
- Added Hindi & Gujarati translation. - Added Hindi & Gujarati translation.
- Added optional support for DEGREE position in Feels like translation. - Added optional support for DEGREE position in Feels like translation.
- Added support for variables in nunjucks templates for translate filter. - Added support for variables in nunjucks templates for translate filter.
- Added Chuvash translation. - Added Chuvash translation.
- Calendar: new options "limitDays" and "coloredEvents".
- Added new option "limitDays" - limit the number of discreet days displayed. - Added new option "limitDays" - limit the number of discreet days displayed.
- Added new option "customEvents" - use custom symbol/color based on keyword in event title. - Added new option "customEvents" - use custom symbol/color based on keyword in event title.
- Added GitHub workflows for automated testing and changelog enforcement.
### Updated ### Updated
@@ -44,7 +106,7 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
### Deleted ### Deleted
- Removed Travis CI intergration. - Removed Travis CI integration.
### Fixed ### Fixed
@@ -61,8 +123,8 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
- Fix non-fullday recurring rule processing. (#2216) - Fix non-fullday recurring rule processing. (#2216)
- Catch errors when parsing calendar data with ical. (#2022) - Catch errors when parsing calendar data with ical. (#2022)
- Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228) - Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228)
- Weather module - Always displays night icons when local is other then English. (#2221) - Weather module - Always displays night icons when local is other than English. (#2221)
- Update Node-ical 0.12.4 , fix invalid RRULE format in cal entries - Update node-ical 0.12.4, fix invalid RRULE format in cal entries
- Fix package.json for optional electron dependency (2378) - Fix package.json for optional electron dependency (2378)
- Update node-ical version again, 0.12.5, change RRULE fix (#2371, #2379) - Update node-ical version again, 0.12.5, change RRULE fix (#2371, #2379)
- Remove undefined objects from modules array (#2382) - Remove undefined objects from modules array (#2382)
@@ -77,11 +139,11 @@ Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura
### Added ### Added
- `--dry-run` option adde in fetch call within updatenotification node_helper. This is to prevent - `--dry-run` Added option in fetch call within updatenotification node_helper. This is to prevent
MagicMirror from consuming any fetch result. Causes conflict with MMPM when attempting to check MagicMirror from consuming any fetch result. Causes conflict with MMPM when attempting to check
for updates to MagicMirror and/or MagicMirror modules. for updates to MagicMirror and/or MagicMirror modules.
- Test coverage with Istanbul, run it with `npm run test:coverage`. - Test coverage with Istanbul, run it with `npm run test:coverage`.
- Add lithuanian language. - Added lithuanian language.
- Added support in weatherforecast for OpenWeather onecall API. - Added support in weatherforecast for OpenWeather onecall API.
- Added config option to calendar-icons for recurring- and fullday-events. - Added config option to calendar-icons for recurring- and fullday-events.
- Added current, hourly (max 48), and daily (max 7) weather forecasts to weather module via OpenWeatherMap One Call API. - Added current, hourly (max 48), and daily (max 7) weather forecasts to weather module via OpenWeatherMap One Call API.
@@ -145,7 +207,7 @@ Special thanks to the following contributors: @AndreKoepke, @andrezibaia, @bryan
- Fix the use of "maxNumberOfDays" in the module "weatherforecast". [#2018](https://github.com/MichMich/MagicMirror/issues/2018) - Fix the use of "maxNumberOfDays" in the module "weatherforecast". [#2018](https://github.com/MichMich/MagicMirror/issues/2018)
- Throw error when check_config fails. [#1928](https://github.com/MichMich/MagicMirror/issues/1928) - Throw error when check_config fails. [#1928](https://github.com/MichMich/MagicMirror/issues/1928)
- Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MichMich/MagicMirror/issues/2050) - Bug fix related to 'maxEntries' not displaying Calendar events. [#2050](https://github.com/MichMich/MagicMirror/issues/2050)
- Updated ical library to latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926) - Updated ical library to the latest version. [#1926](https://github.com/MichMich/MagicMirror/issues/1926)
- Fix config check after merge of prettier [#2109](https://github.com/MichMich/MagicMirror/issues/2109) - Fix config check after merge of prettier [#2109](https://github.com/MichMich/MagicMirror/issues/2109)
## [2.11.0] - 2020-04-01 ## [2.11.0] - 2020-04-01
@@ -439,7 +501,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
### Fixed ### Fixed
- Fixed gzip encoded calendar loading issue #1400. - Fixed gzip encoded calendar loading issue #1400.
- Mixup between german and spanish translation for newsfeed. - Fixed mixup between german and spanish translation for newsfeed.
- Fixed close dates to be absolute, if no configured in the config.js - module Calendar - Fixed close dates to be absolute, if no configured in the config.js - module Calendar
- Fixed the updatenotification module message about new commits in the repository, so they can be correctly localized in singular and plural form. - Fixed the updatenotification module message about new commits in the repository, so they can be correctly localized in singular and plural form.
- Fix for weatherforecast rainfall rounding [#1374](https://github.com/MichMich/MagicMirror/issues/1374) - Fix for weatherforecast rainfall rounding [#1374](https://github.com/MichMich/MagicMirror/issues/1374)

View File

@@ -1,6 +1,6 @@
# The MIT License (MIT) # The MIT License (MIT)
Copyright © 2016-2020 Michael Teeuw Copyright © 2016-2021 Michael Teeuw
Permission is hereby granted, free of charge, to any person Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation obtaining a copy of this software and associated documentation

View File

@@ -1,11 +1,13 @@
![MagicMirror²: The open source modular smart mirror platform. ](.github/header.png) ![MagicMirror²: The open source modular smart mirror platform. ](.github/header.png)
<p align="center"> <p style="text-align: center">
<a href="https://david-dm.org/MichMich/MagicMirror"><img src="https://david-dm.org/MichMich/MagicMirror.svg" alt="Dependency Status"></a> <a href="https://david-dm.org/MichMich/MagicMirror"><img src="https://david-dm.org/MichMich/MagicMirror.svg" alt="Dependency Status"></a>
<a href="https://david-dm.org/MichMich/MagicMirror#info=devDependencies"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a> <a href="https://david-dm.org/MichMich/MagicMirror?type=dev"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge"></a> <a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge" alt="CLI Best Practices"></a>
<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg?token=LEG1KitZR6" alt="CodeCov Status"/></a>
<a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a> <a href="https://choosealicense.com/licenses/mit"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
<a href="https://github.com/MichMich/MagicMirror/actions?query=workflow%3A%22Automated+Tests%22"><img src="https://github.com/MichMich/MagicMirror/workflows/Automated%20Tests/badge.svg" alt="Tests"></a> <a href="https://github.com/MichMich/MagicMirror/actions?query=workflow%3A%22Automated+Tests%22"><img src="https://github.com/MichMich/MagicMirror/workflows/Automated%20Tests/badge.svg" alt="Tests"></a>
<a href="https://codecov.io/gh/MichMich/MagicMirror"><img src="https://codecov.io/gh/MichMich/MagicMirror/branch/master/graph/badge.svg" /></a>
</p> </p>
**MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors). **MagicMirror²** is an open source modular smart mirror platform. With a growing list of installable modules, the **MagicMirror²** allows you to convert your hallway or bathroom mirror into your personal assistant. **MagicMirror²** is built by the creator of [the original MagicMirror](https://michaelteeuw.nl/tagged/magicmirror) with the incredible help of a [growing community of contributors](https://github.com/MichMich/MagicMirror/graphs/contributors).
@@ -27,7 +29,13 @@ For the full documentation including **[installation instructions](https://docs.
## Contributing Guidelines ## Contributing Guidelines
Contributions of all kinds are welcome, not only in the form of code but also with regards bug reports and documentation. For the full contribution guidelines, check out: [https://docs.magicmirror.builders/getting-started/contributing.html](https://docs.magicmirror.builders/getting-started/contributing.html) Contributions of all kinds are welcome, not only in the form of code but also with regards to
- bug reports
- documentation
- translations
For the full contribution guidelines, check out: [https://docs.magicmirror.builders/getting-started/contributing.html](https://docs.magicmirror.builders/getting-started/contributing.html)
## Enjoying MagicMirror? Consider a donation! ## Enjoying MagicMirror? Consider a donation!
@@ -38,7 +46,6 @@ If we receive enough donations we might even be able to free up some working hou
To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link. To donate, please follow [this](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G5D8E9MR5DTD2&source=url) link.
<p align="center"> <p style="text-align: center">
<br>
<a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a> <a href="https://forum.magicmirror.builders/topic/728/magicmirror-is-voted-number-1-in-the-magpi-top-50"><img src="https://magicmirror.builders/img/magpi-best-watermark-custom.png" width="150" alt="MagPi Top 50"></a>
</p> </p>

View File

@@ -14,7 +14,6 @@
* *
* @param {string} key key to look for at the command line * @param {string} key key to look for at the command line
* @param {string} defaultValue value if no key is given at the command line * @param {string} defaultValue value if no key is given at the command line
*
* @returns {string} the value of the parameter * @returns {string} the value of the parameter
*/ */
function getCommandLineParameter(key, defaultValue = undefined) { function getCommandLineParameter(key, defaultValue = undefined) {
@@ -36,7 +35,6 @@
* Gets the config from the specified server url * Gets the config from the specified server url
* *
* @param {string} url location where the server is running. * @param {string} url location where the server is running.
*
* @returns {Promise} the config * @returns {Promise} the config
*/ */
function getServerConfig(url) { function getServerConfig(url) {
@@ -66,7 +64,7 @@
/** /**
* Print a message to the console in case of errors * Print a message to the console in case of errors
* *
* @param {string} [message] error message to print * @param {string} message error message to print
* @param {number} code error code for the exit call * @param {number} code error code for the exit call
*/ */
function fail(message, code = 1) { function fail(message, code = 1) {

View File

@@ -28,6 +28,7 @@ var config = {
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
language: "en", language: "en",
locale: "en-US",
logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
timeFormat: 24, timeFormat: 24,
units: "metric", units: "metric",
@@ -66,22 +67,26 @@ var config = {
position: "lower_third" position: "lower_third"
}, },
{ {
module: "currentweather", module: "weather",
position: "top_right", position: "top_right",
config: { config: {
weatherProvider: "openweathermap",
type: "current",
location: "New York", location: "New York",
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
appid: "YOUR_OPENWEATHER_API_KEY" apiKey: "YOUR_OPENWEATHER_API_KEY"
} }
}, },
{ {
module: "weatherforecast", module: "weather",
position: "top_right", position: "top_right",
header: "Weather Forecast", header: "Weather Forecast",
config: { config: {
weatherProvider: "openweathermap",
type: "forecast",
location: "New York", location: "New York",
locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city locationID: "5128581", //ID from http://bulk.openweathermap.org/sample/city.list.json.gz; unzip the gz file and find your city
appid: "YOUR_OPENWEATHER_API_KEY" apiKey: "YOUR_OPENWEATHER_API_KEY"
} }
}, },
{ {

View File

@@ -1,17 +0,0 @@
import { danger, fail, warn } from "danger";
// Check if the CHANGELOG.md file has been edited
// Fail the build and post a comment reminding submitters to do so if it wasn't changed
if (!danger.git.modified_files.includes("CHANGELOG.md")) {
warn("Please include an updated `CHANGELOG.md` file.<br>This way we can keep track of all the contributions.");
}
// Check if the PR request is send to the master branch.
// This should only be done by MichMich.
if (danger.github.pr.base.ref === "master" && danger.github.pr.user.login !== "MichMich") {
// Check if the PR body or title includes the text: #accepted.
// If not, the PR will fail.
if ((danger.github.pr.body + danger.github.pr.title).includes("#accepted")) {
fail("Please send all your pull requests to the `develop` branch.<br>Pull requests on the `master` branch will not be accepted.");
}
}

130
js/app.js
View File

@@ -4,22 +4,23 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
var fs = require("fs");
var path = require("path");
var Log = require(__dirname + "/logger.js");
var Server = require(__dirname + "/server.js");
var Utils = require(__dirname + "/utils.js");
var defaultModules = require(__dirname + "/../modules/default/defaultmodules.js");
// Alias modules mentioned in package.js under _moduleAliases. // Alias modules mentioned in package.js under _moduleAliases.
require("module-alias/register"); require("module-alias/register");
const fs = require("fs");
const path = require("path");
const Log = require("logger");
const Server = require(`${__dirname}/server`);
const Utils = require(`${__dirname}/utils`);
const defaultModules = require(`${__dirname}/../modules/default/defaultmodules`);
// Get version number. // Get version number.
global.version = JSON.parse(fs.readFileSync("package.json", "utf8")).version; global.version = require(`${__dirname}/../package.json`).version;
Log.log("Starting MagicMirror: v" + global.version); Log.log("Starting MagicMirror: v" + global.version);
// global absolute root path // global absolute root path
global.root_path = path.resolve(__dirname + "/../"); global.root_path = path.resolve(`${__dirname}/../`);
if (process.env.MM_CONFIG_FILE) { if (process.env.MM_CONFIG_FILE) {
global.configuration_file = process.env.MM_CONFIG_FILE; global.configuration_file = process.env.MM_CONFIG_FILE;
@@ -45,8 +46,8 @@ process.on("uncaughtException", function (err) {
* *
* @class * @class
*/ */
var App = function () { function App() {
var nodeHelpers = []; let nodeHelpers = [];
/** /**
* Loads the config file. Combines it with the defaults, and runs the * Loads the config file. Combines it with the defaults, and runs the
@@ -54,34 +55,31 @@ var App = function () {
* *
* @param {Function} callback Function to be called after loading the config * @param {Function} callback Function to be called after loading the config
*/ */
var loadConfig = function (callback) { function loadConfig(callback) {
Log.log("Loading config ..."); Log.log("Loading config ...");
var defaults = require(__dirname + "/defaults.js"); const defaults = require(`${__dirname}/defaults`);
// For this check proposed to TestSuite // For this check proposed to TestSuite
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8 // https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
var configFilename = path.resolve(global.root_path + "/config/config.js"); const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`);
if (typeof global.configuration_file !== "undefined") {
configFilename = path.resolve(global.configuration_file);
}
try { try {
fs.accessSync(configFilename, fs.F_OK); fs.accessSync(configFilename, fs.F_OK);
var c = require(configFilename); const c = require(configFilename);
checkDeprecatedOptions(c); checkDeprecatedOptions(c);
var config = Object.assign(defaults, c); const config = Object.assign(defaults, c);
callback(config); callback(config);
} catch (e) { } catch (e) {
if (e.code === "ENOENT") { if (e.code === "ENOENT") {
Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration.")); Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
} else if (e instanceof ReferenceError || e instanceof SyntaxError) { } else if (e instanceof ReferenceError || e instanceof SyntaxError) {
Log.error(Utils.colors.error("WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: " + e.stack)); Log.error(Utils.colors.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`));
} else { } else {
Log.error(Utils.colors.error("WARNING! Could not load config file. Starting with default configuration. Error found: " + e)); Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`));
} }
callback(defaults); callback(defaults);
} }
}; }
/** /**
* Checks the config for deprecated options and throws a warning in the logs * Checks the config for deprecated options and throws a warning in the logs
@@ -89,21 +87,15 @@ var App = function () {
* *
* @param {object} userConfig The user config * @param {object} userConfig The user config
*/ */
var checkDeprecatedOptions = function (userConfig) { function checkDeprecatedOptions(userConfig) {
var deprecated = require(global.root_path + "/js/deprecated.js"); const deprecated = require(`${global.root_path}/js/deprecated`);
var deprecatedOptions = deprecated.configs; const deprecatedOptions = deprecated.configs;
var usedDeprecated = []; const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option));
deprecatedOptions.forEach(function (option) {
if (userConfig.hasOwnProperty(option)) {
usedDeprecated.push(option);
}
});
if (usedDeprecated.length > 0) { if (usedDeprecated.length > 0) {
Log.warn(Utils.colors.warn("WARNING! Your config is using deprecated options: " + usedDeprecated.join(", ") + ". Check README and CHANGELOG for more up-to-date ways of getting the same functionality.")); Log.warn(Utils.colors.warn(`WARNING! Your config is using deprecated options: ${usedDeprecated.join(", ")}. Check README and CHANGELOG for more up-to-date ways of getting the same functionality.`));
}
} }
};
/** /**
* Loads a specific module. * Loads a specific module.
@@ -111,35 +103,35 @@ var App = function () {
* @param {string} module The name of the module (including subpath). * @param {string} module The name of the module (including subpath).
* @param {Function} callback Function to be called after loading * @param {Function} callback Function to be called after loading
*/ */
var loadModule = function (module, callback) { function loadModule(module, callback) {
var elements = module.split("/"); const elements = module.split("/");
var moduleName = elements[elements.length - 1]; const moduleName = elements[elements.length - 1];
var moduleFolder = __dirname + "/../modules/" + module; let moduleFolder = `${__dirname}/../modules/${module}`;
if (defaultModules.indexOf(moduleName) !== -1) { if (defaultModules.includes(moduleName)) {
moduleFolder = __dirname + "/../modules/default/" + module; moduleFolder = `${__dirname}/../modules/default/${module}`;
} }
var helperPath = moduleFolder + "/node_helper.js"; const helperPath = `${moduleFolder}/node_helper.js`;
var loadModule = true; let loadHelper = true;
try { try {
fs.accessSync(helperPath, fs.R_OK); fs.accessSync(helperPath, fs.R_OK);
} catch (e) { } catch (e) {
loadModule = false; loadHelper = false;
Log.log("No helper found for module: " + moduleName + "."); Log.log(`No helper found for module: ${moduleName}.`);
} }
if (loadModule) { if (loadHelper) {
var Module = require(helperPath); const Module = require(helperPath);
var m = new Module(); let m = new Module();
if (m.requiresVersion) { if (m.requiresVersion) {
Log.log("Check MagicMirror version for node helper '" + moduleName + "' - Minimum version: " + m.requiresVersion + " - Current version: " + global.version); Log.log(`Check MagicMirror version for node helper '${moduleName}' - Minimum version: ${m.requiresVersion} - Current version: ${global.version}`);
if (cmpVersions(global.version, m.requiresVersion) >= 0) { if (cmpVersions(global.version, m.requiresVersion) >= 0) {
Log.log("Version is ok!"); Log.log("Version is ok!");
} else { } else {
Log.log("Version is incorrect. Skip module: '" + moduleName + "'"); Log.warn(`Version is incorrect. Skip module: '${moduleName}'`);
return; return;
} }
} }
@@ -152,7 +144,7 @@ var App = function () {
} else { } else {
callback(); callback();
} }
}; }
/** /**
* Loads all modules. * Loads all modules.
@@ -160,12 +152,15 @@ var App = function () {
* @param {Module[]} modules All modules to be loaded * @param {Module[]} modules All modules to be loaded
* @param {Function} callback Function to be called after loading * @param {Function} callback Function to be called after loading
*/ */
var loadModules = function (modules, callback) { function loadModules(modules, callback) {
Log.log("Loading module helpers ..."); Log.log("Loading module helpers ...");
var loadNextModule = function () { /**
*
*/
function loadNextModule() {
if (modules.length > 0) { if (modules.length > 0) {
var nextModule = modules[0]; const nextModule = modules[0];
loadModule(nextModule, function () { loadModule(nextModule, function () {
modules = modules.slice(1); modules = modules.slice(1);
loadNextModule(); loadNextModule();
@@ -175,10 +170,10 @@ var App = function () {
Log.log("All module helpers loaded."); Log.log("All module helpers loaded.");
callback(); callback();
} }
}; }
loadNextModule(); loadNextModule();
}; }
/** /**
* Compare two semantic version numbers and return the difference. * Compare two semantic version numbers and return the difference.
@@ -190,11 +185,11 @@ var App = function () {
* number if a is smaller and 0 if they are the same * number if a is smaller and 0 if they are the same
*/ */
function cmpVersions(a, b) { function cmpVersions(a, b) {
var i, diff; let i, diff;
var regExStrip0 = /(\.0+)+$/; const regExStrip0 = /(\.0+)+$/;
var segmentsA = a.replace(regExStrip0, "").split("."); const segmentsA = a.replace(regExStrip0, "").split(".");
var segmentsB = b.replace(regExStrip0, "").split("."); const segmentsB = b.replace(regExStrip0, "").split(".");
var l = Math.min(segmentsA.length, segmentsB.length); const l = Math.min(segmentsA.length, segmentsB.length);
for (i = 0; i < l; i++) { for (i = 0; i < l; i++) {
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10); diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
@@ -219,21 +214,19 @@ var App = function () {
Log.setLogLevel(config.logLevel); Log.setLogLevel(config.logLevel);
var modules = []; let modules = [];
for (var m in config.modules) { for (const module of config.modules) {
var module = config.modules[m]; if (!modules.includes(module.module) && !module.disabled) {
if (modules.indexOf(module.module) === -1 && !module.disabled) {
modules.push(module.module); modules.push(module.module);
} }
} }
loadModules(modules, function () { loadModules(modules, function () {
var server = new Server(config, function (app, io) { const server = new Server(config, function (app, io) {
Log.log("Server started ..."); Log.log("Server started ...");
for (var h in nodeHelpers) { for (let nodeHelper of nodeHelpers) {
var nodeHelper = nodeHelpers[h];
nodeHelper.setExpressApp(app); nodeHelper.setExpressApp(app);
nodeHelper.setSocketIO(io); nodeHelper.setSocketIO(io);
nodeHelper.start(); nodeHelper.start();
@@ -256,8 +249,7 @@ var App = function () {
* Added to fix #1056 * Added to fix #1056
*/ */
this.stop = function () { this.stop = function () {
for (var h in nodeHelpers) { for (const nodeHelper of nodeHelpers) {
var nodeHelper = nodeHelpers[h];
if (typeof nodeHelper.stop === "function") { if (typeof nodeHelper.stop === "function") {
nodeHelper.stop(); nodeHelper.stop();
} }
@@ -292,6 +284,6 @@ var App = function () {
this.stop(); this.stop();
process.exit(0); process.exit(0);
}); });
}; }
module.exports = new App(); module.exports = new App();

View File

@@ -11,9 +11,9 @@ const linter = new Linter();
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const rootPath = path.resolve(__dirname + "/../"); const rootPath = path.resolve(`${__dirname}/../`);
const Log = require(rootPath + "/js/logger.js"); const Log = require(`${rootPath}/js/logger.js`);
const Utils = require(rootPath + "/js/utils.js"); const Utils = require(`${rootPath}/js/utils.js`);
/** /**
* Returns a string with path of configuration file. * Returns a string with path of configuration file.
@@ -23,11 +23,7 @@ const Utils = require(rootPath + "/js/utils.js");
*/ */
function getConfigFile() { function getConfigFile() {
// FIXME: This function should be in core. Do you want refactor me ;) ?, be good! // FIXME: This function should be in core. Do you want refactor me ;) ?, be good!
let configFileName = path.resolve(rootPath + "/config/config.js"); return path.resolve(process.env.MM_CONFIG_FILE || `${rootPath}/config/config.js`);
if (process.env.MM_CONFIG_FILE) {
configFileName = path.resolve(process.env.MM_CONFIG_FILE);
}
return configFileName;
} }
/** /**
@@ -54,21 +50,18 @@ function checkConfigFile() {
Log.info(Utils.colors.info("Checking file... "), configFileName); Log.info(Utils.colors.info("Checking file... "), configFileName);
// I'm not sure if all ever is utf-8 // I'm not sure if all ever is utf-8
fs.readFile(configFileName, "utf-8", function (err, data) { const configFile = fs.readFileSync(configFileName, "utf-8");
if (err) {
throw err; const errors = linter.verify(configFile);
} if (errors.length === 0) {
const messages = linter.verify(data);
if (messages.length === 0) {
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)")); Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
} else { } else {
Log.error(Utils.colors.error("Your configuration file contains syntax errors :(")); Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
// In case the there errors show messages and return
messages.forEach((error) => { for (const error of errors) {
Log.error("Line", error.line, "col", error.column, error.message); Log.error(`Line ${error.line} column ${error.column}: ${error.message}`);
}); }
} }
});
} }
checkConfigFile(); checkConfigFile();

View File

@@ -20,6 +20,7 @@ var defaults = {
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en", language: "en",
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
timeFormat: 24, timeFormat: 24,
units: "metric", units: "metric",
zoom: 1, zoom: 1,
@@ -42,7 +43,7 @@ var defaults = {
module: "helloworld", module: "helloworld",
position: "middle_center", position: "middle_center",
config: { config: {
text: "Please create a config file." text: "Please create a config file or check the existing one for errors."
} }
}, },
{ {
@@ -58,7 +59,7 @@ var defaults = {
position: "middle_center", position: "middle_center",
classes: "xsmall", classes: "xsmall",
config: { config: {
text: "If you get this message while your config file is already<br>created, your config file probably contains an error.<br>Use a JavaScript linter to validate your file." text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror directory<br>" + "<pre>npm run config:check</pre>"
} }
}, },
{ {

View File

@@ -6,11 +6,6 @@
* Olex S. original idea this deprecated option * Olex S. original idea this deprecated option
*/ */
var deprecated = { module.exports = {
configs: ["kioskmode"] configs: ["kioskmode"]
}; };
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = deprecated;
}

View File

@@ -2,10 +2,10 @@
const electron = require("electron"); const electron = require("electron");
const core = require("./app.js"); const core = require("./app.js");
const Log = require("./logger.js"); const Log = require("logger");
// Config // Config
var config = process.env.config ? JSON.parse(process.env.config) : {}; let config = process.env.config ? JSON.parse(process.env.config) : {};
// Module to control application life. // Module to control application life.
const app = electron.app; const app = electron.app;
// Module to create native browser window. // Module to create native browser window.
@@ -20,13 +20,14 @@ let mainWindow;
*/ */
function createWindow() { function createWindow() {
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
var electronOptionsDefaults = { let electronOptionsDefaults = {
width: 800, width: 800,
height: 600, height: 600,
x: 0, x: 0,
y: 0, y: 0,
darkTheme: true, darkTheme: true,
webPreferences: { webPreferences: {
contextIsolation: true,
nodeIntegration: false, nodeIntegration: false,
zoomFactor: config.zoom zoomFactor: config.zoom
}, },
@@ -42,7 +43,7 @@ function createWindow() {
electronOptionsDefaults.autoHideMenuBar = true; electronOptionsDefaults.autoHideMenuBar = true;
} }
var electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions); const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
// Create the browser window. // Create the browser window.
mainWindow = new BrowserWindow(electronOptions); mainWindow = new BrowserWindow(electronOptions);
@@ -50,14 +51,14 @@ function createWindow() {
// and load the index.html of the app. // and load the index.html of the app.
// If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost // If config.address is not defined or is an empty string (listening on all interfaces), connect to localhost
var prefix; let prefix;
if (config["tls"] !== null && config["tls"]) { if (config["tls"] !== null && config["tls"]) {
prefix = "https://"; prefix = "https://";
} else { } else {
prefix = "http://"; prefix = "http://";
} }
var address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address; let address = (config.address === void 0) | (config.address === "") ? (config.address = "localhost") : config.address;
mainWindow.loadURL(`${prefix}${address}:${config.port}`); mainWindow.loadURL(`${prefix}${address}:${config.port}`);
// Open the DevTools if run with "npm start dev" // Open the DevTools if run with "npm start dev"
@@ -125,7 +126,7 @@ app.on("before-quit", (event) => {
// Start the core application if server is run on localhost // Start the core application if server is run on localhost
// This starts all node helpers and starts the webserver. // This starts all node helpers and starts the webserver.
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) > -1) { if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {
core.start(function (c) { core.start(function (c) {
config = c; config = c;
}); });

View File

@@ -54,6 +54,14 @@ var Loader = (function () {
// Notify core of loaded modules. // Notify core of loaded modules.
MM.modulesStarted(moduleObjects); MM.modulesStarted(moduleObjects);
// Starting modules also hides any modules that have requested to be initially hidden
for (let thisModule of moduleObjects) {
if (thisModule.data.hiddenOnStartup) {
Log.info("Initially hiding " + thisModule.name);
thisModule.hide();
}
}
}; };
/** /**
@@ -97,6 +105,7 @@ var Loader = (function () {
path: moduleFolder + "/", path: moduleFolder + "/",
file: moduleName + ".js", file: moduleName + ".js",
position: moduleData.position, position: moduleData.position,
hiddenOnStartup: moduleData.hiddenOnStartup,
header: moduleData.header, header: moduleData.header,
configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false, configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false,
config: moduleData.config, config: moduleData.config,
@@ -114,7 +123,7 @@ var Loader = (function () {
* @param {Function} callback Function called when done. * @param {Function} callback Function called when done.
*/ */
var loadModule = function (module, callback) { var loadModule = function (module, callback) {
var url = module.path + "/" + module.file; var url = module.path + module.file;
var afterLoad = function () { var afterLoad = function () {
var moduleObject = Module.create(module.name); var moduleObject = Module.create(module.name);

View File

@@ -295,6 +295,9 @@ var MM = (function () {
// Otherwise cancel show action. // Otherwise cancel show action.
if (module.lockStrings.length !== 0 && options.force !== true) { if (module.lockStrings.length !== 0 && options.force !== true) {
Log.log("Will not show " + module.name + ". LockStrings active: " + module.lockStrings.join(",")); Log.log("Will not show " + module.name + ". LockStrings active: " + module.lockStrings.join(","));
if (typeof options.onError === "function") {
options.onError(new Error("LOCK_STRING_ACTIVE"));
}
return; return;
} }
@@ -440,7 +443,6 @@ var MM = (function () {
* Removes a module instance from the collection. * Removes a module instance from the collection.
* *
* @param {object} module The module instance to remove from the collection. * @param {object} module The module instance to remove from the collection.
*
* @returns {Module[]} Filtered collection of modules. * @returns {Module[]} Filtered collection of modules.
*/ */
var exceptModule = function (module) { var exceptModule = function (module) {
@@ -547,6 +549,11 @@ var MM = (function () {
return; return;
} }
if (!module.data.position) {
Log.warn("module tries to update the DOM without being displayed.");
return;
}
// Further implementation is done in the private method. // Further implementation is done in the private method.
updateDom(module, speed); updateDom(module, speed);
}, },

View File

@@ -176,7 +176,7 @@ var Module = Class.extend({
}); });
this._nunjucksEnvironment.addFilter("translate", function (str, variables) { this._nunjucksEnvironment.addFilter("translate", function (str, variables) {
return self.translate(str, variables); return nunjucks.runtime.markSafe(self.translate(str, variables));
}); });
return this._nunjucksEnvironment; return this._nunjucksEnvironment;
@@ -311,33 +311,33 @@ var Module = Class.extend({
* *
* @param {Function} callback Function called when done. * @param {Function} callback Function called when done.
*/ */
loadTranslations: function (callback) { loadTranslations(callback) {
var self = this; const translations = this.getTranslations() || {};
var translations = this.getTranslations(); const language = config.language.toLowerCase();
var lang = config.language.toLowerCase();
// The variable `first` will contain the first const languages = Object.keys(translations);
// defined translation after the following line. const fallbackLanguage = languages[0];
for (var first in translations) {
break; if (languages.length === 0) {
callback();
return;
} }
if (translations) { const translationFile = translations[language];
var translationFile = translations[lang] || undefined; const translationsFallbackFile = translations[fallbackLanguage];
var translationsFallbackFile = translations[first];
// If a translation file is set, load it and then also load the fallback translation file. if (!translationFile) {
// Otherwise only load the fallback translation file. Translator.load(this, translationsFallbackFile, true, callback);
if (translationFile !== undefined && translationFile !== translationsFallbackFile) { return;
Translator.load(self, translationFile, false, function () {
Translator.load(self, translationsFallbackFile, true, callback);
});
} else {
Translator.load(self, translationsFallbackFile, true, callback);
} }
Translator.load(this, translationFile, false, () => {
if (translationFile !== translationsFallbackFile) {
Translator.load(this, translationsFallbackFile, true, callback);
} else { } else {
callback(); callback();
} }
});
}, },
/** /**
@@ -428,12 +428,11 @@ var Module = Class.extend({
callback = callback || function () {}; callback = callback || function () {};
options = options || {}; options = options || {};
var self = this;
MM.showModule( MM.showModule(
this, this,
speed, speed,
function () { () => {
self.resume(); this.resume();
callback(); callback();
}, },
options options

View File

@@ -5,21 +5,21 @@
* MIT Licensed. * MIT Licensed.
*/ */
const Class = require("./class.js"); const Class = require("./class.js");
const Log = require("./logger.js"); const Log = require("logger");
const express = require("express"); const express = require("express");
var NodeHelper = Class.extend({ const NodeHelper = Class.extend({
init: function () { init() {
Log.log("Initializing new module helper ..."); Log.log("Initializing new module helper ...");
}, },
loaded: function (callback) { loaded(callback) {
Log.log("Module helper loaded: " + this.name); Log.log(`Module helper loaded: ${this.name}`);
callback(); callback();
}, },
start: function () { start() {
Log.log("Starting module helper: " + this.name); Log.log(`Starting module helper: ${this.name}`);
}, },
/* stop() /* stop()
@@ -28,8 +28,8 @@ var NodeHelper = Class.extend({
* gracefully exit the module. * gracefully exit the module.
* *
*/ */
stop: function () { stop() {
Log.log("Stopping module helper: " + this.name); Log.log(`Stopping module helper: ${this.name}`);
}, },
/* socketNotificationReceived(notification, payload) /* socketNotificationReceived(notification, payload)
@@ -38,8 +38,8 @@ var NodeHelper = Class.extend({
* argument notification string - The identifier of the notification. * argument notification string - The identifier of the notification.
* argument payload mixed - The payload of the notification. * argument payload mixed - The payload of the notification.
*/ */
socketNotificationReceived: function (notification, payload) { socketNotificationReceived(notification, payload) {
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload); Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
}, },
/* setName(name) /* setName(name)
@@ -47,7 +47,7 @@ var NodeHelper = Class.extend({
* *
* argument name string - Module name. * argument name string - Module name.
*/ */
setName: function (name) { setName(name) {
this.name = name; this.name = name;
}, },
@@ -56,7 +56,7 @@ var NodeHelper = Class.extend({
* *
* argument path string - Module path. * argument path string - Module path.
*/ */
setPath: function (path) { setPath(path) {
this.path = path; this.path = path;
}, },
@@ -66,7 +66,7 @@ var NodeHelper = Class.extend({
* argument notification string - The identifier of the notification. * argument notification string - The identifier of the notification.
* argument payload mixed - The payload of the notification. * argument payload mixed - The payload of the notification.
*/ */
sendSocketNotification: function (notification, payload) { sendSocketNotification(notification, payload) {
this.io.of(this.name).emit(notification, payload); this.io.of(this.name).emit(notification, payload);
}, },
@@ -76,11 +76,10 @@ var NodeHelper = Class.extend({
* *
* argument app Express app - The Express app object. * argument app Express app - The Express app object.
*/ */
setExpressApp: function (app) { setExpressApp(app) {
this.expressApp = app; this.expressApp = app;
var publicPath = this.path + "/public"; app.use(`/${this.name}`, express.static(`${this.path}/public`));
app.use("/" + this.name, express.static(publicPath));
}, },
/* setSocketIO(io) /* setSocketIO(io)
@@ -89,27 +88,25 @@ var NodeHelper = Class.extend({
* *
* argument io Socket.io - The Socket io object. * argument io Socket.io - The Socket io object.
*/ */
setSocketIO: function (io) { setSocketIO(io) {
var self = this; this.io = io;
self.io = io;
Log.log("Connecting socket for: " + this.name); Log.log(`Connecting socket for: ${this.name}`);
var namespace = this.name;
io.of(namespace).on("connection", function (socket) { io.of(this.name).on("connection", (socket) => {
// add a catch all event. // add a catch all event.
var onevent = socket.onevent; const onevent = socket.onevent;
socket.onevent = function (packet) { socket.onevent = function (packet) {
var args = packet.data || []; const args = packet.data || [];
onevent.call(this, packet); // original call onevent.call(this, packet); // original call
packet.data = ["*"].concat(args); packet.data = ["*"].concat(args);
onevent.call(this, packet); // additional call to catch-all onevent.call(this, packet); // additional call to catch-all
}; };
// register catch all. // register catch all.
socket.on("*", function (notification, payload) { socket.on("*", (notification, payload) => {
if (notification !== "*") { if (notification !== "*") {
//Log.log('received message in namespace: ' + namespace); this.socketNotificationReceived(notification, payload);
self.socketNotificationReceived(notification, payload);
} }
}); });
}); });
@@ -120,7 +117,4 @@ NodeHelper.create = function (moduleDefinition) {
return NodeHelper.extend(moduleDefinition); return NodeHelper.extend(moduleDefinition);
}; };
/*************** DO NOT EDIT THE LINE BELOW ***************/ module.exports = NodeHelper;
if (typeof module !== "undefined") {
module.exports = NodeHelper;
}

View File

@@ -4,25 +4,29 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
var express = require("express"); const express = require("express");
var app = require("express")(); const app = require("express")();
var path = require("path"); const path = require("path");
var ipfilter = require("express-ipfilter").IpFilter; const ipfilter = require("express-ipfilter").IpFilter;
var fs = require("fs"); const fs = require("fs");
var helmet = require("helmet"); const helmet = require("helmet");
var Log = require("./logger.js"); const Log = require("logger");
var Utils = require("./utils.js"); const Utils = require("./utils.js");
var Server = function (config, callback) { /**
var port = config.port; * Server
if (process.env.MM_PORT) { *
port = process.env.MM_PORT; * @param {object} config The MM config
} * @param {Function} callback Function called when done.
* @class
*/
function Server(config, callback) {
const port = process.env.MM_PORT || config.port;
var server = null; let server = null;
if (config.useHttps) { if (config.useHttps) {
var options = { const options = {
key: fs.readFileSync(config.httpsPrivateKey), key: fs.readFileSync(config.httpsPrivateKey),
cert: fs.readFileSync(config.httpsCertificate) cert: fs.readFileSync(config.httpsCertificate)
}; };
@@ -30,18 +34,24 @@ var Server = function (config, callback) {
} else { } else {
server = require("http").Server(app); server = require("http").Server(app);
} }
var io = require("socket.io")(server); const io = require("socket.io")(server, {
cors: {
origin: /.*$/,
credentials: true
},
allowEIO3: true
});
Log.log("Starting server on port " + port + " ... "); Log.log(`Starting server on port ${port} ... `);
server.listen(port, config.address ? config.address : "localhost"); server.listen(port, config.address || "localhost");
if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) { if (config.ipWhitelist instanceof Array && config.ipWhitelist.length === 0) {
Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs")); Log.warn(Utils.colors.warn("You're using a full whitelist configuration to allow for all IPs"));
} }
app.use(function (req, res, next) { app.use(function (req, res, next) {
var result = ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) { ipfilter(config.ipWhitelist, { mode: config.ipWhitelist.length === 0 ? "deny" : "allow", log: false })(req, res, function (err) {
if (err === undefined) { if (err === undefined) {
return next(); return next();
} }
@@ -52,10 +62,9 @@ var Server = function (config, callback) {
app.use(helmet({ contentSecurityPolicy: false })); app.use(helmet({ contentSecurityPolicy: false }));
app.use("/js", express.static(__dirname)); app.use("/js", express.static(__dirname));
var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
var directory; const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
for (var i in directories) { for (const directory of directories) {
directory = directories[i];
app.use(directory, express.static(path.resolve(global.root_path + directory))); app.use(directory, express.static(path.resolve(global.root_path + directory)));
} }
@@ -68,10 +77,10 @@ var Server = function (config, callback) {
}); });
app.get("/", function (req, res) { app.get("/", function (req, res) {
var html = fs.readFileSync(path.resolve(global.root_path + "/index.html"), { encoding: "utf8" }); let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" });
html = html.replace("#VERSION#", global.version); html = html.replace("#VERSION#", global.version);
var configFile = "config/config.js"; let configFile = "config/config.js";
if (typeof global.configuration_file !== "undefined") { if (typeof global.configuration_file !== "undefined") {
configFile = global.configuration_file; configFile = global.configuration_file;
} }
@@ -83,6 +92,6 @@ var Server = function (config, callback) {
if (typeof callback === "function") { if (typeof callback === "function") {
callback(app, io); callback(app, io);
} }
}; }
module.exports = Server; module.exports = Server;

View File

@@ -14,7 +14,7 @@ var Translator = (function () {
* @param {Function} callback Function called when done. * @param {Function} callback Function called when done.
*/ */
function loadJSON(file, callback) { function loadJSON(file, callback) {
var xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.overrideMimeType("application/json"); xhr.overrideMimeType("application/json");
xhr.open("GET", file, true); xhr.open("GET", file, true);
xhr.onreadystatechange = function () { xhr.onreadystatechange = function () {
@@ -103,26 +103,19 @@ var Translator = (function () {
* @param {boolean} isFallback Flag to indicate fallback translations. * @param {boolean} isFallback Flag to indicate fallback translations.
* @param {Function} callback Function called when done. * @param {Function} callback Function called when done.
*/ */
load: function (module, file, isFallback, callback) { load(module, file, isFallback, callback) {
if (!isFallback) { Log.log(`${module.name} - Load translation${isFallback && " fallback"}: ${file}`);
Log.log(module.name + " - Load translation: " + file);
} else { if (this.translationsFallback[module.name]) {
Log.log(module.name + " - Load translation fallback: " + file); callback();
return;
} }
var self = this; loadJSON(module.file(file), (json) => {
if (!this.translationsFallback[module.name]) { const property = isFallback ? "translationsFallback" : "translations";
loadJSON(module.file(file), function (json) { this[property][module.name] = json;
if (!isFallback) {
self.translations[module.name] = json;
} else {
self.translationsFallback[module.name] = json;
}
callback(); callback();
}); });
} else {
callback();
}
}, },
/** /**
@@ -131,18 +124,16 @@ var Translator = (function () {
* @param {string} lang The language identifier of the core language. * @param {string} lang The language identifier of the core language.
*/ */
loadCoreTranslations: function (lang) { loadCoreTranslations: function (lang) {
var self = this;
if (lang in translations) { if (lang in translations) {
Log.log("Loading core translation file: " + translations[lang]); Log.log("Loading core translation file: " + translations[lang]);
loadJSON(translations[lang], function (translations) { loadJSON(translations[lang], (translations) => {
self.coreTranslations = translations; this.coreTranslations = translations;
}); });
} else { } else {
Log.log("Configured language not found in core translations."); Log.log("Configured language not found in core translations.");
} }
self.loadCoreTranslationsFallback(); this.loadCoreTranslationsFallback();
}, },
/** /**
@@ -150,8 +141,6 @@ var Translator = (function () {
* The first language defined in translations.js will be used. * The first language defined in translations.js will be used.
*/ */
loadCoreTranslationsFallback: function () { loadCoreTranslationsFallback: function () {
var self = this;
// The variable `first` will contain the first // The variable `first` will contain the first
// defined translation after the following line. // defined translation after the following line.
for (var first in translations) { for (var first in translations) {
@@ -160,8 +149,8 @@ var Translator = (function () {
if (first) { if (first) {
Log.log("Loading core translation fallback file: " + translations[first]); Log.log("Loading core translation fallback file: " + translations[first]);
loadJSON(translations[first], function (translations) { loadJSON(translations[first], (translations) => {
self.coreTranslationsFallback = translations; this.coreTranslationsFallback = translations;
}); });
} }
} }

View File

@@ -4,9 +4,9 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com * By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed. * MIT Licensed.
*/ */
var colors = require("colors/safe"); const colors = require("colors/safe");
var Utils = { module.exports = {
colors: { colors: {
warn: colors.yellow, warn: colors.yellow,
error: colors.red, error: colors.red,
@@ -14,7 +14,3 @@ var Utils = {
pass: colors.green pass: colors.green
} }
}; };
if (typeof module !== "undefined") {
module.exports = Utils;
}

View File

@@ -36,6 +36,7 @@ Module.register("calendar", {
fadePoint: 0.25, // Start on 1/4th of the list. fadePoint: 0.25, // Start on 1/4th of the list.
hidePrivate: false, hidePrivate: false,
hideOngoing: false, hideOngoing: false,
hideTime: false,
colored: false, colored: false,
coloredSymbolOnly: false, coloredSymbolOnly: false,
customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched customEvents: [], // Array of {keyword: "", symbol: "", color: ""} where Keyword is a regexp and symbol/color are to be applied for matched
@@ -57,7 +58,8 @@ Module.register("calendar", {
excludedEvents: [], excludedEvents: [],
sliceMultiDayEvents: false, sliceMultiDayEvents: false,
broadcastPastEvents: false, broadcastPastEvents: false,
nextDaysRelative: false nextDaysRelative: false,
selfSignedCert: false
}, },
requiresVersion: "2.1.0", requiresVersion: "2.1.0",
@@ -75,7 +77,7 @@ Module.register("calendar", {
// Define required translations. // Define required translations.
getTranslations: function () { getTranslations: function () {
// The translations for the default modules are defined in the core translation files. // The translations for the default modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionary. // Therefore we can just return false. Otherwise we should have returned a dictionary.
// If you're trying to build your own module including translations, check out the documentation. // If you're trying to build your own module including translations, check out the documentation.
return false; return false;
}, },
@@ -93,15 +95,16 @@ Module.register("calendar", {
// indicate no data available yet // indicate no data available yet
this.loaded = false; this.loaded = false;
for (var c in this.config.calendars) { this.config.calendars.forEach((calendar) => {
var calendar = this.config.calendars[c];
calendar.url = calendar.url.replace("webcal://", "http://"); calendar.url = calendar.url.replace("webcal://", "http://");
var calendarConfig = { const calendarConfig = {
maximumEntries: calendar.maximumEntries, maximumEntries: calendar.maximumEntries,
maximumNumberOfDays: calendar.maximumNumberOfDays, maximumNumberOfDays: calendar.maximumNumberOfDays,
broadcastPastEvents: calendar.broadcastPastEvents broadcastPastEvents: calendar.broadcastPastEvents,
selfSignedCert: calendar.selfSignedCert
}; };
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) { if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
calendarConfig.symbolClass = ""; calendarConfig.symbolClass = "";
} }
@@ -125,7 +128,7 @@ Module.register("calendar", {
// tell helper to start a fetcher for this calendar // tell helper to start a fetcher for this calendar
// fetcher till cycle // fetcher till cycle
this.addCalendar(calendar.url, calendar.auth, calendarConfig); this.addCalendar(calendar.url, calendar.auth, calendarConfig);
} });
}, },
// Override socket notification handler. // Override socket notification handler.
@@ -161,8 +164,8 @@ Module.register("calendar", {
const oneHour = oneMinute * 60; const oneHour = oneMinute * 60;
const oneDay = oneHour * 24; const oneDay = oneHour * 24;
var events = this.createEventList(); const events = this.createEventList();
var wrapper = document.createElement("table"); const wrapper = document.createElement("table");
wrapper.className = this.config.tableClass; wrapper.className = this.config.tableClass;
if (events.length === 0) { if (events.length === 0) {
@@ -171,37 +174,37 @@ Module.register("calendar", {
return wrapper; return wrapper;
} }
let currentFadeStep = 0;
let startFade;
let fadeSteps;
if (this.config.fade && this.config.fadePoint < 1) { if (this.config.fade && this.config.fadePoint < 1) {
if (this.config.fadePoint < 0) { if (this.config.fadePoint < 0) {
this.config.fadePoint = 0; this.config.fadePoint = 0;
} }
var startFade = events.length * this.config.fadePoint; startFade = events.length * this.config.fadePoint;
var fadeSteps = events.length - startFade; fadeSteps = events.length - startFade;
} }
var currentFadeStep = 0; let lastSeenDate = "";
var lastSeenDate = "";
var ev;
var needle;
for (var e in events) { events.forEach((event, index) => {
var event = events[e]; const dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
var dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
if (this.config.timeFormat === "dateheaders") { if (this.config.timeFormat === "dateheaders") {
if (lastSeenDate !== dateAsString) { if (lastSeenDate !== dateAsString) {
var dateRow = document.createElement("tr"); const dateRow = document.createElement("tr");
dateRow.className = "normal"; dateRow.className = "normal";
var dateCell = document.createElement("td");
const dateCell = document.createElement("td");
dateCell.colSpan = "3"; dateCell.colSpan = "3";
dateCell.innerHTML = dateAsString; dateCell.innerHTML = dateAsString;
dateCell.style.paddingTop = "10px"; dateCell.style.paddingTop = "10px";
dateRow.appendChild(dateCell); dateRow.appendChild(dateCell);
wrapper.appendChild(dateRow); wrapper.appendChild(dateRow);
if (e >= startFade) { if (this.config.fade && index >= startFade) {
//fading //fading
currentFadeStep = e - startFade; currentFadeStep = index - startFade;
dateRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep; dateRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
} }
@@ -209,7 +212,7 @@ Module.register("calendar", {
} }
} }
var eventWrapper = document.createElement("tr"); const eventWrapper = document.createElement("tr");
if (this.config.colored && !this.config.coloredSymbolOnly) { if (this.config.colored && !this.config.coloredSymbolOnly) {
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url); eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
@@ -217,22 +220,22 @@ Module.register("calendar", {
eventWrapper.className = "normal event"; eventWrapper.className = "normal event";
if (this.config.displaySymbol) { const symbolWrapper = document.createElement("td");
var symbolWrapper = document.createElement("td");
if (this.config.displaySymbol) {
if (this.config.colored && this.config.coloredSymbolOnly) { if (this.config.colored && this.config.coloredSymbolOnly) {
symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url); symbolWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
} }
var symbolClass = this.symbolClassForUrl(event.url); const symbolClass = this.symbolClassForUrl(event.url);
symbolWrapper.className = "symbol align-right " + symbolClass; symbolWrapper.className = "symbol align-right " + symbolClass;
var symbols = this.symbolsForEvent(event); const symbols = this.symbolsForEvent(event);
// If symbols are displayed and custom symbol is set, replace event symbol // If symbols are displayed and custom symbol is set, replace event symbol
if (this.config.displaySymbol && this.config.customEvents.length > 0) { if (this.config.displaySymbol && this.config.customEvents.length > 0) {
for (ev in this.config.customEvents) { for (let ev in this.config.customEvents) {
if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") { if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") {
needle = new RegExp(this.config.customEvents[ev].keyword, "gi"); let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
if (needle.test(event.title)) { if (needle.test(event.title)) {
symbols[0] = this.config.customEvents[ev].symbol; symbols[0] = this.config.customEvents[ev].symbol;
break; break;
@@ -240,31 +243,29 @@ Module.register("calendar", {
} }
} }
} }
symbols.forEach((s, index) => {
for (var i = 0; i < symbols.length; i++) { const symbol = document.createElement("span");
var symbol = document.createElement("span"); symbol.className = "fa fa-fw fa-" + s;
symbol.className = "fa fa-fw fa-" + symbols[i]; if (index > 0) {
if (i > 0) {
symbol.style.paddingLeft = "5px"; symbol.style.paddingLeft = "5px";
} }
symbolWrapper.appendChild(symbol); symbolWrapper.appendChild(symbol);
} });
eventWrapper.appendChild(symbolWrapper); eventWrapper.appendChild(symbolWrapper);
} else if (this.config.timeFormat === "dateheaders") { } else if (this.config.timeFormat === "dateheaders") {
var blankCell = document.createElement("td"); const blankCell = document.createElement("td");
blankCell.innerHTML = "&nbsp;&nbsp;&nbsp;"; blankCell.innerHTML = "&nbsp;&nbsp;&nbsp;";
eventWrapper.appendChild(blankCell); eventWrapper.appendChild(blankCell);
} }
var titleWrapper = document.createElement("td"), const titleWrapper = document.createElement("td");
repeatingCountTitle = ""; let repeatingCountTitle = "";
if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) { if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) {
repeatingCountTitle = this.countTitleForUrl(event.url); repeatingCountTitle = this.countTitleForUrl(event.url);
if (repeatingCountTitle !== "") { if (repeatingCountTitle !== "") {
var thisYear = new Date(parseInt(event.startDate)).getFullYear(), const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
yearDiff = thisYear - event.firstYear; yearDiff = thisYear - event.firstYear;
repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle; repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle;
@@ -273,12 +274,15 @@ Module.register("calendar", {
// Color events if custom color is specified // Color events if custom color is specified
if (this.config.customEvents.length > 0) { if (this.config.customEvents.length > 0) {
for (ev in this.config.customEvents) { for (let ev in this.config.customEvents) {
if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") { if (typeof this.config.customEvents[ev].color !== "undefined" && this.config.customEvents[ev].color !== "") {
needle = new RegExp(this.config.customEvents[ev].keyword, "gi"); let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
if (needle.test(event.title)) { if (needle.test(event.title)) {
// Respect parameter ColoredSymbolOnly also for custom events
if (!this.config.coloredSymbolOnly) {
eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color; eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
titleWrapper.style.cssText = "color:" + this.config.customEvents[ev].color; titleWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
}
if (this.config.displaySymbol) { if (this.config.displaySymbol) {
symbolWrapper.style.cssText = "color:" + this.config.customEvents[ev].color; symbolWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
} }
@@ -290,7 +294,7 @@ Module.register("calendar", {
titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle; titleWrapper.innerHTML = this.titleTransform(event.title, this.config.titleReplace, this.config.wrapEvents, this.config.maxTitleLength, this.config.maxTitleLines) + repeatingCountTitle;
var titleClass = this.titleClassForUrl(event.url); const titleClass = this.titleClassForUrl(event.url);
if (!this.config.colored) { if (!this.config.colored) {
titleWrapper.className = "title bright " + titleClass; titleWrapper.className = "title bright " + titleClass;
@@ -298,14 +302,12 @@ Module.register("calendar", {
titleWrapper.className = "title " + titleClass; titleWrapper.className = "title " + titleClass;
} }
var timeWrapper;
if (this.config.timeFormat === "dateheaders") { if (this.config.timeFormat === "dateheaders") {
if (event.fullDayEvent) { if (event.fullDayEvent) {
titleWrapper.colSpan = "2"; titleWrapper.colSpan = "2";
titleWrapper.align = "left"; titleWrapper.align = "left";
} else { } else {
timeWrapper = document.createElement("td"); const timeWrapper = document.createElement("td");
timeWrapper.className = "time light " + this.timeClassForUrl(event.url); timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
timeWrapper.align = "left"; timeWrapper.align = "left";
timeWrapper.style.paddingLeft = "2px"; timeWrapper.style.paddingLeft = "2px";
@@ -316,10 +318,10 @@ Module.register("calendar", {
eventWrapper.appendChild(titleWrapper); eventWrapper.appendChild(titleWrapper);
} else { } else {
timeWrapper = document.createElement("td"); const timeWrapper = document.createElement("td");
eventWrapper.appendChild(titleWrapper); eventWrapper.appendChild(titleWrapper);
var now = new Date(); const now = new Date();
if (this.config.timeFormat === "absolute") { if (this.config.timeFormat === "absolute") {
// Use dateFormat // Use dateFormat
@@ -363,7 +365,17 @@ Module.register("calendar", {
// Show relative times // Show relative times
if (event.startDate >= now) { if (event.startDate >= now) {
// Use relative time // Use relative time
if (!this.config.hideTime) {
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar()); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
} else {
timeWrapper.innerHTML = this.capFirst(
moment(event.startDate, "x").calendar(null, {
sameDay: "[" + this.translate("TODAY") + "]",
nextDay: "[" + this.translate("TOMORROW") + "]",
nextWeek: "dddd"
})
);
}
if (event.startDate - now < this.config.getRelative * oneHour) { if (event.startDate - now < this.config.getRelative * oneHour) {
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow() // If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow()); timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
@@ -385,22 +397,22 @@ Module.register("calendar", {
wrapper.appendChild(eventWrapper); wrapper.appendChild(eventWrapper);
// Create fade effect. // Create fade effect.
if (e >= startFade) { if (index >= startFade) {
currentFadeStep = e - startFade; currentFadeStep = index - startFade;
eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep; eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
} }
if (this.config.showLocation) { if (this.config.showLocation) {
if (event.location !== false) { if (event.location !== false) {
var locationRow = document.createElement("tr"); const locationRow = document.createElement("tr");
locationRow.className = "normal xsmall light"; locationRow.className = "normal xsmall light";
if (this.config.displaySymbol) { if (this.config.displaySymbol) {
var symbolCell = document.createElement("td"); const symbolCell = document.createElement("td");
locationRow.appendChild(symbolCell); locationRow.appendChild(symbolCell);
} }
var descCell = document.createElement("td"); const descCell = document.createElement("td");
descCell.className = "location"; descCell.className = "location";
descCell.colSpan = "2"; descCell.colSpan = "2";
descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines); descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines);
@@ -408,13 +420,13 @@ Module.register("calendar", {
wrapper.appendChild(locationRow); wrapper.appendChild(locationRow);
if (e >= startFade) { if (index >= startFade) {
currentFadeStep = e - startFade; currentFadeStep = index - startFade;
locationRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep; locationRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
} }
} }
} }
} });
return wrapper; return wrapper;
}, },
@@ -448,8 +460,7 @@ Module.register("calendar", {
* @returns {boolean} True if the calendar config contains the url, False otherwise * @returns {boolean} True if the calendar config contains the url, False otherwise
*/ */
hasCalendarURL: function (url) { hasCalendarURL: function (url) {
for (var c in this.config.calendars) { for (const calendar of this.config.calendars) {
var calendar = this.config.calendars[c];
if (calendar.url === url) { if (calendar.url === url) {
return true; return true;
} }
@@ -464,14 +475,15 @@ Module.register("calendar", {
* @returns {object[]} Array with events. * @returns {object[]} Array with events.
*/ */
createEventList: function () { createEventList: function () {
var events = []; const now = new Date();
var today = moment().startOf("day"); const today = moment().startOf("day");
var now = new Date(); const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
var future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate(); let events = [];
for (var c in this.calendarData) {
var calendar = this.calendarData[c]; for (const calendarUrl in this.calendarData) {
for (var e in calendar) { const calendar = this.calendarData[calendarUrl];
var event = JSON.parse(JSON.stringify(calendar[e])); // clone object for (const e in calendar) {
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
if (event.endDate < now) { if (event.endDate < now) {
continue; continue;
@@ -490,19 +502,19 @@ Module.register("calendar", {
if (this.listContainsEvent(events, event)) { if (this.listContainsEvent(events, event)) {
continue; continue;
} }
event.url = c; event.url = calendarUrl;
event.today = event.startDate >= today && event.startDate < today + 24 * 60 * 60 * 1000; event.today = event.startDate >= today && event.startDate < today + 24 * 60 * 60 * 1000;
/* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days, /* if sliceMultiDayEvents is set to true, multiday events (events exceeding at least one midnight) are sliced into days,
* otherwise, esp. in dateheaders mode it is not clear how long these events are. * otherwise, esp. in dateheaders mode it is not clear how long these events are.
*/ */
var maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / (1000 * 60 * 60 * 24)) + 1; const maxCount = Math.ceil((event.endDate - 1 - moment(event.startDate, "x").endOf("day").format("x")) / (1000 * 60 * 60 * 24)) + 1;
if (this.config.sliceMultiDayEvents && maxCount > 1) { if (this.config.sliceMultiDayEvents && maxCount > 1) {
var splitEvents = []; const splitEvents = [];
var midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x"); let midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
var count = 1; let count = 1;
while (event.endDate > midnight) { while (event.endDate > midnight) {
var thisEvent = JSON.parse(JSON.stringify(event)); // clone object const thisEvent = JSON.parse(JSON.stringify(event)); // clone object
thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + 24 * 60 * 60 * 1000; thisEvent.today = thisEvent.startDate >= today && thisEvent.startDate < today + 24 * 60 * 60 * 1000;
thisEvent.endDate = midnight; thisEvent.endDate = midnight;
thisEvent.title += " (" + count + "/" + maxCount + ")"; thisEvent.title += " (" + count + "/" + maxCount + ")";
@@ -516,9 +528,9 @@ Module.register("calendar", {
event.title += " (" + count + "/" + maxCount + ")"; event.title += " (" + count + "/" + maxCount + ")";
splitEvents.push(event); splitEvents.push(event);
for (event of splitEvents) { for (let splitEvent of splitEvents) {
if (event.endDate > now && event.endDate <= future) { if (splitEvent.endDate > now && splitEvent.endDate <= future) {
events.push(event); events.push(splitEvent);
} }
} }
} else { } else {
@@ -534,12 +546,11 @@ Module.register("calendar", {
// Limit the number of days displayed // Limit the number of days displayed
// If limitDays is set > 0, limit display to that number of days // If limitDays is set > 0, limit display to that number of days
if (this.config.limitDays > 0) { if (this.config.limitDays > 0) {
var newEvents = []; let newEvents = [];
var lastDate = today.clone().subtract(1, "days").format("YYYYMMDD"); let lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
var days = 0; let days = 0;
var eventDate; for (const ev of events) {
for (var ev of events) { let eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
// if date of event is later than lastdate // if date of event is later than lastdate
// check if we already are showing max unique days // check if we already are showing max unique days
if (eventDate > lastDate) { if (eventDate > lastDate) {
@@ -563,7 +574,7 @@ Module.register("calendar", {
}, },
listContainsEvent: function (eventList, event) { listContainsEvent: function (eventList, event) {
for (var evt of eventList) { for (const evt of eventList) {
if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)) { if (evt.title === event.title && parseInt(evt.startDate) === parseInt(event.startDate)) {
return true; return true;
} }
@@ -579,8 +590,6 @@ Module.register("calendar", {
* @param {object} calendarConfig The config of the specific calendar * @param {object} calendarConfig The config of the specific calendar
*/ */
addCalendar: function (url, auth, calendarConfig) { addCalendar: function (url, auth, calendarConfig) {
var self = this;
this.sendSocketNotification("ADD_CALENDAR", { this.sendSocketNotification("ADD_CALENDAR", {
id: this.identifier, id: this.identifier,
url: url, url: url,
@@ -592,7 +601,8 @@ Module.register("calendar", {
titleClass: calendarConfig.titleClass, titleClass: calendarConfig.titleClass,
timeClass: calendarConfig.timeClass, timeClass: calendarConfig.timeClass,
auth: auth, auth: auth,
broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents,
selfSignedCert: calendarConfig.selfSignedCert || this.config.selfSignedCert
}); });
}, },
@@ -693,8 +703,7 @@ Module.register("calendar", {
* @returns {*} The property * @returns {*} The property
*/ */
getCalendarProperty: function (url, property, defaultValue) { getCalendarProperty: function (url, property, defaultValue) {
for (var c in this.config.calendars) { for (const calendar of this.config.calendars) {
var calendar = this.config.calendars[c];
if (calendar.url === url && calendar.hasOwnProperty(property)) { if (calendar.url === url && calendar.hasOwnProperty(property)) {
return calendar[property]; return calendar[property];
} }
@@ -728,13 +737,13 @@ Module.register("calendar", {
} }
if (wrapEvents === true) { if (wrapEvents === true) {
var temp = ""; const words = string.split(" ");
var currentLine = ""; let temp = "";
var words = string.split(" "); let currentLine = "";
var line = 0; let line = 0;
for (var i = 0; i < words.length; i++) { for (let i = 0; i < words.length; i++) {
var word = words[i]; const word = words[i];
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) { if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
// max - 1 to account for a space // max - 1 to account for a space
currentLine += word + " "; currentLine += word + " ";
@@ -789,10 +798,10 @@ Module.register("calendar", {
* @returns {string} The transformed title. * @returns {string} The transformed title.
*/ */
titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) { titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
for (var needle in titleReplace) { for (let needle in titleReplace) {
var replacement = titleReplace[needle]; const replacement = titleReplace[needle];
var regParts = needle.match(/^\/(.+)\/([gim]*)$/); const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
if (regParts) { if (regParts) {
// the parsed pattern is a regexp. // the parsed pattern is a regexp.
needle = new RegExp(regParts[1], regParts[2]); needle = new RegExp(regParts[1], regParts[2]);
@@ -810,11 +819,10 @@ Module.register("calendar", {
* The all events available in one array, sorted on startdate. * The all events available in one array, sorted on startdate.
*/ */
broadcastEvents: function () { broadcastEvents: function () {
var eventList = []; const eventList = [];
for (var url in this.calendarData) { for (const url in this.calendarData) {
var calendar = this.calendarData[url]; for (const ev of this.calendarData[url]) {
for (var e in calendar) { const event = cloneObject(ev);
var event = cloneObject(calendar[e]);
event.symbol = this.symbolsForEvent(event); event.symbol = this.symbolsForEvent(event);
event.calendarName = this.calendarNameForUrl(url); event.calendarName = this.calendarNameForUrl(url);
event.color = this.colorForUrl(url); event.color = this.colorForUrl(url);

View File

@@ -4,17 +4,12 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
const Log = require("../../../js/logger.js"); const CalendarUtils = require("./calendarutils");
const Log = require("logger");
const ical = require("node-ical"); const ical = require("node-ical");
const request = require("request"); const fetch = require("node-fetch");
const digest = require("digest-fetch");
/** const https = require("https");
* Moment date
*
* @external Moment
* @see {@link http://momentjs.com}
*/
const moment = require("moment");
/** /**
* *
@@ -25,11 +20,10 @@ const moment = require("moment");
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future. * @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
* @param {object} auth The object containing options for authentication against the calendar. * @param {object} auth The object containing options for authentication against the calendar.
* @param {boolean} includePastEvents If true events from the past maximumNumberOfDays will be fetched too * @param {boolean} includePastEvents If true events from the past maximumNumberOfDays will be fetched too
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
* @class * @class
*/ */
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents) { const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents, selfSignedCert) {
const self = this;
let reloadTimer = null; let reloadTimer = null;
let events = []; let events = [];
@@ -39,494 +33,69 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
/** /**
* Initiates calendar fetch. * Initiates calendar fetch.
*/ */
const fetchCalendar = function () { const fetchCalendar = () => {
clearTimeout(reloadTimer); clearTimeout(reloadTimer);
reloadTimer = null; reloadTimer = null;
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
const opts = { let fetcher = null;
headers: { let httpsAgent = null;
let headers = {
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)" "User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
},
gzip: true
}; };
if (selfSignedCert) {
httpsAgent = new https.Agent({
rejectUnauthorized: false
});
}
if (auth) { if (auth) {
if (auth.method === "bearer") { if (auth.method === "bearer") {
opts.auth = { headers.Authorization = "Bearer " + auth.pass;
bearer: auth.pass } else if (auth.method === "digest") {
}; fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, httpsAgent: httpsAgent });
} else { } else {
opts.auth = { headers.Authorization = "Basic " + Buffer.from(auth.user + ":" + auth.pass).toString("base64");
user: auth.user,
pass: auth.pass,
sendImmediately: auth.method !== "digest"
};
} }
} }
if (fetcher === null) {
fetcher = fetch(url, { headers: headers, httpsAgent: httpsAgent });
}
request(url, opts, function (err, r, requestData) { fetcher
if (err) { .catch((error) => {
fetchFailedCallback(self, err); fetchFailedCallback(this, error);
scheduleTimer(); scheduleTimer();
return; })
} else if (r.statusCode !== 200) { .then((response) => {
fetchFailedCallback(self, r.statusCode + ": " + r.statusMessage); if (response.status !== 200) {
fetchFailedCallback(this, response.statusText);
scheduleTimer(); scheduleTimer();
return;
} }
return response;
})
.then((response) => response.text())
.then((responseData) => {
let data = []; let data = [];
try { try {
data = ical.parseICS(requestData); data = ical.parseICS(responseData);
Log.debug("parsed data=" + JSON.stringify(data));
events = CalendarUtils.filterEvents(data, {
excludedEvents,
includePastEvents,
maximumEntries,
maximumNumberOfDays
});
} catch (error) { } catch (error) {
fetchFailedCallback(self, error.message); fetchFailedCallback(this, error.message);
scheduleTimer(); scheduleTimer();
return; return;
} }
this.broadcastEvents();
Log.debug(" parsed data=" + JSON.stringify(data));
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 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]) => {
const now = new Date();
const today = moment().startOf("day").toDate();
const future = moment().startOf("day").add(maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
let past = today;
Log.debug("have entries ");
if (includePastEvents) {
past = moment().startOf("day").subtract(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") {
let startDate = eventDate(event, "start");
let endDate;
Log.debug("\nevent=" + JSON.stringify(event));
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() + " end=" + endDate.toDate());
// calculate the duration of the event for use with recurring events.
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
if (event.start.length === 8) {
startDate = startDate.startOf("day");
}
const title = getTitleFromEvent(event);
let excluded = false,
dateFilter = null;
for (let f in excludedEvents) {
let filter = 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 (testTitleByFilter(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 (isFullDayEvent(event)) {
// if full day event, only use the date part of the ranges
pastLocal = pastMoment.toDate();
futureLocal = futureMoment.toDate();
} else {
// if we want past events
if (includePastEvents) {
// use the calculated past time for the between from
pastLocal = pastMoment.toDate();
} else {
// otherwise use NOW.. cause we shouldnt use any before now
pastLocal = moment().toDate(); //now
}
futureLocal = futureMoment.toDate(); // future
}
Log.debug(" between=" + pastLocal + " to " + futureLocal);
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
Log.debug("title=" + event.summary + " 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.
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];
// ical.js started returning recurrences and exdates as ISOStrings without time information.
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
// (see https://github.com/peterbraden/ical.js/pull/84 )
const dateKey = date.toISOString().substring(0, 10);
let curEvent = event;
let showRecurrence = true;
// for full day events, the time might be off from RRULE/Luxon problem
if (isFullDayEvent(event)) {
Log.debug("fullday");
// if the offset is negative, east of GMT where the problem is
if (date.getTimezoneOffset() < 0) {
// get the offset of today where we are processing
// this will be the correction we need to apply
let nowOffset = new Date().getTimezoneOffset();
Log.debug("now offset is " + nowOffset);
// reduce the time by the offset
Log.debug(" recurring date is " + date + " offset is " + date.getTimezoneOffset());
// apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(nowOffset) * 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 date is " + date);
}
}
startDate = moment(date);
let adjustDays = getCorrection(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 = 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 (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: 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 : isFullDayEvent(event);
// Log.debug("full day event")
if (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 (timeFilterApplies(now, endDate, dateFilter)) {
return;
}
// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
if (fullDayEvent && startDate <= today) {
startDate = moment(today);
}
// 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 = getCorrection(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;
});
// include up to maximumEntries current or upcoming events
// If past events should be included, include all past events
const now = moment();
var entries = 0;
events = [];
for (let ne of newEvents) {
if (moment(ne.endDate, "x").isBefore(now)) {
if (includePastEvents) events.push(ne);
continue;
}
entries++;
// If max events has been saved, skip the rest
if (entries > maximumEntries) break;
events.push(ne);
}
self.broadcastEvents();
scheduleTimer(); scheduleTimer();
}); });
}; };
/*
*
* get the time correction, either dst/std or full day in cases where utc time is day before plus offset
*
*/
const getCorrection = 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 = getIanaTZFromMS(event.start.tz);
Log.debug("corrected TZ=" + tz);
// watch out for unregistered windows timezone names
// if we had a successfule 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
Log.debug("start date/time=" + moment(event.start).toDate());
start_offset = moment.tz(moment(event.start), 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;
};
/**
*
* lookup iana tz from windows
*/
let zoneTable = null;
const getIanaTZFromMS = function (msTZName) {
if (!zoneTable) {
const p = require("path");
zoneTable = require(p.join(__dirname, "windowsZones.json"));
}
// Get hash entry
const he = zoneTable[msTZName];
// If found return iana name, else null
return he ? he.iana[0] : null;
};
/** /**
* Schedule the timer for the next update. * Schedule the timer for the next update.
*/ */
@@ -537,82 +106,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
}, reloadInterval); }, reloadInterval);
}; };
/**
* 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
*/
const 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
*/
const 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;
};
/**
* 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.
*/
const 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;
};
const testTitleByFilter = 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);
} else {
return title.includes(filter);
}
};
/* public methods */ /* public methods */
/** /**
@@ -627,7 +120,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
*/ */
this.broadcastEvents = function () { this.broadcastEvents = function () {
Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events."); Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events.");
eventsReceivedCallback(self); eventsReceivedCallback(this);
}; };
/** /**

View File

@@ -0,0 +1,600 @@
/* Magic Mirror
* Calendar Util Methods
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
/**
* @external Moment
*/
const moment = require("moment");
const path = require("path");
const zoneTable = require(path.join(__dirname, "windowsZones.json"));
const Log = require("../../../js/logger.js");
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
* @param {Date} date
* @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 = 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;
},
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]) => {
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;
Log.debug("have entries ");
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") {
let startDate = eventDate(event, "start");
let endDate;
Log.debug("\nevent=" + JSON.stringify(event));
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() + " end=" + endDate.toDate());
// calculate the duration of the event for use with recurring events.
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
if (event.start.length === 8) {
startDate = startDate.startOf("day");
}
const title = CalendarUtils.getTitleFromEvent(event);
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;
}
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)) {
// if full day event, only use the date part of the ranges
pastLocal = pastMoment.toDate();
futureLocal = futureMoment.toDate();
} 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(" between=" + pastLocal + " to " + futureLocal);
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
Log.debug("title=" + event.summary + " 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.
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];
// ical.js started returning recurrences and exdates as ISOStrings without time information.
// .toISOString().substring(0,10) is the method they use to calculate keys, so we'll do the same
// (see https://github.com/peterbraden/ical.js/pull/84 )
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 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 the date hour is less than the offset
if (dh < Math.abs(dateoffset / 60)) {
// reduce the time by the offset
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
// apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(nowOffset) * 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)) {
// 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);
}
}
}
} 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)) {
// reduce the time by the offset
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
// apply the correction to the date/time to get it UTC relative
date = new Date(date.getTime() - Math.abs(nowOffset) * 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)) {
// 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);
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;
}
// Adjust start date so multiple day events will be displayed as happening today even though they started some days ago already
if (fullDayEvent && startDate <= today) {
startDate = moment(today);
}
// 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
});
}
}
});
newEvents.sort(function (a, b) {
return a.startDate - b.startDate;
});
// include up to maximumEntries current or upcoming events
// If past events should be included, include all past events
const now = moment();
let entries = 0;
let events = [];
for (let ne of newEvents) {
if (moment(ne.endDate, "x").isBefore(now)) {
if (config.includePastEvents) events.push(ne);
continue;
}
entries++;
// If max events has been saved, skip the rest
if (entries > config.maximumEntries) break;
events.push(ne);
}
return events;
},
/**
* Lookup iana tz from windows
*
* @param msTZName
* @returns {*|null}
*/
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;
},
/**
*
* @param title
* @param filter
* @param useRegex
* @param regexFlags
* @returns {boolean|*}
*/
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);
} else {
return title.includes(filter);
}
}
};
if (typeof module !== "undefined") {
module.exports = CalendarUtils;
}

View File

@@ -5,9 +5,8 @@
* MIT Licensed. * MIT Licensed.
*/ */
const NodeHelper = require("node_helper"); const NodeHelper = require("node_helper");
const validUrl = require("valid-url");
const CalendarFetcher = require("./calendarfetcher.js"); const CalendarFetcher = require("./calendarfetcher.js");
const Log = require("../../../js/logger"); const Log = require("logger");
module.exports = NodeHelper.create({ module.exports = NodeHelper.create({
// Override start method. // Override start method.
@@ -19,7 +18,7 @@ module.exports = NodeHelper.create({
// Override socketNotificationReceived method. // Override socketNotificationReceived method.
socketNotificationReceived: function (notification, payload) { socketNotificationReceived: function (notification, payload) {
if (notification === "ADD_CALENDAR") { if (notification === "ADD_CALENDAR") {
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.id); this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.selfSignedCert, payload.id);
} }
}, },
@@ -34,44 +33,55 @@ module.exports = NodeHelper.create({
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future. * @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
* @param {object} auth The object containing options for authentication against the calendar. * @param {object} auth The object containing options for authentication against the calendar.
* @param {boolean} broadcastPastEvents If true events from the past maximumNumberOfDays will be included in event broadcasts * @param {boolean} broadcastPastEvents If true events from the past maximumNumberOfDays will be included in event broadcasts
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
* @param {string} identifier ID of the module * @param {string} identifier ID of the module
*/ */
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, identifier) { createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
var self = this; try {
new URL(url);
if (!validUrl.isUri(url)) { } catch (error) {
self.sendSocketNotification("INCORRECT_URL", { id: identifier, url: url }); this.sendSocketNotification("INCORRECT_URL", { id: identifier, url: url });
return; return;
} }
var fetcher; let fetcher;
if (typeof self.fetchers[identifier + url] === "undefined") { if (typeof this.fetchers[identifier + url] === "undefined") {
Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval); Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents); fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
fetcher.onReceive(function (fetcher) { fetcher.onReceive((fetcher) => {
self.sendSocketNotification("CALENDAR_EVENTS", { this.broadcastEvents(fetcher, identifier);
id: identifier,
url: fetcher.url(),
events: fetcher.events()
});
}); });
fetcher.onError(function (fetcher, error) { fetcher.onError((fetcher, error) => {
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error); Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
self.sendSocketNotification("FETCH_ERROR", { this.sendSocketNotification("FETCH_ERROR", {
id: identifier, id: identifier,
url: fetcher.url(), url: fetcher.url(),
error: error error: error
}); });
}); });
self.fetchers[identifier + url] = fetcher; this.fetchers[identifier + url] = fetcher;
} else { } else {
Log.log("Use existing calendar fetcher for url: " + url); Log.log("Use existing calendar fetcher for url: " + url);
fetcher = self.fetchers[identifier + url]; fetcher = this.fetchers[identifier + url];
fetcher.broadcastEvents();
} }
fetcher.startFetch(); fetcher.startFetch();
},
/**
*
* @param {object} fetcher the fetcher associated with the calendar
* @param {string} identifier the identifier of the calendar
*/
broadcastEvents: function (fetcher, identifier) {
this.sendSocketNotification("CALENDAR_EVENTS", {
id: identifier,
url: fetcher.url(),
events: fetcher.events()
});
} }
}); });

View File

@@ -182,34 +182,14 @@ Module.register("compliments", {
}, },
// From data currentweather set weather type // From data currentweather set weather type
setCurrentWeatherType: function (data) { setCurrentWeatherType: function (type) {
var weatherIconTable = { this.currentWeatherType = type;
"01d": "day_sunny",
"02d": "day_cloudy",
"03d": "cloudy",
"04d": "cloudy_windy",
"09d": "showers",
"10d": "rain",
"11d": "thunderstorm",
"13d": "snow",
"50d": "fog",
"01n": "night_clear",
"02n": "night_cloudy",
"03n": "night_cloudy",
"04n": "night_cloudy",
"09n": "night_showers",
"10n": "night_rain",
"11n": "night_thunderstorm",
"13n": "night_snow",
"50n": "night_alt_cloudy_windy"
};
this.currentWeatherType = weatherIconTable[data.weather[0].icon];
}, },
// Override notification handler. // Override notification handler.
notificationReceived: function (notification, payload, sender) { notificationReceived: function (notification, payload, sender) {
if (notification === "CURRENTWEATHER_DATA") { if (notification === "CURRENTWEATHER_TYPE") {
this.setCurrentWeatherType(payload.data); this.setCurrentWeatherType(payload.type);
} }
} }
}); });

View File

@@ -1,5 +1,7 @@
# Module: Current Weather # Module: Current Weather
> :warning: **This module is deprecated in favor of the [weather](https://docs.magicmirror.builders/modules/weather.html) module.**
The `currentweather` module is one of the default modules of the MagicMirror. The `currentweather` module is one of the default modules of the MagicMirror.
This module displays the current weather, including the windspeed, the sunset or sunrise time, the temperature and an icon to display the current conditions. This module displays the current weather, including the windspeed, the sunset or sunrise time, the temperature and an icon to display the current conditions.

View File

@@ -3,6 +3,8 @@
* *
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*
* This module is deprecated. Any additional feature will no longer be merged.
*/ */
Module.register("currentweather", { Module.register("currentweather", {
// Default module config. // Default module config.
@@ -47,24 +49,24 @@ Module.register("currentweather", {
roundTemp: false, roundTemp: false,
iconTable: { iconTable: {
"01d": "wi-day-sunny", "01d": "day-sunny",
"02d": "wi-day-cloudy", "02d": "day-cloudy",
"03d": "wi-cloudy", "03d": "cloudy",
"04d": "wi-cloudy-windy", "04d": "cloudy-windy",
"09d": "wi-showers", "09d": "showers",
"10d": "wi-rain", "10d": "rain",
"11d": "wi-thunderstorm", "11d": "thunderstorm",
"13d": "wi-snow", "13d": "snow",
"50d": "wi-fog", "50d": "fog",
"01n": "wi-night-clear", "01n": "night-clear",
"02n": "wi-night-cloudy", "02n": "night-cloudy",
"03n": "wi-night-cloudy", "03n": "night-cloudy",
"04n": "wi-night-cloudy", "04n": "night-cloudy",
"09n": "wi-night-showers", "09n": "night-showers",
"10n": "wi-night-rain", "10n": "night-rain",
"11n": "wi-night-thunderstorm", "11n": "night-thunderstorm",
"13n": "wi-night-snow", "13n": "night-snow",
"50n": "wi-night-alt-cloudy-windy" "50n": "night-alt-cloudy-windy"
} }
}, },
@@ -219,7 +221,7 @@ Module.register("currentweather", {
if (this.config.hideTemp === false) { if (this.config.hideTemp === false) {
var weatherIcon = document.createElement("span"); var weatherIcon = document.createElement("span");
weatherIcon.className = "wi weathericon " + this.weatherType; weatherIcon.className = "wi weathericon wi-" + this.weatherType;
large.appendChild(weatherIcon); large.appendChild(weatherIcon);
var temperature = document.createElement("span"); var temperature = document.createElement("span");
@@ -258,13 +260,9 @@ Module.register("currentweather", {
var feelsLike = document.createElement("span"); var feelsLike = document.createElement("span");
feelsLike.className = "dimmed"; feelsLike.className = "dimmed";
var feelsLikeHtml = this.translate("FEELS"); feelsLike.innerHTML = this.translate("FEELS", {
if (feelsLikeHtml.indexOf("{DEGREE}") > -1) {
feelsLikeHtml = this.translate("FEELS", {
DEGREE: this.feelsLike + degreeLabel DEGREE: this.feelsLike + degreeLabel
}); });
} else feelsLikeHtml += " " + this.feelsLike + degreeLabel;
feelsLike.innerHTML = feelsLikeHtml;
small.appendChild(feelsLike); small.appendChild(feelsLike);
wrapper.appendChild(small); wrapper.appendChild(small);
@@ -506,6 +504,7 @@ Module.register("currentweather", {
this.loaded = true; this.loaded = true;
this.updateDom(this.config.animationSpeed); this.updateDom(this.config.animationSpeed);
this.sendNotification("CURRENTWEATHER_DATA", { data: data }); this.sendNotification("CURRENTWEATHER_DATA", { data: data });
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.config.iconTable[data.weather[0].icon].replace("-", "_") });
}, },
/* scheduleUpdate() /* scheduleUpdate()
@@ -593,6 +592,7 @@ Module.register("currentweather", {
*/ */
roundValue: function (temperature) { roundValue: function (temperature) {
var decimals = this.config.roundTemp ? 0 : 1; var decimals = this.config.roundTemp ? 0 : 1;
return parseFloat(temperature).toFixed(decimals); var roundValue = parseFloat(temperature).toFixed(decimals);
return roundValue === "-0" ? 0 : roundValue;
} }
}); });

View File

@@ -0,0 +1,9 @@
const NodeHelper = require("node_helper");
const Log = require("logger");
module.exports = NodeHelper.create({
// Override start method.
start: function () {
Log.warn(`The module '${this.name}' is deprecated in favor of the 'weather'-module, please refer to the documentation for a migration path`);
}
});

View File

@@ -0,0 +1,3 @@
<div>
<iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe>
</div>

View File

@@ -0,0 +1,14 @@
iframe.newsfeed-fullarticle {
width: 100vw;
/* very large height value to allow scrolling */
height: 3000px;
top: 0;
left: 0;
border: none;
z-index: 1;
}
.region.bottom.bar.newsfeed-fullarticle {
bottom: inherit;
top: -90px;
}

View File

@@ -44,6 +44,11 @@ Module.register("newsfeed", {
return ["moment.js"]; return ["moment.js"];
}, },
//Define required styles.
getStyles: function () {
return ["newsfeed.css"];
},
// Define required translations. // Define required translations.
getTranslations: function () { getTranslations: function () {
// The translations for the default modules are defined in the core translation files. // The translations for the default modules are defined in the core translation files.
@@ -61,6 +66,7 @@ Module.register("newsfeed", {
this.newsItems = []; this.newsItems = [];
this.loaded = false; this.loaded = false;
this.error = null;
this.activeItem = 0; this.activeItem = 0;
this.scrollPosition = 0; this.scrollPosition = 0;
@@ -75,130 +81,62 @@ Module.register("newsfeed", {
this.generateFeed(payload); this.generateFeed(payload);
if (!this.loaded) { if (!this.loaded) {
if (this.config.hideLoading) {
this.show();
}
this.scheduleUpdateInterval(); this.scheduleUpdateInterval();
} }
this.loaded = true; this.loaded = true;
this.error = null;
} else if (notification === "INCORRECT_URL") {
this.error = `Incorrect url: ${payload.url}`;
this.scheduleUpdateInterval();
} }
}, },
// Override dom generator. //Override fetching of template name
getDom: function () { getTemplate: function () {
const wrapper = document.createElement("div");
if (this.config.feedUrl) { if (this.config.feedUrl) {
wrapper.className = "small bright"; return "oldconfig.njk";
wrapper.innerHTML = this.translate("MODULE_CONFIG_CHANGED", { MODULE_NAME: "Newsfeed" }); } else if (this.config.showFullArticle) {
return wrapper; return "fullarticle.njk";
} }
return "newsfeed.njk";
},
//Override template data and return whats used for the current template
getTemplateData: function () {
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
if (this.config.showFullArticle) {
return {
url: this.getActiveItemURL()
};
}
if (this.error) {
return {
error: this.error
};
}
if (this.newsItems.length === 0) {
return {
loaded: false
};
}
if (this.activeItem >= this.newsItems.length) { if (this.activeItem >= this.newsItems.length) {
this.activeItem = 0; this.activeItem = 0;
} }
if (this.newsItems.length > 0) { const item = this.newsItems[this.activeItem];
// this.config.showFullArticle is a run-time configuration, triggered by optional notifications
if (!this.config.showFullArticle && (this.config.showSourceTitle || this.config.showPublishDate)) {
const sourceAndTimestamp = document.createElement("div");
sourceAndTimestamp.className = "newsfeed-source light small dimmed";
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") { return {
sourceAndTimestamp.innerHTML = this.newsItems[this.activeItem].sourceTitle; loaded: true,
} config: this.config,
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "" && this.config.showPublishDate) { sourceTitle: item.sourceTitle,
sourceAndTimestamp.innerHTML += ", "; publishDate: moment(new Date(item.pubdate)).fromNow(),
} title: item.title,
if (this.config.showPublishDate) { description: item.description
sourceAndTimestamp.innerHTML += moment(new Date(this.newsItems[this.activeItem].pubdate)).fromNow(); };
}
if ((this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") || this.config.showPublishDate) {
sourceAndTimestamp.innerHTML += ":";
}
wrapper.appendChild(sourceAndTimestamp);
}
//Remove selected tags from the beginning of rss feed items (title or description)
if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {
for (let f = 0; f < this.config.startTags.length; f++) {
if (this.newsItems[this.activeItem].title.slice(0, this.config.startTags[f].length) === this.config.startTags[f]) {
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(this.config.startTags[f].length, this.newsItems[this.activeItem].title.length);
}
}
}
if (this.config.removeStartTags === "description" || this.config.removeStartTags === "both") {
if (this.isShowingDescription) {
for (let f = 0; f < this.config.startTags.length; f++) {
if (this.newsItems[this.activeItem].description.slice(0, this.config.startTags[f].length) === this.config.startTags[f]) {
this.newsItems[this.activeItem].description = this.newsItems[this.activeItem].description.slice(this.config.startTags[f].length, this.newsItems[this.activeItem].description.length);
}
}
}
}
//Remove selected tags from the end of rss feed items (title or description)
if (this.config.removeEndTags) {
for (let f = 0; f < this.config.endTags.length; f++) {
if (this.newsItems[this.activeItem].title.slice(-this.config.endTags[f].length) === this.config.endTags[f]) {
this.newsItems[this.activeItem].title = this.newsItems[this.activeItem].title.slice(0, -this.config.endTags[f].length);
}
}
if (this.isShowingDescription) {
for (let f = 0; f < this.config.endTags.length; f++) {
if (this.newsItems[this.activeItem].description.slice(-this.config.endTags[f].length) === this.config.endTags[f]) {
this.newsItems[this.activeItem].description = this.newsItems[this.activeItem].description.slice(0, -this.config.endTags[f].length);
}
}
}
}
if (!this.config.showFullArticle) {
const title = document.createElement("div");
title.className = "newsfeed-title bright medium light" + (!this.config.wrapTitle ? " no-wrap" : "");
title.innerHTML = this.newsItems[this.activeItem].title;
wrapper.appendChild(title);
}
if (this.isShowingDescription) {
const description = document.createElement("div");
description.className = "newsfeed-desc small light" + (!this.config.wrapDescription ? " no-wrap" : "");
const txtDesc = this.newsItems[this.activeItem].description;
description.innerHTML = this.config.truncDescription ? (txtDesc.length > this.config.lengthDescription ? txtDesc.substring(0, this.config.lengthDescription) + "..." : txtDesc) : txtDesc;
wrapper.appendChild(description);
}
if (this.config.showFullArticle) {
const fullArticle = document.createElement("iframe");
fullArticle.className = "";
fullArticle.style.width = "100vw";
// very large height value to allow scrolling
fullArticle.height = "3000";
fullArticle.style.height = "3000";
fullArticle.style.top = "0";
fullArticle.style.left = "0";
fullArticle.style.border = "none";
fullArticle.src = this.getActiveItemURL();
fullArticle.style.zIndex = 1;
wrapper.appendChild(fullArticle);
}
if (this.config.hideLoading) {
this.show();
}
} else {
if (this.config.hideLoading) {
this.hide();
} else {
wrapper.innerHTML = this.translate("LOADING");
wrapper.className = "small dimmed";
}
}
return wrapper;
}, },
getActiveItemURL: function () { getActiveItemURL: function () {
@@ -209,8 +147,7 @@ Module.register("newsfeed", {
* Registers the feeds to be used by the backend. * Registers the feeds to be used by the backend.
*/ */
registerFeeds: function () { registerFeeds: function () {
for (var f in this.config.feeds) { for (let feed of this.config.feeds) {
var feed = this.config.feeds[f];
this.sendSocketNotification("ADD_FEED", { this.sendSocketNotification("ADD_FEED", {
feed: feed, feed: feed,
config: this.config config: this.config
@@ -224,12 +161,11 @@ Module.register("newsfeed", {
* @param {object} feeds An object with feeds returned by the node helper. * @param {object} feeds An object with feeds returned by the node helper.
*/ */
generateFeed: function (feeds) { generateFeed: function (feeds) {
var newsItems = []; let newsItems = [];
for (var feed in feeds) { for (let feed in feeds) {
var feedItems = feeds[feed]; const feedItems = feeds[feed];
if (this.subscribedToFeed(feed)) { if (this.subscribedToFeed(feed)) {
for (var i in feedItems) { for (let item of feedItems) {
var item = feedItems[i];
item.sourceTitle = this.titleForFeed(feed); item.sourceTitle = this.titleForFeed(feed);
if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) { if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) {
newsItems.push(item); newsItems.push(item);
@@ -238,8 +174,8 @@ Module.register("newsfeed", {
} }
} }
newsItems.sort(function (a, b) { newsItems.sort(function (a, b) {
var dateA = new Date(a.pubdate); const dateA = new Date(a.pubdate);
var dateB = new Date(b.pubdate); const dateB = new Date(b.pubdate);
return dateB - dateA; return dateB - dateA;
}); });
if (this.config.maxNewsItems > 0) { if (this.config.maxNewsItems > 0) {
@@ -248,8 +184,8 @@ Module.register("newsfeed", {
if (this.config.prohibitedWords.length > 0) { if (this.config.prohibitedWords.length > 0) {
newsItems = newsItems.filter(function (value) { newsItems = newsItems.filter(function (value) {
for (var i = 0; i < this.config.prohibitedWords.length; i++) { for (let word of this.config.prohibitedWords) {
if (value["title"].toLowerCase().indexOf(this.config.prohibitedWords[i].toLowerCase()) > -1) { if (value["title"].toLowerCase().indexOf(word.toLowerCase()) > -1) {
return false; return false;
} }
} }
@@ -257,8 +193,47 @@ Module.register("newsfeed", {
}, this); }, this);
} }
newsItems.forEach((item) => {
//Remove selected tags from the beginning of rss feed items (title or description)
if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") {
for (let startTag of this.config.startTags) {
if (item.title.slice(0, startTag.length) === startTag) {
item.title = item.title.slice(startTag.length, item.title.length);
}
}
}
if (this.config.removeStartTags === "description" || this.config.removeStartTags === "both") {
if (this.isShowingDescription) {
for (let startTag of this.config.startTags) {
if (item.description.slice(0, startTag.length) === startTag) {
item.description = item.description.slice(startTag.length, item.description.length);
}
}
}
}
//Remove selected tags from the end of rss feed items (title or description)
if (this.config.removeEndTags) {
for (let endTag of this.config.endTags) {
if (item.title.slice(-endTag.length) === endTag) {
item.title = item.title.slice(0, -endTag.length);
}
}
if (this.isShowingDescription) {
for (let endTag of this.config.endTags) {
if (item.description.slice(-endTag.length) === endTag) {
item.description = item.description.slice(0, -endTag.length);
}
}
}
}
});
// get updated news items and broadcast them // get updated news items and broadcast them
var updatedItems = []; const updatedItems = [];
newsItems.forEach((value) => { newsItems.forEach((value) => {
if (this.newsItems.findIndex((value1) => value1 === value) === -1) { if (this.newsItems.findIndex((value1) => value1 === value) === -1) {
// Add item to updated items list // Add item to updated items list
@@ -281,8 +256,7 @@ Module.register("newsfeed", {
* @returns {boolean} True if it is subscribed, false otherwise * @returns {boolean} True if it is subscribed, false otherwise
*/ */
subscribedToFeed: function (feedUrl) { subscribedToFeed: function (feedUrl) {
for (var f in this.config.feeds) { for (let feed of this.config.feeds) {
var feed = this.config.feeds[f];
if (feed.url === feedUrl) { if (feed.url === feedUrl) {
return true; return true;
} }
@@ -297,8 +271,7 @@ Module.register("newsfeed", {
* @returns {string} The title of the feed * @returns {string} The title of the feed
*/ */
titleForFeed: function (feedUrl) { titleForFeed: function (feedUrl) {
for (var f in this.config.feeds) { for (let feed of this.config.feeds) {
var feed = this.config.feeds[f];
if (feed.url === feedUrl) { if (feed.url === feedUrl) {
return feed.title || ""; return feed.title || "";
} }
@@ -310,22 +283,20 @@ Module.register("newsfeed", {
* Schedule visual update. * Schedule visual update.
*/ */
scheduleUpdateInterval: function () { scheduleUpdateInterval: function () {
var self = this; this.updateDom(this.config.animationSpeed);
self.updateDom(self.config.animationSpeed);
// Broadcast NewsFeed if needed // Broadcast NewsFeed if needed
if (self.config.broadcastNewsFeeds) { if (this.config.broadcastNewsFeeds) {
self.sendNotification("NEWS_FEED", { items: self.newsItems }); this.sendNotification("NEWS_FEED", { items: this.newsItems });
} }
this.timer = setInterval(function () { this.timer = setInterval(() => {
self.activeItem++; this.activeItem++;
self.updateDom(self.config.animationSpeed); this.updateDom(this.config.animationSpeed);
// Broadcast NewsFeed if needed // Broadcast NewsFeed if needed
if (self.config.broadcastNewsFeeds) { if (this.config.broadcastNewsFeeds) {
self.sendNotification("NEWS_FEED", { items: self.newsItems }); this.sendNotification("NEWS_FEED", { items: this.newsItems });
} }
}, this.config.updateInterval); }, this.config.updateInterval);
}, },
@@ -335,8 +306,7 @@ Module.register("newsfeed", {
this.config.showFullArticle = false; this.config.showFullArticle = false;
this.scrollPosition = 0; this.scrollPosition = 0;
// reset bottom bar alignment // reset bottom bar alignment
document.getElementsByClassName("region bottom bar")[0].style.bottom = "0"; document.getElementsByClassName("region bottom bar")[0].classList.remove("newsfeed-fullarticle");
document.getElementsByClassName("region bottom bar")[0].style.top = "inherit";
if (!this.timer) { if (!this.timer) {
this.scheduleUpdateInterval(); this.scheduleUpdateInterval();
} }
@@ -344,7 +314,9 @@ Module.register("newsfeed", {
notificationReceived: function (notification, payload, sender) { notificationReceived: function (notification, payload, sender) {
const before = this.activeItem; const before = this.activeItem;
if (notification === "ARTICLE_NEXT") { if (notification === "MODULE_DOM_CREATED" && this.config.hideLoading) {
this.hide();
} else if (notification === "ARTICLE_NEXT") {
this.activeItem++; this.activeItem++;
if (this.activeItem >= this.newsItems.length) { if (this.activeItem >= this.newsItems.length) {
this.activeItem = 0; this.activeItem = 0;
@@ -406,8 +378,7 @@ Module.register("newsfeed", {
this.config.showFullArticle = !this.isShowingDescription; this.config.showFullArticle = !this.isShowingDescription;
// make bottom bar align to top to allow scrolling // make bottom bar align to top to allow scrolling
if (this.config.showFullArticle === true) { if (this.config.showFullArticle === true) {
document.getElementsByClassName("region bottom bar")[0].style.bottom = "inherit"; document.getElementsByClassName("region bottom bar")[0].classList.add("newsfeed-fullarticle");
document.getElementsByClassName("region bottom bar")[0].style.top = "-90px";
} }
clearInterval(this.timer); clearInterval(this.timer);
this.timer = null; this.timer = null;

View File

@@ -0,0 +1,32 @@
{% if loaded %}
<div>
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
<div class="newsfeed-source light small dimmed">
{% if sourceTitle and config.showSourceTitle %}
{{ sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %}
{% endif %}
{% if config.showPublishDate %}
{{ publishDate }}:
{% endif %}
</div>
{% endif %}
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
{{ title }}
</div>
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
{% if config.truncDescription %}
{{ description | truncate(config.lengthDescription) }}
{% else %}
{{ description }}
{% endif %}
</div>
</div>
{% elseif error %}
<div class="small dimmed">
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}
</div>
{% else %}
<div class="small dimmed">
{{ "LOADING" | translate | safe }}
</div>
{% endif %}

View File

@@ -4,9 +4,9 @@
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*/ */
const Log = require("../../../js/logger.js"); const Log = require("logger");
const FeedMe = require("feedme"); const FeedMe = require("feedme");
const request = require("request"); const fetch = require("node-fetch");
const iconv = require("iconv-lite"); const iconv = require("iconv-lite");
/** /**
@@ -19,8 +19,6 @@ const iconv = require("iconv-lite");
* @class * @class
*/ */
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings) { const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
const self = this;
let reloadTimer = null; let reloadTimer = null;
let items = []; let items = [];
@@ -36,14 +34,14 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
/** /**
* Request the new items. * Request the new items.
*/ */
const fetchNews = function () { const fetchNews = () => {
clearTimeout(reloadTimer); clearTimeout(reloadTimer);
reloadTimer = null; reloadTimer = null;
items = []; items = [];
const parser = new FeedMe(); const parser = new FeedMe();
parser.on("item", function (item) { parser.on("item", (item) => {
const title = item.title; const title = item.title;
let description = item.description || item.summary || item.content || ""; let description = item.description || item.summary || item.content || "";
const pubdate = item.pubdate || item.published || item.updated || item["dc:date"]; const pubdate = item.pubdate || item.published || item.updated || item["dc:date"];
@@ -68,33 +66,31 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
} }
}); });
parser.on("end", function () { parser.on("end", () => {
self.broadcastItems(); this.broadcastItems();
scheduleTimer(); scheduleTimer();
}); });
parser.on("error", function (error) { parser.on("error", (error) => {
fetchFailedCallback(self, error); fetchFailedCallback(this, error);
scheduleTimer(); scheduleTimer();
}); });
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]); const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
const opts = { const headers = {
headers: {
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)", "User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)",
"Cache-Control": "max-age=0, no-cache, no-store, must-revalidate", "Cache-Control": "max-age=0, no-cache, no-store, must-revalidate",
Pragma: "no-cache" Pragma: "no-cache"
},
encoding: null
}; };
request(url, opts) fetch(url, { headers: headers })
.on("error", function (error) { .catch((error) => {
fetchFailedCallback(self, error); fetchFailedCallback(this, error);
scheduleTimer(); scheduleTimer();
}) })
.pipe(iconv.decodeStream(encoding)) .then((res) => {
.pipe(parser); res.body.pipe(iconv.decodeStream(encoding)).pipe(parser);
});
}; };
/** /**
@@ -136,7 +132,7 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
return; return;
} }
Log.info("Newsfeed-Fetcher: Broadcasting " + items.length + " items."); Log.info("Newsfeed-Fetcher: Broadcasting " + items.length + " items.");
itemsReceivedCallback(self); itemsReceivedCallback(this);
}; };
this.onReceive = function (callback) { this.onReceive = function (callback) {

View File

@@ -6,9 +6,8 @@
*/ */
const NodeHelper = require("node_helper"); const NodeHelper = require("node_helper");
const validUrl = require("valid-url");
const NewsfeedFetcher = require("./newsfeedfetcher.js"); const NewsfeedFetcher = require("./newsfeedfetcher.js");
const Log = require("../../../js/logger"); const Log = require("logger");
module.exports = NodeHelper.create({ module.exports = NodeHelper.create({
// Override start method. // Override start method.
@@ -36,8 +35,10 @@ module.exports = NodeHelper.create({
const encoding = feed.encoding || "UTF-8"; const encoding = feed.encoding || "UTF-8";
const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000; const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
if (!validUrl.isUri(url)) { try {
this.sendSocketNotification("INCORRECT_URL", url); new URL(url);
} catch (error) {
this.sendSocketNotification("INCORRECT_URL", { url: url });
return; return;
} }
@@ -73,8 +74,8 @@ module.exports = NodeHelper.create({
* and broadcasts these using sendSocketNotification. * and broadcasts these using sendSocketNotification.
*/ */
broadcastFeeds: function () { broadcastFeeds: function () {
var feeds = {}; const feeds = {};
for (var f in this.fetchers) { for (let f in this.fetchers) {
feeds[f] = this.fetchers[f].items(); feeds[f] = this.fetchers[f].items();
} }
this.sendSocketNotification("NEWS_ITEMS", feeds); this.sendSocketNotification("NEWS_ITEMS", feeds);

View File

@@ -0,0 +1,3 @@
<div class="small bright">
{{ "MODULE_CONFIG_CHANGED" | translate({MODULE_NAME: "Newsfeed"}) | safe }}
</div>

View File

@@ -3,7 +3,7 @@ const simpleGits = [];
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const defaultModules = require(__dirname + "/../defaultmodules.js"); const defaultModules = require(__dirname + "/../defaultmodules.js");
const Log = require(__dirname + "/../../../js/logger.js"); const Log = require("logger");
const NodeHelper = require("node_helper"); const NodeHelper = require("node_helper");
module.exports = NodeHelper.create({ module.exports = NodeHelper.create({
@@ -14,32 +14,32 @@ module.exports = NodeHelper.create({
start: function () {}, start: function () {},
configureModules: function (modules) { configureModules: async function (modules) {
// Push MagicMirror itself , biggest chance it'll show up last in UI and isn't overwritten // Push MagicMirror itself , biggest chance it'll show up last in UI and isn't overwritten
// others will be added in front // others will be added in front
// this method returns promises so we can't wait for every one to resolve before continuing // this method returns promises so we can't wait for every one to resolve before continuing
simpleGits.push({ module: "default", git: SimpleGit(path.normalize(__dirname + "/../../../")) }); simpleGits.push({ module: "default", git: this.createGit(path.normalize(__dirname + "/../../../")) });
var promises = []; for (let moduleName in modules) {
for (var moduleName in modules) {
if (!this.ignoreUpdateChecking(moduleName)) { if (!this.ignoreUpdateChecking(moduleName)) {
// Default modules are included in the main MagicMirror repo // Default modules are included in the main MagicMirror repo
var moduleFolder = path.normalize(__dirname + "/../../" + moduleName); let moduleFolder = path.normalize(__dirname + "/../../" + moduleName);
try { try {
Log.info("Checking git for module: " + moduleName); Log.info("Checking git for module: " + moduleName);
let stat = fs.statSync(path.join(moduleFolder, ".git")); // Throws error if file doesn't exist
promises.push(this.resolveRemote(moduleName, moduleFolder)); fs.statSync(path.join(moduleFolder, ".git"));
// Fetch the git or throw error if no remotes
let git = await this.resolveRemote(moduleFolder);
// Folder has .git and has at least one git remote, watch this folder
simpleGits.unshift({ module: moduleName, git: git });
} catch (err) { } catch (err) {
// Error when directory .git doesn't exist // Error when directory .git doesn't exist or doesn't have any remotes
// This module is not managed with git, skip // This module is not managed with git, skip
continue; continue;
} }
} }
} }
return Promise.all(promises);
}, },
socketNotificationReceived: function (notification, payload) { socketNotificationReceived: function (notification, payload) {
@@ -54,36 +54,36 @@ module.exports = NodeHelper.create({
} }
}, },
resolveRemote: function (moduleName, moduleFolder) { resolveRemote: async function (moduleFolder) {
return new Promise((resolve, reject) => { let git = this.createGit(moduleFolder);
var git = SimpleGit(moduleFolder); let remotes = await git.getRemotes(true);
git.getRemotes(true, (err, remotes) => {
if (remotes.length < 1 || remotes[0].name.length < 1) { if (remotes.length < 1 || remotes[0].name.length < 1) {
// No valid remote for folder, skip throw new Error("No valid remote for folder " + moduleFolder);
return resolve();
} }
// Folder has .git and has at least one git remote, watch this folder
simpleGits.unshift({ module: moduleName, git: git }); return git;
resolve();
});
});
}, },
performFetch: function () { performFetch: async function () {
var self = this; for (let sg of simpleGits) {
simpleGits.forEach((sg) => { try {
sg.git.fetch(["--dry-run"]).status((err, data) => { let fetchData = await sg.git.fetch(["--dry-run"]).status();
data.module = sg.module; let logData = await sg.git.log({ "-1": null });
if (!err) {
sg.git.log({ "-1": null }, (err, data2) => { if (logData.latest && "hash" in logData.latest) {
if (!err && data2.latest && "hash" in data2.latest) { this.sendSocketNotification("STATUS", {
data.hash = data2.latest.hash; module: sg.module,
self.sendSocketNotification("STATUS", data); behind: fetchData.behind,
} current: fetchData.current,
hash: logData.latest.hash,
tracking: fetchData.tracking
}); });
} }
}); } catch (err) {
}); Log.error("Failed to fetch git data for " + sg.module + ": " + err);
}
}
this.scheduleNextFetch(this.config.updateInterval); this.scheduleNextFetch(this.config.updateInterval);
}, },
@@ -93,13 +93,17 @@ module.exports = NodeHelper.create({
delay = 60 * 1000; delay = 60 * 1000;
} }
var self = this; let self = this;
clearTimeout(this.updateTimer); clearTimeout(this.updateTimer);
this.updateTimer = setTimeout(function () { this.updateTimer = setTimeout(function () {
self.performFetch(); self.performFetch();
}, delay); }, delay);
}, },
createGit: function (folder) {
return SimpleGit({ baseDir: folder, timeout: { block: this.config.timeout } });
},
ignoreUpdateChecking: function (moduleName) { ignoreUpdateChecking: function (moduleName) {
// Should not check for updates for default modules // Should not check for updates for default modules
if (defaultModules.indexOf(moduleName) >= 0) { if (defaultModules.indexOf(moduleName) >= 0) {

View File

@@ -8,7 +8,8 @@ Module.register("updatenotification", {
defaults: { defaults: {
updateInterval: 10 * 60 * 1000, // every 10 minutes updateInterval: 10 * 60 * 1000, // every 10 minutes
refreshInterval: 24 * 60 * 60 * 1000, // one day refreshInterval: 24 * 60 * 60 * 1000, // one day
ignoreModules: [] ignoreModules: [],
timeout: 1000
}, },
suspended: false, suspended: false,

View File

@@ -1,7 +1,4 @@
{% if current or weatherData %} {% if current %}
{% if weatherData %}
{% set current = weatherData.current %}
{% endif %}
{% if not config.onlyTemp %} {% if not config.onlyTemp %}
<div class="normal medium"> <div class="normal medium">
<span class="wi wi-strong-wind dimmed"></span> <span class="wi wi-strong-wind dimmed"></span>
@@ -66,13 +63,10 @@
{% endif %} {% endif %}
</div> </div>
{% if (config.showFeelsLike or config.showPrecipitationAmount) and not config.onlyTemp %} {% if (config.showFeelsLike or config.showPrecipitationAmount) and not config.onlyTemp %}
<div class="normal medium"> <div class="normal medium feelslike">
{% if config.showFeelsLike %} {% if config.showFeelsLike %}
<span class="dimmed"> <span class="dimmed">
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }} {{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
{% if not config.feelsLikeWithDegree %}
{{ current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }}
{% endif %}
</span> </span>
{% endif %} {% endif %}
{% if config.showPrecipitationAmount %} {% if config.showPrecipitationAmount %}
@@ -84,7 +78,7 @@
{% endif %} {% endif %}
{% else %} {% else %}
<div class="dimmed light small"> <div class="dimmed light small">
{{ "LOADING" | translate | safe }} {{ "LOADING" | translate }}
</div> </div>
{% endif %} {% endif %}

View File

@@ -1,10 +1,5 @@
{% if forecast or weatherData %} {% if forecast %}
{% if weatherData %}
{% set forecast = weatherData.days %}
{% set numSteps = forecast | calcNumEntries %}
{% else %}
{% set numSteps = forecast | calcNumSteps %} {% set numSteps = forecast | calcNumSteps %}
{% endif %}
{% set currentStep = 0 %} {% set currentStep = 0 %}
<table class="{{ config.tableClass }}"> <table class="{{ config.tableClass }}">
{% set forecast = forecast.slice(0, numSteps) %} {% set forecast = forecast.slice(0, numSteps) %}
@@ -35,7 +30,7 @@
</table> </table>
{% else %} {% else %}
<div class="dimmed light small"> <div class="dimmed light small">
{{ "LOADING" | translate | safe }} {{ "LOADING" | translate }}
</div> </div>
{% endif %} {% endif %}

View File

@@ -1,7 +1,4 @@
{% if hourly or weatherData %} {% if hourly %}
{% if weatherData %}
{% set hourly = weatherData.hours %}
{% endif %}
{% set numSteps = hourly | calcNumEntries %} {% set numSteps = hourly | calcNumEntries %}
{% set currentStep = 0 %} {% set currentStep = 0 %}
<table class="{{ config.tableClass }}"> <table class="{{ config.tableClass }}">
@@ -24,9 +21,9 @@
</table> </table>
{% else %} {% else %}
<div class="dimmed light small"> <div class="dimmed light small">
{{ "LOADING" | translate | safe }} {{ "LOADING" | translate }}
</div> </div>
{% endif %} {% endif %}
<!-- Uncomment the line below to see the contents of the `hourly` object. --> <!-- Uncomment the line below to see the contents of the `hourly` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{weatherData | dump}}</div> --> <!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{hourly | dump}}</div> -->

View File

@@ -29,16 +29,23 @@ WeatherProvider.register("yourprovider", {
#### `fetchCurrentWeather()` #### `fetchCurrentWeather()`
This method is called when the weather module tries to fetch the current weather of your provider. The implementation of this method is required. This method is called when the weather module tries to fetch the current weather of your provider. The implementation of this method is required for current weather support.
The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise. The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise.
After the response is processed, the current weather information (as a [WeatherObject](#weatherobject)) needs to be set with `this.setCurrentWeather(currentWeather);`. After the response is processed, the current weather information (as a [WeatherObject](#weatherobject)) needs to be set with `this.setCurrentWeather(currentWeather);`.
It will then automatically refresh the module DOM with the new data. It will then automatically refresh the module DOM with the new data.
#### `fetchWeatherForecast()` #### `fetchWeatherForecast()`
This method is called when the weather module tries to fetch the weather of your provider. The implementation of this method is required. This method is called when the weather module tries to fetch the weather of your provider. The implementation of this method is required for forecast support.
The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise. The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise.
After the response is processed, the weather forecast information (as an array of [WeatherObject](#weatherobject)s) needs to be set with `this.setCurrentWeather(forecast);`. After the response is processed, the weather forecast information (as an array of [WeatherObject](#weatherobject)s) needs to be set with `this.setWeatherForecast(forecast);`.
It will then automatically refresh the module DOM with the new data.
#### `fetchWeatherHourly()`
This method is called when the weather module tries to fetch the weather of your provider. The implementation of this method is required for hourly support.
The implementation can make use of the already implemented function `this.fetchData(url, method, data);`, which is returning a promise.
After the response is processed, the hourly weather forecast information (as an array of [WeatherObject](#weatherobject)s) needs to be set with `this.setWeatherHourly(forecast);`.
It will then automatically refresh the module DOM with the new data. It will then automatically refresh the module DOM with the new data.
### Weather Provider instance methods ### Weather Provider instance methods
@@ -63,6 +70,10 @@ This returns a WeatherDay object for the current weather.
This returns an array of WeatherDay objects for the weather forecast. This returns an array of WeatherDay objects for the weather forecast.
#### `weatherHourly()`
This returns an array of WeatherDay objects for the hourly weather forecast.
#### `fetchedLocation()` #### `fetchedLocation()`
This returns the name of the fetched location or an empty string. This returns the name of the fetched location or an empty string.
@@ -75,6 +86,10 @@ Set the currentWeather and notify the delegate that new information is available
Set the weatherForecastArray and notify the delegate that new information is available. Set the weatherForecastArray and notify the delegate that new information is available.
#### `setWeatherHourly(weatherHourlyArray)`
Set the weatherHourlyArray and notify the delegate that new information is available.
#### `setFetchedLocation(name)` #### `setFetchedLocation(name)`
Set the fetched location name. Set the fetched location name.

View File

@@ -15,6 +15,15 @@ WeatherProvider.register("darksky", {
// Not strictly required, but helps for debugging. // Not strictly required, but helps for debugging.
providerName: "Dark Sky", providerName: "Dark Sky",
// Set the default config properties that is specific to this provider
defaults: {
apiBase: "https://cors-anywhere.herokuapp.com/https://api.darksky.net",
weatherEndpoint: "/forecast",
apiKey: "",
lat: 0,
lon: 0
},
units: { units: {
imperial: "us", imperial: "us",
metric: "si" metric: "si"

View File

@@ -14,6 +14,18 @@ WeatherProvider.register("openweathermap", {
// But for debugging (and future alerts) it would be nice to have the real name. // But for debugging (and future alerts) it would be nice to have the real name.
providerName: "OpenWeatherMap", providerName: "OpenWeatherMap",
// Set the default config properties that is specific to this provider
defaults: {
apiVersion: "2.5",
apiBase: "https://api.openweathermap.org/data/",
weatherEndpoint: "",
locationID: false,
location: false,
lat: 0,
lon: 0,
apiKey: ""
},
// Overwrite the fetchCurrentWeather method. // Overwrite the fetchCurrentWeather method.
fetchCurrentWeather() { fetchCurrentWeather() {
this.fetchData(this.getUrl()) this.fetchData(this.getUrl())
@@ -56,8 +68,8 @@ WeatherProvider.register("openweathermap", {
.finally(() => this.updateAvailable()); .finally(() => this.updateAvailable());
}, },
// Overwrite the fetchWeatherData method. // Overwrite the fetchWeatherHourly method.
fetchWeatherData() { fetchWeatherHourly() {
this.fetchData(this.getUrl()) this.fetchData(this.getUrl())
.then((data) => { .then((data) => {
if (!data) { if (!data) {
@@ -69,7 +81,7 @@ WeatherProvider.register("openweathermap", {
this.setFetchedLocation(`(${data.lat},${data.lon})`); this.setFetchedLocation(`(${data.lat},${data.lon})`);
const weatherData = this.generateWeatherObjectsFromOnecall(data); const weatherData = this.generateWeatherObjectsFromOnecall(data);
this.setWeatherData(weatherData); this.setWeatherHourly(weatherData.hours);
}) })
.catch(function (request) { .catch(function (request) {
Log.error("Could not load data ... ", request); Log.error("Could not load data ... ", request);
@@ -77,6 +89,31 @@ WeatherProvider.register("openweathermap", {
.finally(() => this.updateAvailable()); .finally(() => this.updateAvailable());
}, },
/**
* Overrides method for setting config to check if endpoint is correct for hourly
*
* @param config
*/
setConfig(config) {
this.config = config;
if (!this.config.weatherEndpoint) {
switch (this.config.type) {
case "hourly":
this.config.weatherEndpoint = "/onecall";
break;
case "daily":
case "forecast":
this.config.weatherEndpoint = "/forecast";
break;
case "current":
this.config.weatherEndpoint = "/weather";
break;
default:
Log.error("weatherEndpoint not configured and could not resolve it based on type");
}
}
},
/** OpenWeatherMap Specific Methods - These are not part of the default provider methods */ /** OpenWeatherMap Specific Methods - These are not part of the default provider methods */
/* /*
* Gets the complete url for the request * Gets the complete url for the request
@@ -428,6 +465,8 @@ WeatherProvider.register("openweathermap", {
} else { } else {
params += "&exclude=minutely"; params += "&exclude=minutely";
} }
} else if (this.config.lat && this.config.lon) {
params += "lat=" + this.config.lat + "&lon=" + this.config.lon;
} else if (this.config.locationID) { } else if (this.config.locationID) {
params += "id=" + this.config.locationID; params += "id=" + this.config.locationID;
} else if (this.config.location) { } else if (this.config.location) {

View File

@@ -14,6 +14,13 @@
WeatherProvider.register("smhi", { WeatherProvider.register("smhi", {
providerName: "SMHI", providerName: "SMHI",
// Set the default config properties that is specific to this provider
defaults: {
lat: 0,
lon: 0,
precipitationValue: "pmedian"
},
/** /**
* Implements method in interface for fetching current weather * Implements method in interface for fetching current weather
*/ */
@@ -55,7 +62,7 @@ WeatherProvider.register("smhi", {
this.config = config; this.config = config;
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) == -1) { if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) == -1) {
console.log("invalid or not set: " + config.precipitationValue); console.log("invalid or not set: " + config.precipitationValue);
config.precipitationValue = "pmedian"; config.precipitationValue = this.defaults.precipitationValue;
} }
}, },

View File

@@ -14,6 +14,13 @@ WeatherProvider.register("ukmetoffice", {
// But for debugging (and future alerts) it would be nice to have the real name. // But for debugging (and future alerts) it would be nice to have the real name.
providerName: "UK Met Office", providerName: "UK Met Office",
// Set the default config properties that is specific to this provider
defaults: {
apiBase: "http://datapoint.metoffice.gov.uk/public/data/val/wxfcs/all/json/",
locationID: false,
apiKey: ""
},
units: { units: {
imperial: "us", imperial: "us",
metric: "si" metric: "si"

View File

@@ -44,6 +44,16 @@ WeatherProvider.register("ukmetofficedatahub", {
// Set the name of the provider. // Set the name of the provider.
providerName: "UK Met Office (DataHub)", providerName: "UK Met Office (DataHub)",
// Set the default config properties that is specific to this provider
defaults: {
apiBase: "https://api-metoffice.apiconnect.ibmcloud.com/metoffice/production/v0/forecasts/point/",
apiKey: "",
apiSecret: "",
lat: 0,
lon: 0,
windUnits: "mph"
},
// Build URL with query strings according to DataHub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api) // Build URL with query strings according to DataHub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
getUrl(forecastType) { getUrl(forecastType) {
let queryStrings = "?"; let queryStrings = "?";

View File

@@ -14,6 +14,15 @@ WeatherProvider.register("weatherbit", {
// Not strictly required, but helps for debugging. // Not strictly required, but helps for debugging.
providerName: "Weatherbit", providerName: "Weatherbit",
// Set the default config properties that is specific to this provider
defaults: {
apiBase: "https://api.weatherbit.io/v2.0",
weatherEndpoint: "/current",
apiKey: "",
lat: 0,
lon: 0
},
units: { units: {
imperial: "I", imperial: "I",
metric: "M" metric: "M"

View File

@@ -19,6 +19,14 @@ WeatherProvider.register("weathergov", {
// But for debugging (and future alerts) it would be nice to have the real name. // But for debugging (and future alerts) it would be nice to have the real name.
providerName: "Weather.gov", providerName: "Weather.gov",
// Set the default config properties that is specific to this provider
defaults: {
apiBase: "https://api.weatherbit.io/v2.0",
weatherEndpoint: "/forecast",
lat: 0,
lon: 0
},
// Flag all needed URLs availability // Flag all needed URLs availability
configURLs: false, configURLs: false,

View File

@@ -12,10 +12,6 @@ Module.register("weather", {
weatherProvider: "openweathermap", weatherProvider: "openweathermap",
roundTemp: false, roundTemp: false,
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint) type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
lat: 0,
lon: 0,
location: false,
locationID: false,
units: config.units, units: config.units,
useKmh: false, useKmh: false,
tempUnits: config.units, tempUnits: config.units,
@@ -40,20 +36,13 @@ Module.register("weather", {
fade: true, fade: true,
fadePoint: 0.25, // Start on 1/4th of the list. fadePoint: 0.25, // Start on 1/4th of the list.
initialLoadDelay: 0, // 0 seconds delay initialLoadDelay: 0, // 0 seconds delay
retryDelay: 2500,
apiKey: "",
apiSecret: "",
apiVersion: "2.5",
apiBase: "https://api.openweathermap.org/data/", // TODO: this should not be part of the weather.js file, but should be contained in the openweatherprovider
weatherEndpoint: "/weather",
appendLocationNameToHeader: true, appendLocationNameToHeader: true,
calendarClass: "calendar", calendarClass: "calendar",
tableClass: "small", tableClass: "small",
onlyTemp: false, onlyTemp: false,
showPrecipitationAmount: false, showPrecipitationAmount: false,
colored: false, colored: false,
showFeelsLike: true, showFeelsLike: true
feelsLikeWithDegree: false
}, },
// Module properties. // Module properties.
@@ -89,8 +78,6 @@ Module.register("weather", {
// Let the weather provider know we are starting. // Let the weather provider know we are starting.
this.weatherProvider.start(); this.weatherProvider.start();
this.config.feelsLikeWithDegree = this.translate("FEELS").indexOf("{DEGREE}") > -1;
// Add custom filters // Add custom filters
this.addFilters(); this.addFilters();
@@ -133,8 +120,9 @@ Module.register("weather", {
case "daily": case "daily":
case "forecast": case "forecast":
return `forecast.njk`; return `forecast.njk`;
//Make the invalid values use the "Loading..." from forecast
default: default:
return `${this.config.type.toLowerCase()}.njk`; return `forecast.njk`;
} }
}, },
@@ -144,7 +132,7 @@ Module.register("weather", {
config: this.config, config: this.config,
current: this.weatherProvider.currentWeather(), current: this.weatherProvider.currentWeather(),
forecast: this.weatherProvider.weatherForecast(), forecast: this.weatherProvider.weatherForecast(),
weatherData: this.weatherProvider.weatherData(), hourly: this.weatherProvider.weatherHourly(),
indoor: { indoor: {
humidity: this.indoorHumidity, humidity: this.indoorHumidity,
temperature: this.indoorTemperature temperature: this.indoorTemperature
@@ -157,6 +145,10 @@ Module.register("weather", {
Log.log("New weather information available."); Log.log("New weather information available.");
this.updateDom(0); this.updateDom(0);
this.scheduleUpdate(); this.scheduleUpdate();
if (this.weatherProvider.currentWeather()) {
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.weatherProvider.currentWeather().weatherType.replace("-", "_") });
}
}, },
scheduleUpdate: function (delay = null) { scheduleUpdate: function (delay = null) {
@@ -166,19 +158,27 @@ Module.register("weather", {
} }
setTimeout(() => { setTimeout(() => {
if (this.config.weatherEndpoint === "/onecall") { switch (this.config.type.toLowerCase()) {
this.weatherProvider.fetchWeatherData(); case "current":
} else if (this.config.type === "forecast") {
this.weatherProvider.fetchWeatherForecast();
} else {
this.weatherProvider.fetchCurrentWeather(); this.weatherProvider.fetchCurrentWeather();
break;
case "hourly":
this.weatherProvider.fetchWeatherHourly();
break;
case "daily":
case "forecast":
this.weatherProvider.fetchWeatherForecast();
break;
default:
Log.error(`Invalid type ${this.config.type} configured (must be one of 'current', 'hourly', 'daily' or 'forecast')`);
} }
}, nextLoad); }, nextLoad);
}, },
roundValue: function (temperature) { roundValue: function (temperature) {
var decimals = this.config.roundTemp ? 0 : 1; var decimals = this.config.roundTemp ? 0 : 1;
return parseFloat(temperature).toFixed(decimals); var roundValue = parseFloat(temperature).toFixed(decimals);
return roundValue === "-0" ? 0 : roundValue;
}, },
addFilters() { addFilters() {

View File

@@ -11,12 +11,13 @@
var WeatherProvider = Class.extend({ var WeatherProvider = Class.extend({
// Weather Provider Properties // Weather Provider Properties
providerName: null, providerName: null,
defaults: {},
// The following properties have accessor methods. // The following properties have accessor methods.
// Try to not access them directly. // Try to not access them directly.
currentWeatherObject: null, currentWeatherObject: null,
weatherForecastArray: null, weatherForecastArray: null,
weatherDataObject: null, weatherHourlyArray: null,
fetchedLocationName: null, fetchedLocationName: null,
// The following properties will be set automatically. // The following properties will be set automatically.
@@ -57,10 +58,10 @@ var WeatherProvider = Class.extend({
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherForecast method.`); Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherForecast method.`);
}, },
// This method should start the API request to fetch the weather forecast. // This method should start the API request to fetch the weather hourly.
// This method should definitely be overwritten in the provider. // This method should definitely be overwritten in the provider.
fetchWeatherData: function () { fetchWeatherHourly: function () {
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherData method.`); Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherHourly method.`);
}, },
// This returns a WeatherDay object for the current weather. // This returns a WeatherDay object for the current weather.
@@ -74,8 +75,8 @@ var WeatherProvider = Class.extend({
}, },
// This returns an object containing WeatherDay object(s) depending on the type of call. // This returns an object containing WeatherDay object(s) depending on the type of call.
weatherData: function () { weatherHourly: function () {
return this.weatherDataObject; return this.weatherHourlyArray;
}, },
// This returns the name of the fetched location or an empty string. // This returns the name of the fetched location or an empty string.
@@ -95,9 +96,9 @@ var WeatherProvider = Class.extend({
this.weatherForecastArray = weatherForecastArray; this.weatherForecastArray = weatherForecastArray;
}, },
// Set the weatherDataObject and notify the delegate that new information is available. // Set the weatherHourlyArray and notify the delegate that new information is available.
setWeatherData: function (weatherDataObject) { setWeatherHourly: function (weatherHourlyArray) {
this.weatherDataObject = weatherDataObject; this.weatherHourlyArray = weatherHourlyArray;
}, },
// Set the fetched location name. // Set the fetched location name.
@@ -154,10 +155,11 @@ WeatherProvider.register = function (providerIdentifier, providerDetails) {
WeatherProvider.initialize = function (providerIdentifier, delegate) { WeatherProvider.initialize = function (providerIdentifier, delegate) {
providerIdentifier = providerIdentifier.toLowerCase(); providerIdentifier = providerIdentifier.toLowerCase();
var provider = new WeatherProvider.providers[providerIdentifier](); const provider = new WeatherProvider.providers[providerIdentifier]();
const config = Object.assign({}, provider.defaults, delegate.config);
provider.delegate = delegate; provider.delegate = delegate;
provider.setConfig(delegate.config); provider.setConfig(config);
provider.providerIdentifier = providerIdentifier; provider.providerIdentifier = providerIdentifier;
if (!provider.providerName) { if (!provider.providerName) {

View File

@@ -1,5 +1,7 @@
# Module: Weather Forecast # Module: Weather Forecast
> :warning: **This module is deprecated in favor of the [weather](https://docs.magicmirror.builders/modules/weather.html) module.**
The `weatherforecast` module is one of the default modules of the MagicMirror. The `weatherforecast` module is one of the default modules of the MagicMirror.
This module displays the weather forecast for the coming week, including an an icon to display the current conditions, the minimum temperature and the maximum temperature. This module displays the weather forecast for the coming week, including an an icon to display the current conditions, the minimum temperature and the maximum temperature.

View File

@@ -0,0 +1,9 @@
const NodeHelper = require("node_helper");
const Log = require("logger");
module.exports = NodeHelper.create({
// Override start method.
start: function () {
Log.warn(`The module '${this.name}' is deprecated in favor of the 'weather'-module, please refer to the documentation for a migration path`);
}
});

View File

@@ -3,6 +3,8 @@
* *
* By Michael Teeuw https://michaelteeuw.nl * By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed. * MIT Licensed.
*
* This module is deprecated. Any additional feature will no longer be merged.
*/ */
Module.register("weatherforecast", { Module.register("weatherforecast", {
// Default module config. // Default module config.
@@ -351,6 +353,13 @@ Module.register("weatherforecast", {
this.forecast = []; this.forecast = [];
var lastDay = null; var lastDay = null;
var forecastData = {}; var forecastData = {};
var dayStarts = 8;
var dayEnds = 17;
if (data.city && data.city.sunrise && data.city.sunset) {
dayStarts = new Date(moment.unix(data.city.sunrise).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
dayEnds = new Date(moment.unix(data.city.sunset).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
}
// Handle different structs between forecast16 and onecall endpoints // Handle different structs between forecast16 and onecall endpoints
var forecastList = null; var forecastList = null;
@@ -371,10 +380,10 @@ Module.register("weatherforecast", {
var hour; var hour;
if (forecast.dt_txt) { if (forecast.dt_txt) {
day = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd"); day = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
hour = moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").toDate().getHours(); hour = new Date(moment(forecast.dt_txt).locale("en").format("YYYY-MM-DD HH:mm:ss")).getHours();
} else { } else {
day = moment(forecast.dt, "X").format("ddd"); day = moment(forecast.dt, "X").format("ddd");
hour = moment(forecast.dt, "X").toDate().getHours(); hour = new Date(moment(forecast.dt, "X")).getHours();
} }
if (day !== lastDay) { if (day !== lastDay) {
@@ -400,7 +409,7 @@ Module.register("weatherforecast", {
// Since we don't want an icon from the start of the day (in the middle of the night) // Since we don't want an icon from the start of the day (in the middle of the night)
// we update the icon as long as it's somewhere during the day. // we update the icon as long as it's somewhere during the day.
if (hour >= 8 && hour <= 17) { if (hour > dayStarts && hour < dayEnds) {
forecastData.icon = this.config.iconTable[forecast.weather[0].icon]; forecastData.icon = this.config.iconTable[forecast.weather[0].icon];
} }
} }
@@ -462,7 +471,8 @@ Module.register("weatherforecast", {
*/ */
roundValue: function (temperature) { roundValue: function (temperature) {
var decimals = this.config.roundTemp ? 0 : 1; var decimals = this.config.roundTemp ? 0 : 1;
return parseFloat(temperature).toFixed(decimals); var roundValue = parseFloat(temperature).toFixed(decimals);
return roundValue === "-0" ? 0 : roundValue;
}, },
/* processRain(forecast, allForecasts) /* processRain(forecast, allForecasts)

6909
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,26 @@
{ {
"name": "magicmirror", "name": "magicmirror",
"version": "2.14.0", "version": "2.15.0",
"description": "The open source modular smart mirror platform.", "description": "The open source modular smart mirror platform.",
"main": "js/electron.js", "main": "js/electron.js",
"scripts": { "scripts": {
"start": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js", "start": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js",
"start:dev": "DISPLAY=\"${DISPLAY:=:0}\" ./node_modules/.bin/electron js/electron.js dev",
"server": "node ./serveronly", "server": "node ./serveronly",
"install": "echo \"Installing vendor files ...\n\" && cd vendor && npm install --loglevel=error", "install": "echo \"Installing vendor files ...\n\" && cd vendor && npm install --loglevel=error",
"install-fonts": "echo \"Installing fonts ...\n\" && cd fonts && npm install --loglevel=error", "install-fonts": "echo \"Installing fonts ...\n\" && cd fonts && npm install --loglevel=error",
"postinstall": "npm run install-fonts && echo \"MagicMirror installation finished successfully! \n\"", "postinstall": "npm run install-fonts && echo \"MagicMirror installation finished successfully! \n\"",
"test": "NODE_ENV=test mocha tests --recursive", "test": "NODE_ENV=test mocha tests --recursive",
"test:coverage": "NODE_ENV=test nyc mocha tests --recursive --timeout=3000", "test:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text mocha tests --recursive --timeout=3000",
"test:e2e": "NODE_ENV=test mocha tests/e2e --recursive", "test:e2e": "NODE_ENV=test mocha tests/e2e --recursive",
"test:unit": "NODE_ENV=test mocha tests/unit --recursive", "test:unit": "NODE_ENV=test mocha tests/unit --recursive",
"test:prettier": "prettier --check **/*.{js,css,json,md,yml}", "test:prettier": "prettier --check **/*.{js,css,json,md,yml}",
"test:js": "eslint *.js js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --quiet", "test:js": "eslint js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --quiet",
"test:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json", "test:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json",
"test:calendar": "node ./modules/default/calendar/debug.js", "test:calendar": "node ./modules/default/calendar/debug.js",
"config:check": "node js/check_config.js", "config:check": "node js/check_config.js",
"lint:prettier": "prettier --write **/*.{js,css,json,md,yml}", "lint:prettier": "prettier --write **/*.{js,css,json,md,yml}",
"lint:js": "eslint *.js js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --fix", "lint:js": "eslint js/**/*.js modules/default/**/*.js clientonly/*.js serveronly/*.js translations/*.js vendor/*.js tests/**/*.js config/* --config .eslintrc.json --fix",
"lint:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json --fix" "lint:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json --fix"
}, },
"repository": { "repository": {
@@ -42,53 +43,56 @@
}, },
"homepage": "https://magicmirror.builders", "homepage": "https://magicmirror.builders",
"devDependencies": { "devDependencies": {
"chai": "^4.2.0", "chai": "^4.3.4",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"danger": "^10.5.4", "eslint-config-prettier": "^8.1.0",
"eslint-config-prettier": "^7.0.0", "eslint-plugin-jsdoc": "^32.3.0",
"eslint-plugin-jsdoc": "^30.7.8", "eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-prettier": "^3.2.0",
"express-basic-auth": "^1.2.0", "express-basic-auth": "^1.2.0",
"husky": "^4.3.5", "husky": "^4.3.8",
"jsdom": "^16.4.0", "jsdom": "^16.5.1",
"lodash": "^4.17.20", "lodash": "^4.17.21",
"mocha": "^8.2.1", "mocha": "^8.3.2",
"mocha-each": "^2.0.1", "mocha-each": "^2.0.1",
"mocha-logger": "^1.0.7", "mocha-logger": "^1.0.7",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"pretty-quick": "^3.1.0", "pretty-quick": "^3.1.0",
"spectron": "^10.0.1", "sinon": "^10.0.0",
"stylelint": "^13.8.0", "spectron": "^13.0.0",
"stylelint": "^13.12.0",
"stylelint-config-prettier": "^8.0.2", "stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^20.0.0", "stylelint-config-standard": "^21.0.0",
"stylelint-prettier": "^1.1.2" "stylelint-prettier": "^1.2.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"electron": "^8.5.3" "electron": "^11.3.0"
}, },
"dependencies": { "dependencies": {
"colors": "^1.4.0", "colors": "^1.4.0",
"console-stamp": "^3.0.0-rc4.2", "console-stamp": "^3.0.0-rc4.2",
"eslint": "^7.15.0", "digest-fetch": "^1.1.6",
"eslint": "^7.23.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-ipfilter": "^1.1.2", "express-ipfilter": "^1.1.2",
"feedme": "^2.0.2", "feedme": "^2.0.2",
"helmet": "^4.2.0", "helmet": "^4.4.1",
"ical": "^0.8.0",
"iconv-lite": "^0.6.2", "iconv-lite": "^0.6.2",
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"moment": "^2.29.1", "moment": "^2.29.1",
"node-ical": "^0.12.7", "node-fetch": "^2.6.1",
"request": "^2.88.2", "node-ical": "^0.12.9",
"rrule": "^2.6.6", "rrule": "^2.6.8",
"rrule-alt": "^2.2.8", "rrule-alt": "^2.2.8",
"simple-git": "^2.31.0", "simple-git": "^2.37.0",
"socket.io": "^3.0.4", "socket.io": "^4.0.0"
"valid-url": "^1.0.9"
}, },
"_moduleAliases": { "_moduleAliases": {
"node_helper": "js/node_helper.js" "node_helper": "js/node_helper.js",
"logger": "js/logger.js"
},
"engines": {
"node": ">=10"
}, },
"husky": { "husky": {
"hooks": { "hooks": {

View File

@@ -1,5 +1,5 @@
const app = require("../js/app.js"); const app = require("../js/app.js");
const Log = require("../js/logger.js"); const Log = require("logger");
app.start(function (config) { app.start(function (config) {
var bindAddress = config.address ? config.address : "localhost"; var bindAddress = config.address ? config.address : "localhost";

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },
@@ -25,7 +26,7 @@ var config = {
calendars: [ calendars: [
{ {
maximumNumberOfDays: 10000, maximumNumberOfDays: 10000,
url: "http://localhost:8011/tests/configs/data/calendar_test.ics", url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
auth: { auth: {
user: "MagicMirror", user: "MagicMirror",
pass: "CallMeADog" pass: "CallMeADog"

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },
@@ -25,7 +26,7 @@ var config = {
calendars: [ calendars: [
{ {
maximumNumberOfDays: 10000, maximumNumberOfDays: 10000,
url: "http://localhost:8010/tests/configs/data/calendar_test.ics", url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
auth: { auth: {
user: "MagicMirror", user: "MagicMirror",
pass: "CallMeADog", pass: "CallMeADog",

View File

@@ -0,0 +1,44 @@
/* Magic Mirror Test config default calendar with auth by default
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
calendars: [
{
maximumNumberOfDays: 10000,
url: "http://localhost:8010/tests/configs/data/calendar_test.ics",
auth: {
user: "MagicMirror",
pass: "CallMeADog"
}
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -11,7 +11,8 @@ let config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -15,7 +15,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },
@@ -25,7 +26,7 @@ var config = {
calendars: [ calendars: [
{ {
maximumNumberOfDays: 10000, maximumNumberOfDays: 10000,
url: "http://localhost:8012/tests/configs/data/calendar_test.ics", url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
user: "MagicMirror", user: "MagicMirror",
pass: "CallMeADog" pass: "CallMeADog"
} }

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -11,7 +11,8 @@ let config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -16,7 +16,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -14,7 +14,8 @@ let config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -14,7 +14,8 @@ var config = {
width: 800, width: 800,
height: 600, height: 600,
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -3,8 +3,7 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com * By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed. * MIT Licensed.
*/ */
let config = {
var config = {
port: 8080, port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"], ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
@@ -13,7 +12,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -0,0 +1,38 @@
/* Magic Mirror Test config newsfeed module
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "newsfeed",
position: "bottom_bar",
config: {
feeds: [
{
title: "Incorrect Url",
url: "this is not a valid url"
}
]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -0,0 +1,39 @@
/* Magic Mirror Test config newsfeed module
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "newsfeed",
position: "bottom_bar",
config: {
feeds: [
{
title: "Rodrigo Ramirez Blog",
url: "http://localhost:8080/tests/configs/data/feed_test_rodrigoramirez.xml"
}
],
prohibitedWords: ["QPanel"]
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -15,7 +15,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -0,0 +1,49 @@
/* Magic Mirror Test config current weather compliments
*
* By rejas https://github.com/rejas
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
fullscreen: false,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
},
modules: [
{
module: "compliments",
position: "top_bar",
config: {
compliments: {
snow: ["snow"]
},
updateInterval: 4000
}
},
{
module: "weather",
position: "bottom_bar",
config: {
location: "Munich",
apiKey: "fake key",
initialLoadDelay: 3000
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -14,7 +14,8 @@ let config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -14,7 +14,8 @@ let config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -14,7 +14,8 @@ let config = {
units: "imperial", units: "imperial",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -14,7 +14,8 @@ let config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -14,7 +14,8 @@ let config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
}, },

View File

@@ -13,7 +13,8 @@ var config = {
units: "metric", units: "metric",
electronOptions: { electronOptions: {
webPreferences: { webPreferences: {
nodeIntegration: true nodeIntegration: true,
enableRemoteModule: true
} }
} }
}; };

Some files were not shown because too many files have changed in this diff Show More