mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-08-22 05:07:05 +00:00
5
.github/CONTRIBUTING.md
vendored
5
.github/CONTRIBUTING.md
vendored
@@ -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
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github: MichMich
|
||||||
|
custom: ['https://magicmirror.builders/#donate']
|
6
.github/ISSUE_TEMPLATE.md
vendored
6
.github/ISSUE_TEMPLATE.md
vendored
@@ -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.
|
||||||
|
|
||||||
|
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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.
|
|
||||||
|
24
.github/workflows/codecov-test-suites.yml
vendored
Normal file
24
.github/workflows/codecov-test-suites.yml
vendored
Normal 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
|
4
.github/workflows/enforce-changelog.yml
vendored
4
.github/workflows/enforce-changelog.yml
vendored
@@ -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:
|
||||||
|
5
.github/workflows/node-ci.js.yml
vendored
5
.github/workflows/node-ci.js.yml
vendored
@@ -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 }}
|
||||||
|
82
CHANGELOG.md
82
CHANGELOG.md
@@ -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 …` 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)
|
||||||
|
@@ -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
|
||||||
|
19
README.md
19
README.md
@@ -1,11 +1,13 @@
|
|||||||

|

|
||||||
|
|
||||||
<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>
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -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.");
|
|
||||||
}
|
|
||||||
}
|
|
132
js/app.js
132
js/app.js
@@ -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,43 +46,40 @@ 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
|
||||||
* callback with the found config as argument.
|
* callback with the found config as argument.
|
||||||
*
|
*
|
||||||
* @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();
|
||||||
|
@@ -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) {
|
||||||
|
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
|
||||||
|
} else {
|
||||||
|
Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
|
||||||
|
|
||||||
|
for (const error of errors) {
|
||||||
|
Log.error(`Line ${error.line} column ${error.column}: ${error.message}`);
|
||||||
}
|
}
|
||||||
const messages = linter.verify(data);
|
}
|
||||||
if (messages.length === 0) {
|
|
||||||
Log.info(Utils.colors.pass("Your configuration file doesn't contain syntax errors :)"));
|
|
||||||
} else {
|
|
||||||
Log.error(Utils.colors.error("Your configuration file contains syntax errors :("));
|
|
||||||
// In case the there errors show messages and return
|
|
||||||
messages.forEach((error) => {
|
|
||||||
Log.error("Line", error.line, "col", error.column, error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
checkConfigFile();
|
checkConfigFile();
|
||||||
|
@@ -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>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
|
@@ -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;
|
||||||
});
|
});
|
||||||
|
11
js/loader.js
11
js/loader.js
@@ -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);
|
||||||
|
@@ -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);
|
||||||
},
|
},
|
||||||
|
53
js/module.js
53
js/module.js
@@ -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 (translations) {
|
if (languages.length === 0) {
|
||||||
var translationFile = translations[lang] || undefined;
|
|
||||||
var translationsFallbackFile = translations[first];
|
|
||||||
|
|
||||||
// If a translation file is set, load it and then also load the fallback translation file.
|
|
||||||
// Otherwise only load the fallback translation file.
|
|
||||||
if (translationFile !== undefined && translationFile !== translationsFallbackFile) {
|
|
||||||
Translator.load(self, translationFile, false, function () {
|
|
||||||
Translator.load(self, translationsFallbackFile, true, callback);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Translator.load(self, translationsFallbackFile, true, callback);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
callback();
|
callback();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const translationFile = translations[language];
|
||||||
|
const translationsFallbackFile = translations[fallbackLanguage];
|
||||||
|
|
||||||
|
if (!translationFile) {
|
||||||
|
Translator.load(this, translationsFallbackFile, true, callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Translator.load(this, translationFile, false, () => {
|
||||||
|
if (translationFile !== translationsFallbackFile) {
|
||||||
|
Translator.load(this, translationsFallbackFile, true, callback);
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
|
61
js/server.js
61
js/server.js
@@ -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;
|
||||||
|
@@ -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();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
callback();
|
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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
|
@@ -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 = " ";
|
blankCell.innerHTML = " ";
|
||||||
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)) {
|
||||||
eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
// Respect parameter ColoredSymbolOnly also for custom events
|
||||||
titleWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
|
if (!this.config.coloredSymbolOnly) {
|
||||||
|
eventWrapper.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
|
||||||
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").calendar());
|
if (!this.config.hideTime) {
|
||||||
|
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);
|
||||||
|
@@ -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,492 +33,67 @@ 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;
|
||||||
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
|
let headers = {
|
||||||
},
|
"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();
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((responseData) => {
|
||||||
|
let data = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = ical.parseICS(responseData);
|
||||||
|
Log.debug("parsed data=" + JSON.stringify(data));
|
||||||
|
events = CalendarUtils.filterEvents(data, {
|
||||||
|
excludedEvents,
|
||||||
|
includePastEvents,
|
||||||
|
maximumEntries,
|
||||||
|
maximumNumberOfDays
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
fetchFailedCallback(this, error.message);
|
||||||
|
scheduleTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.broadcastEvents();
|
||||||
scheduleTimer();
|
scheduleTimer();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
data = ical.parseICS(requestData);
|
|
||||||
} catch (error) {
|
|
||||||
fetchFailedCallback(self, error.message);
|
|
||||||
scheduleTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
*
|
|
||||||
* 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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
600
modules/default/calendar/calendarutils.js
Normal file
600
modules/default/calendar/calendarutils.js
Normal 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;
|
||||||
|
}
|
@@ -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()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -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.
|
||||||
|
|
||||||
|
@@ -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) {
|
DEGREE: this.feelsLike + degreeLabel
|
||||||
feelsLikeHtml = this.translate("FEELS", {
|
});
|
||||||
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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
9
modules/default/currentweather/node_helper.js
Normal file
9
modules/default/currentweather/node_helper.js
Normal 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`);
|
||||||
|
}
|
||||||
|
});
|
3
modules/default/newsfeed/fullarticle.njk
Normal file
3
modules/default/newsfeed/fullarticle.njk
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<div>
|
||||||
|
<iframe class="newsfeed-fullarticle" src="{{ url }}"></iframe>
|
||||||
|
</div>
|
14
modules/default/newsfeed/newsfeed.css
Normal file
14
modules/default/newsfeed/newsfeed.css
Normal 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;
|
||||||
|
}
|
@@ -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;
|
||||||
|
32
modules/default/newsfeed/newsfeed.njk
Normal file
32
modules/default/newsfeed/newsfeed.njk
Normal 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 %}
|
@@ -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) {
|
||||||
|
@@ -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);
|
||||||
|
3
modules/default/newsfeed/oldconfig.njk
Normal file
3
modules/default/newsfeed/oldconfig.njk
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="small bright">
|
||||||
|
{{ "MODULE_CONFIG_CHANGED" | translate({MODULE_NAME: "Newsfeed"}) | safe }}
|
||||||
|
</div>
|
@@ -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
|
return git;
|
||||||
simpleGits.unshift({ module: moduleName, git: 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) {
|
||||||
|
@@ -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,
|
||||||
|
@@ -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 %}
|
||||||
|
|
||||||
|
@@ -1,10 +1,5 @@
|
|||||||
{% if forecast or weatherData %}
|
{% if forecast %}
|
||||||
{% if weatherData %}
|
{% set numSteps = forecast | calcNumSteps %}
|
||||||
{% set forecast = weatherData.days %}
|
|
||||||
{% set numSteps = forecast | calcNumEntries %}
|
|
||||||
{% else %}
|
|
||||||
{% 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 %}
|
||||||
|
|
||||||
|
@@ -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> -->
|
||||||
|
@@ -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.
|
||||||
|
@@ -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"
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -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"
|
||||||
|
@@ -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 = "?";
|
||||||
|
@@ -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"
|
||||||
|
@@ -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,
|
||||||
|
|
||||||
|
@@ -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.fetchCurrentWeather();
|
||||||
this.weatherProvider.fetchWeatherForecast();
|
break;
|
||||||
} else {
|
case "hourly":
|
||||||
this.weatherProvider.fetchCurrentWeather();
|
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() {
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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.
|
||||||
|
|
||||||
|
9
modules/default/weatherforecast/node_helper.js
Normal file
9
modules/default/weatherforecast/node_helper.js
Normal 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`);
|
||||||
|
}
|
||||||
|
});
|
@@ -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)
|
||||||
|
6961
package-lock.json
generated
6961
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
60
package.json
60
package.json
@@ -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": {
|
||||||
|
@@ -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";
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -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"
|
||||||
|
@@ -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",
|
||||||
|
44
tests/configs/modules/calendar/changed-port.js
Normal file
44
tests/configs/modules/calendar/changed-port.js
Normal 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;
|
||||||
|
}
|
@@ -11,7 +11,8 @@ let config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -15,7 +15,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -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"
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -11,7 +11,8 @@ let config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -16,7 +16,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -14,7 +14,8 @@ let config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -14,7 +14,8 @@ var config = {
|
|||||||
width: 800,
|
width: 800,
|
||||||
height: 600,
|
height: 600,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
38
tests/configs/modules/newsfeed/incorrect_url.js
Normal file
38
tests/configs/modules/newsfeed/incorrect_url.js
Normal 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;
|
||||||
|
}
|
39
tests/configs/modules/newsfeed/prohibited_words.js
Normal file
39
tests/configs/modules/newsfeed/prohibited_words.js
Normal 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;
|
||||||
|
}
|
@@ -15,7 +15,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
49
tests/configs/modules/weather/currentweather_compliments.js
Normal file
49
tests/configs/modules/weather/currentweather_compliments.js
Normal 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;
|
||||||
|
}
|
@@ -14,7 +14,8 @@ let config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -14,7 +14,8 @@ let config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -14,7 +14,8 @@ let config = {
|
|||||||
units: "imperial",
|
units: "imperial",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -14,7 +14,8 @@ let config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -14,7 +14,8 @@ let config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -13,7 +13,8 @@ var config = {
|
|||||||
units: "metric",
|
units: "metric",
|
||||||
electronOptions: {
|
electronOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -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
Reference in New Issue
Block a user