Compare commits

..

687 Commits

Author SHA1 Message Date
Michael Teeuw
222a5f3779 Merge pull request #2674 from rejas/issue_2671_master
Update electron to fix certificate errors
2021-10-01 18:48:07 +02:00
veeck
72f7106086 Update version 2021-10-01 18:31:29 +02:00
veeck
33387b60cc Update CHANGELOG 2021-10-01 18:30:06 +02:00
veeck
83cc18f648 Update electron to fix certificate errors 2021-10-01 18:27:29 +02:00
Michael Teeuw
5a4fbbf48a Merge pull request #2672 from MichMich/develop
Release 2.17.0
2021-10-01 15:17:49 +02:00
Michael Teeuw
f7465679c0 Prepare 2.17.0 Release 2021-10-01 15:06:26 +02:00
Michael Teeuw
2a5299ebcb Merge pull request #2670 from khassel/new-e2e 2021-10-01 14:41:44 +02:00
Karsten Hassel
b3bddb2c99 fix without_modules.js 2021-09-29 22:01:35 +02:00
Karsten Hassel
997aec8cc2 remove default values from test configs 2021-09-29 21:12:03 +02:00
Karsten Hassel
c67320f185 fix logger.js 2021-09-28 22:08:21 +02:00
Karsten Hassel
8224a6ac35 add custom.css before testing 2021-09-28 21:38:30 +02:00
Karsten Hassel
abcee8aa56 fix global-setup.js 2021-09-28 21:23:59 +02:00
Karsten Hassel
23c6b44921 Merge branch 'develop' into new-e2e
# Conflicts:
#	CHANGELOG.md
2021-09-28 21:12:04 +02:00
Karsten Hassel
1034171e91 add CHANGELOG 2021-09-28 21:10:24 +02:00
Michael Teeuw
bf49f79e6e Merge pull request #2669 from rejas/cleanup 2021-09-28 13:34:08 +02:00
veeck
f7f24dbdfe Update CHANGELOGE 2021-09-28 12:15:07 +02:00
veeck
31ec848aec Update dependencies 2021-09-28 12:14:58 +02:00
veeck
9eb08420b6 Add proper names to github action steps 2021-09-28 12:14:39 +02:00
veeck
eb6d8d4f83 Remove unused file 2021-09-28 12:14:16 +02:00
Michael Teeuw
e10f620cf9 Merge pull request #2664 from khassel/test-setup 2021-09-28 11:51:19 +02:00
Karsten Hassel
f750436b64 update module clock_es 2021-09-26 22:09:41 +02:00
Karsten Hassel
a4a8504558 update module clock 2021-09-26 22:02:29 +02:00
Karsten Hassel
385e5aabaa prettier 2021-09-26 00:29:03 +02:00
Karsten Hassel
d831315e20 update module newsfeed 2021-09-26 00:18:22 +02:00
Karsten Hassel
e0906f3462 update module helloworld 2021-09-25 23:52:38 +02:00
Karsten Hassel
6595b6a44f refactor global-setup.js 2021-09-25 23:45:34 +02:00
Karsten Hassel
0183d7a080 update modules alert and compliments 2021-09-25 23:37:37 +02:00
Karsten Hassel
89e803ee42 update without_modules.js and env_spec.js 2021-09-25 22:12:53 +02:00
Karsten Hassel
c0ce52abe3 change getDocument, delay needed, now 2 tests moved 2021-09-25 00:01:41 +02:00
Karsten Hassel
a1c7f20990 fix logger.js, move jsdom in startApplication 2021-09-24 21:30:51 +02:00
Karsten Hassel
0ef6f89d44 fix server.close() issue 2021-09-24 00:30:00 +02:00
Karsten Hassel
54b04962a8 snapshot 2021-09-23 22:52:32 +02:00
Karsten Hassel
60f8de282d snapshot 2021-09-23 00:05:30 +02:00
karsten13
b4350278a0 first tests 2021-09-21 23:48:29 +02:00
Karsten Hassel
c3c5307624 fix basic-auth.js 2021-09-17 20:03:57 +02:00
Karsten Hassel
879d585f2e update dependencies, add CHANGELOG 2021-09-16 23:16:38 +02:00
Karsten Hassel
9969fede35 refactor e2e 2021-09-16 23:02:17 +02:00
Karsten Hassel
c15b31b374 close server 2021-09-15 21:23:43 +02:00
Karsten Hassel
a3a6c33b32 move translations_spec to e2e 2021-09-13 23:57:18 +02:00
Karsten Hassel
236bf6e0fc silence logger for tests, use modulePaths for e2e 2021-09-13 23:55:41 +02:00
Karsten Hassel
974de179e0 refactor tests in 3 categories unit, e2e and electron 2021-09-13 23:07:34 +02:00
Michael Teeuw
60e03777f3 Merge pull request #2662 from rejas/prettier 2021-09-11 16:10:40 +02:00
Michael Teeuw
05f6b2510f Merge pull request #2661 from khassel/mock_git 2021-09-11 16:09:04 +02:00
rejas
f3274977f5 Ignore config dir 2021-09-11 11:55:51 +02:00
rejas
cf7fb1a3b9 Update CHANGELOG 2021-09-11 11:20:00 +02:00
rejas
12457a87d4 Test on latest version of node 16 again 2021-09-11 11:17:12 +02:00
rejas
9a8de7db80 Run prettier again 2021-09-11 11:16:09 +02:00
rejas
68e02a528e Dont ignore everything when running prettier 2021-09-11 11:15:33 +02:00
Karsten Hassel
277055f44e added tests for updatenotification 2021-09-10 21:12:27 +02:00
Michael Teeuw
c3fc745c7e Merge pull request #2655 from khassel/logger
change use of logger.js in jest tests
2021-09-10 09:01:10 +02:00
Karsten Hassel
8901ed219d update dependencies 2021-09-09 23:56:30 +02:00
Karsten Hassel
d7c70dc021 fix logger.js 2021-09-09 23:30:36 +02:00
Karsten Hassel
53c789bff9 again logger.js due to problem with e2e test 2021-09-09 21:54:20 +02:00
Karsten Hassel
eb63745664 update tests in updatenotification_spec.js due to problems after merging PR's 2021-09-09 21:29:28 +02:00
Karsten Hassel
91d72e48ad Merge branch 'develop' into logger 2021-09-09 21:18:28 +02:00
Karsten Hassel
1dcda63192 add logger.js to jests moduleNameMapper 2021-09-09 21:17:16 +02:00
Karsten Hassel
3ea6544f77 remove logger special 2021-09-09 21:12:55 +02:00
Karsten Hassel
d12a587f11 Merge branch 'updatenotification' into logger 2021-09-09 20:51:58 +02:00
Karsten Hassel
2b147bb98b do logger mocking in logger.js, remove sandbox stuff from unit tests 2021-09-09 20:50:35 +02:00
Michael Teeuw
6529eaaf9a Merge pull request #2650 from rejas/weather 2021-09-09 20:30:37 +02:00
Michael Teeuw
a68aa148b8 Merge pull request #2651 from khassel/updatenotification 2021-09-09 20:30:15 +02:00
Karsten Hassel
98942d6f9c rename to GitHelper 2021-09-09 19:51:07 +02:00
Karsten Hassel
690efc0aff fix test 2021-09-09 00:48:39 +02:00
Karsten Hassel
627cfa1dff hold node v16 at v16.8 due to errors in e2e tests with v16.9 2021-09-09 00:31:24 +02:00
Karsten Hassel
99aca932db update dependencies 2021-09-09 00:24:13 +02:00
Karsten Hassel
dd43f35bbe add unit tests 2021-09-09 00:03:28 +02:00
Karsten Hassel
093988e136 extract git stuff in own class 2021-09-06 23:55:32 +02:00
rejas
087a472765 Fix tomezone test error as reported by @khassel 2021-09-06 21:15:23 +02:00
Karsten Hassel
ce13d7f98b added comments, get hash only for mm 2021-09-05 23:39:23 +02:00
Karsten Hassel
b1fc766908 update dependencies 2021-09-04 23:17:54 +02:00
Karsten Hassel
22384342db fix update notification, remove simple-git 2021-09-04 22:55:03 +02:00
rejas
badce5146a Update CHANGELOG 2021-09-04 22:54:58 +02:00
rejas
0bf3ff9c17 Refer to new docs page for development documentation 2021-09-04 22:49:40 +02:00
Karsten Hassel
860840c367 dirty working snapshot 2021-09-04 19:08:18 +02:00
rejas
221b6325f6 Cleanup some docs in the weather modules 2021-09-04 13:50:21 +02:00
Michael Teeuw
06389e35f9 Merge pull request #2648 from rejas/weather_cleanup
Add common methods to weatherobject
2021-09-03 11:08:43 +02:00
veeck
a7756cec13 Inline some functions 2021-09-02 20:37:41 +02:00
veeck
9ee11654a6 Update jsdocs 2021-09-02 20:35:43 +02:00
veeck
a273266e5e Remove useless returns and now unused jsdoc variables 2021-09-02 20:35:25 +02:00
rejas
e2158716d6 Revert "Update dependencies"
This reverts commit f49312ed13.
2021-09-01 20:50:43 +02:00
rejas
c132206543 Use new method in ukmetofficedatahub provider 2021-09-01 20:02:45 +02:00
rejas
f49312ed13 Update dependencies 2021-09-01 11:02:06 +02:00
rejas
a9f69f07e6 Update CHANGELOG 2021-08-31 23:43:33 +02:00
rejas
d7429a4812 Add eslint rule for === vs == and fix its occurence 2021-08-31 23:39:40 +02:00
rejas
be76d5ce9a Use new method in smhi provider 2021-08-31 23:34:22 +02:00
rejas
f2bc10c5c0 Add tests for new methods 2021-08-31 23:32:05 +02:00
rejas
43eb760bce Use isDayTime method 2021-08-31 22:01:30 +02:00
rejas
a7684e3e9f Add common method for determining if it is daytime 2021-08-31 21:55:43 +02:00
rejas
8949aa3bec Add common method for updating suntimes 2021-08-31 21:41:27 +02:00
Michael Teeuw
e40ddd4b69 Merge pull request #2643 from MMRIZE/develop
Add custom switches for electron mainWindow
2021-08-31 13:12:52 +02:00
Seongnoh Sean Yi
17637fb1f6 Merge branch 'develop' into develop 2021-08-31 10:50:05 +02:00
Michael Teeuw
f71defe958 Merge pull request #2641 from rejas/issue_2462
Actually use showTime parameter in clock module
2021-08-31 09:22:32 +02:00
Michael Teeuw
b8d6a6da1f Merge pull request #2645 from khassel/black_cursor
electron: disable black cursor on start
2021-08-31 09:21:39 +02:00
Karsten Hassel
fbc886b21c run prettier ... 2021-08-30 19:55:26 +02:00
Karsten Hassel
8879fb55de disable black cursor on start 2021-08-30 19:47:26 +02:00
rejas
ed316e8bf3 Try fixing test 2021-08-30 16:38:59 +02:00
eouia
45529f7de9 Add custom switches for electron mainWindow 2021-08-30 11:38:40 +02:00
eouia
dbdff38d2e Add custom switches for electron mainWindow 2021-08-30 11:32:24 +02:00
rejas
21c3179e03 Update dependencies 2021-08-29 20:07:29 +02:00
rejas
c05d93aed8 Update CHANGELOG 2021-08-29 20:05:42 +02:00
rejas
6225abb010 Fix showTime parameter 2021-08-29 20:00:55 +02:00
rejas
c41fff8f5c Add test for showTime parameter 2021-08-29 20:00:42 +02:00
Michael Teeuw
8589d9c482 Merge pull request #2634 from jupadin/fixTimeOffset 2021-08-29 14:03:07 +02:00
Michael Teeuw
7f264953af Merge pull request #2635 from rejas/clock_layout_fix 2021-08-29 14:01:02 +02:00
Julian Dinter
cfff2ad72b Changed comment regarding "ical.js" and "node-ical". 2021-08-25 17:28:37 +02:00
rejas
c0258b352e Update dependencies 2021-08-22 22:45:08 +02:00
rejas
3e1b051ec3 Fix layout of digital clock
ALignment was always set to center. Instead it now takes the positions alignment (left/center/right)
2021-08-22 22:34:11 +02:00
Julian Dinter
b34bb87d7a Added fix to CHANGELOG. 2021-08-22 14:27:18 +02:00
Julian Dinter
83b8cc6729 Ran npm run lint:prettier.. 2021-08-22 14:23:10 +02:00
Julian Dinter
878c0be727 [Fix] start time of calendar event gets corrected by time zone offset. 2021-08-22 14:16:02 +02:00
Julian Dinter
e7f06f5c0c Removed duplicated and thus superfluous debug messages. 2021-08-22 14:06:24 +02:00
Julian Dinter
a1fc38c5fe Prettified and added debug messages. 2021-08-22 13:57:24 +02:00
Michael Teeuw
ff0ab24000 Merge pull request #2631 from apiontek/develop 2021-08-15 23:26:35 +02:00
Michael Teeuw
56a10d192d Merge pull request #2627 from rejas/clock 2021-08-15 23:26:07 +02:00
Adam Piontek
1a8413d8f0 update weathergov provider to try fetching not just current but also forecast when API URLs available 2021-08-10 18:54:34 -04:00
rejas
934b156ebb Update CHANGELOG 2021-08-07 13:33:11 +02:00
rejas
f9639d9705 Refactor clock to allow finer placement of analog clock 2021-08-07 13:30:55 +02:00
Michael Teeuw
4c345c4f33 Merge pull request #2625 from rejas/jsdoc 2021-08-05 19:18:30 +02:00
rejas
490151267a Print warnings about jsdoc during testing too 2021-08-05 16:41:10 +02:00
rejas
3d19a08cc7 Update CHANGELOG 2021-08-05 16:40:49 +02:00
rejas
385c4c32f9 Update dependencies 2021-08-05 16:39:14 +02:00
rejas
3a5052c871 Final jsdoc comments 2021-08-05 16:38:57 +02:00
rejas
f84f590f1d Start filling last gaps of jsdoc 2021-08-03 08:25:57 +02:00
Michael Teeuw
5b9eba7819 Merge pull request #2621 from rejas/issue_2620 2021-08-02 18:55:35 +02:00
rejas
cd18794fca Update jsdocs 2021-08-01 09:53:28 +02:00
rejas
ae3d552ad7 Update dependencies 2021-08-01 09:52:04 +02:00
rejas
be5f71f4a7 Update CHANGELOG 2021-08-01 09:40:44 +02:00
rejas
745a5f0376 Move ignoreToday logic into template to fix undefined forecast 2021-08-01 09:39:07 +02:00
Michael Teeuw
99114b2a61 Merge pull request #2614 from khassel/update_templates 2021-07-21 08:30:40 +02:00
Karsten Hassel
df9bd2b0f9 Merge branch 'develop' into update_templates 2021-07-20 22:33:02 +02:00
Michael Teeuw
e194b559ac Merge pull request #2613 from rejas/cleanup 2021-07-20 21:06:05 +02:00
Karsten Hassel
af5344dccd update github templates 2021-07-14 21:25:12 +02:00
rejas
2d7b8121d7 Update CHANGELOG 2021-07-14 16:03:19 +02:00
rejas
0297450702 Remove eslint rules that now pass 2021-07-14 15:21:03 +02:00
rejas
6b17f6aa28 Final var conversions 2021-07-14 15:06:23 +02:00
rejas
8a7abfe42d Update dependencies 2021-07-14 15:05:43 +02:00
veeck
dd5041395c Run stylelint over all css files 2021-07-14 10:41:29 +02:00
veeck
36d6a5bc15 Start cleaning up some jsdoc 2021-07-14 10:41:29 +02:00
rejas
2619f92d09 More var -> let/const conversions 2021-07-14 10:41:29 +02:00
rejas
53720ae8ae Fix some eslint issues in the tests 2021-07-14 10:41:29 +02:00
veeck
bcff953fbb Fix warning in weather provider 2021-07-14 10:41:29 +02:00
veeck
bcc0cc599d Fix == usages 2021-07-14 10:41:29 +02:00
veeck
a1e3fed312 Disable eslint checks in deprecated weather modules 2021-07-14 10:41:29 +02:00
veeck
399dca2ef9 Make eslint complain about var usage 2021-07-14 10:41:29 +02:00
veeck
2e44e1626d Remove unused variables 2021-07-14 10:41:29 +02:00
veeck
39aa2dfe01 Run linter over all files again 2021-07-14 10:41:29 +02:00
veeck
099929c677 Actually test all js and css files when lint script is run 2021-07-14 10:41:29 +02:00
rejas
af5d132410 Rename global version variable 2021-07-14 10:41:29 +02:00
Michael Teeuw
79acbc3a98 Merge pull request #2611 from khassel/test_config
refactor test configs
2021-07-14 10:40:27 +02:00
Karsten Hassel
e75e4e2284 use process.cwd() for correct path 2021-07-14 00:12:59 +02:00
Karsten Hassel
9aa0af4f9c factory again 2021-07-05 22:07:33 +02:00
Michael Teeuw
50e272efba Fix release date. 2021-07-05 19:54:31 +02:00
karsten13
209e049893 run prettier 2021-07-05 19:51:18 +02:00
Karsten Hassel
bbb3accf0c use factory 2021-07-05 19:45:58 +02:00
Karsten Hassel
179989aa42 add missing comments 2021-07-05 19:21:39 +02:00
Karsten Hassel
2881d19d43 remove env.js 2021-07-05 18:27:14 +02:00
Karsten Hassel
7cfc3458ec fix tests 2021-07-05 18:08:56 +02:00
karsten13
659e1da79d run prettier 2021-07-05 00:40:01 +02:00
Karsten Hassel
a7ae79493d refactor test config 2021-07-05 00:35:28 +02:00
Michael Teeuw
8b484ee707 Merge pull request #2609 from khassel/electron13 2021-07-04 21:39:31 +02:00
Karsten Hassel
d617d4aa09 update package-lock.json 2021-07-04 20:54:54 +02:00
Karsten Hassel
99c04648b4 update CHANGELOG.md 2021-07-04 19:35:36 +02:00
karsten13
f945d50c0d run prettier 2021-07-04 19:21:17 +02:00
Karsten Hassel
e9fabd59ed contextIsolation: false 2021-07-04 18:30:43 +02:00
Karsten Hassel
ad13de3588 update electron to v13 2021-07-04 17:46:22 +02:00
Michael Teeuw
aad8141e27 Prepare v.2.17.0-develop 2021-07-01 14:34:02 +02:00
Michael Teeuw
26a76f80d6 Merge pull request #2607 from MichMich/develop
Release v2.16.0
2021-07-01 14:30:00 +02:00
Michael Teeuw
53ead2087f Prepare v2.16.0 2021-07-01 14:08:10 +02:00
Michael Teeuw
faee811d67 Merge pull request #2605 from rico24/patch-1
Update nl.json
2021-06-29 09:56:35 +02:00
Michael Teeuw
b9c739df1f Fix prettier issues. 2021-06-29 09:49:13 +02:00
rico24
6e124842e8 Merge branch 'develop' into patch-1 2021-06-28 22:38:10 +02:00
rico24
7a5928ea24 Update CHANGELOG.md 2021-06-28 21:03:05 +02:00
Michael Teeuw
7fdf7de11c Merge pull request #2600 from njwilliams/njw/openweathermap-onecall-fix 2021-06-25 11:32:49 +02:00
Michael Teeuw
b75eedb84e Merge pull request #2604 from khassel/update_deps 2021-06-25 11:01:08 +02:00
rico24
3b92ae49a9 Update nl.json
Added the missing variables based on the en.json file.
2021-06-25 10:58:05 +02:00
Nick Williams
775d1091db typo when not using /onecall - was getting forecast from wrong attr 2021-06-24 21:39:32 -04:00
Karsten Hassel
1f77b491fc update dependencies 2021-06-24 23:01:29 +02:00
Michael Teeuw
eff2fd7cc0 Merge pull request #2603 from khassel/dev_console_test 2021-06-24 08:38:49 +02:00
Karsten Hassel
58d2a0d874 Merge branch 'develop' into dev_console_test
# Conflicts:
#	CHANGELOG.md
2021-06-23 23:14:19 +02:00
Michael Teeuw
ea9def997a Merge pull request #2602 from khassel/jest 2021-06-22 08:31:05 +02:00
Karsten Hassel
a222c58047 use --forceExit running jest, define timeouts for github workflows 2021-06-21 22:27:36 +02:00
Michael Teeuw
39a838c2ab Merge pull request #2601 from khassel/jest 2021-06-20 14:45:41 +02:00
Karsten Hassel
cfc0bcd5ad workaround for dev_console test using getWindowCount 2021-06-18 21:39:55 +02:00
Karsten Hassel
3418c9b50f revert changes to dev_console test (has impact on other tests), add CHANGELOG 2021-06-18 12:42:13 +02:00
Nick Williams
e686611890 update One Call API results for openweathermap
The results from the /onecall endpoint were not
being parsed correctly in current and forecast mode - it was
assuming the current/forecast endpoint API, and the return
datasets are different. The effect was that the module
would simply display "Loading..." when in /onecall mode, since
it has no way of displaying error status (ideally, it should,
but leave that for another day)
2021-06-17 19:30:56 -04:00
karsten13
ebb5dee1fc run prettier 2021-06-17 23:19:57 +02:00
Karsten Hassel
d9edaffd9c reset changes on js/logger.js, mock logger.js in global_vars tests, workaround for failing dev_console test 2021-06-17 22:50:26 +02:00
Michael Teeuw
cbe7b1a5b9 Merge pull request #2599 from khassel/jest 2021-06-17 07:06:36 +02:00
Karsten Hassel
e758fd4093 add CHANGELOG 2021-06-16 20:54:03 +02:00
Karsten Hassel
14a99a3b25 Merge branch 'develop' into jest 2021-06-16 20:53:02 +02:00
Karsten Hassel
1ba67506a0 fix logger.js after jest changes 2021-06-16 20:51:32 +02:00
Michael Teeuw
2a6ca5d5ac Merge pull request #2581 from Crazylegstoo/envCanadaFix
Fix to Environment Canada weather provider
2021-06-16 10:07:48 +02:00
Michael Teeuw
aa12e6495a Merge pull request #2593 from khassel/jest
replace mocha with jest
2021-06-16 10:06:32 +02:00
Karsten Hassel
9269848f66 Merge branch 'develop' into jest
# Conflicts:
#	CHANGELOG.md
2021-06-15 23:32:22 +02:00
Karsten Hassel
a71e61cd30 use short if in weatherforecast.js, add comment to logger.js 2021-06-15 23:29:07 +02:00
Michael Teeuw
8be4604c97 Merge pull request #2580 from r3wald/feature/newsfeed-show-as-list
feature: show newsfeed as list
2021-06-15 11:14:15 +02:00
Michael Teeuw
a7bba903f5 Merge branch 'develop' into envCanadaFix 2021-06-15 11:13:28 +02:00
Karsten Hassel
1d8af5835d Merge branch 'develop' into jest
# Conflicts:
#	package-lock.json
#	package.json
2021-06-14 23:21:08 +02:00
Karsten Hassel
189c01fc74 update moduleNameMapper 2021-06-14 22:45:21 +02:00
Karsten Hassel
e3a5bbf661 use expect where missing 2021-06-14 21:07:38 +02:00
Karsten Hassel
ee23c5f72c update clock tests, remove testSequencer 2021-06-14 19:21:21 +02:00
Karsten Hassel
a2083be76b test order 2021-06-14 00:45:40 +02:00
Michael Teeuw
a1c4be83d6 Merge pull request #2578 from rejas/docs 2021-06-13 19:27:24 +02:00
Karsten Hassel
d2fde2bfc8 fix segment faults in node16 on ubuntu 2021-06-12 23:53:04 +02:00
Karsten Hassel
e8956b0b55 update package-lock.json 2021-06-12 22:52:05 +02:00
Karsten Hassel
2af4009a93 change missing translations log, disable console.log in unit tests 2021-06-12 22:26:02 +02:00
Karsten Hassel
a5f7c946cc jest setup changes, increase setMaxListeners, disable console.log in tests/unit/global_vars 2021-06-12 21:26:08 +02:00
Karsten Hassel
298542b531 enable missing translation messages 2021-06-11 23:42:24 +02:00
Karsten Hassel
fca6707a29 add eslint-plugin-jest 2021-06-11 23:11:30 +02:00
Karsten Hassel
10d3a284e9 fix js lint warnings 2021-06-11 23:10:41 +02:00
karsten13
1a244726aa run prettier 2021-06-11 22:24:21 +02:00
Karsten Hassel
044935a164 add jest options 2021-06-11 22:18:05 +02:00
Karsten Hassel
99e5edf2c5 add CHANGELOG.md 2021-06-11 01:00:05 +02:00
Karsten Hassel
b26270bd13 fix helloworld_spec tests 2021-06-11 00:36:53 +02:00
Karsten Hassel
65a8cb9ddb remove unused references 2021-06-11 00:32:54 +02:00
Karsten Hassel
ba4b976e80 test config 2021-06-10 00:24:08 +02:00
veeck
297ae1dbaf Try updating dependencies 2021-06-09 13:09:28 +02:00
veeck
214614f740 Cleanup jsdoc via eslint 2021-06-09 12:49:57 +02:00
Karsten Hassel
0e14d3d6e8 snapshot e2e 2021-06-09 00:19:43 +02:00
Karsten Hassel
67011c0c32 snapshot e2e 2021-06-08 00:47:40 +02:00
Karsten Hassel
16bbb42b8d remove chai from unit tests 2021-06-08 00:47:15 +02:00
Kevin G
b85ac91e6c Corrected bullet for Updated list
Wording/location corrected for update to WEATHER module
2021-06-07 09:28:33 -04:00
Karsten Hassel
66759a33fa unit tests 2021-06-07 00:16:53 +02:00
Kevin G
bf5e83861c Highlight new custom precipitation unit-of-measure for WEATHER
With the new Environment Canada provider in WEATHER, it is now possible for provider to set a customized precipitation unit-of-measure
2021-06-05 11:39:21 -04:00
Robert Ewald
d56a6fb06f disabled by default. 2021-06-03 12:45:08 +02:00
Robert Ewald
5e7aa8e16d changelog updated. 2021-06-03 12:42:39 +02:00
Robert Ewald
bace0ad339 indentation in template fixed. 2021-06-03 12:38:54 +02:00
Robert Ewald
6014eaf8eb Merge remote-tracking branch 'upstream/develop' into feature/newsfeed-show-as-list 2021-06-03 12:27:26 +02:00
Robert Ewald
3e96e8b3f5 newsfeed: showAsList implemented. 2021-06-03 11:40:18 +02:00
Kevin G
95d1b8a6d0 Corrected formating 2021-06-02 16:25:53 -04:00
Kevin G
0ecb66c99e Fixed precipitation unit conversion logic
Found an error that precipitation amount was not being calculated correctly when config.js is asking for Imperial units. This has been fixed.
2021-06-02 16:15:44 -04:00
Kevin G
af52b91799 Fix to precipitation logic
Found a really dumb error I made that broke compatibility with OpenWeatherMap hourly forecast under certain conditions. This is now fixed.
2021-06-02 11:43:56 -04:00
veeck
3c50d6c30a Update CHANGELOG 2021-05-29 16:23:53 +02:00
veeck
2722c72c43 Update issue template 2021-05-29 16:23:41 +02:00
rejas
d5ab3101c6 Update links to config documentation 2021-05-29 16:23:41 +02:00
Michael Teeuw
32df76bdff Merge pull request #2577 from rejas/prettier 2021-05-29 16:22:52 +02:00
rejas
68a06c3d1d Update CHANGELOG 2021-05-29 16:13:24 +02:00
rejas
1b42dc779b Run prettier again 2021-05-29 16:11:39 +02:00
rejas
cedffd40f2 Lint all files, exclude others 2021-05-29 16:11:25 +02:00
Michael Teeuw
cdc8db4837 Merge pull request #2576 from rejas/calendar_fix 2021-05-29 14:02:13 +02:00
Michael Teeuw
a0ee23d84e Merge pull request #2568 from Crazylegstoo/envcanada 2021-05-29 14:01:04 +02:00
Michael Teeuw
49d2d8c9d0 Merge pull request #2574 from daniel-windsor/forecase-ignore-today 2021-05-29 13:59:55 +02:00
rejas
63620aa811 Update CHANGELOG 2021-05-29 13:33:41 +02:00
rejas
d5b11a1dba Update dependencies 2021-05-29 13:32:24 +02:00
rejas
4a63af0490 Set error to null once the events are coming again 2021-05-29 13:22:31 +02:00
Michael Teeuw
a68019293f Merge pull request #2567 from khassel/fix_husky 2021-05-27 07:14:56 +02:00
Karsten Hassel
6b1c91f0dd Merge branch 'develop' into fix_husky
# Conflicts:
#	CHANGELOG.md
2021-05-26 20:20:55 +02:00
Michael Teeuw
ea93785581 Merge pull request #2552 from rejas/error_handling 2021-05-26 20:12:17 +02:00
Karsten Hassel
32819c4fd5 Merge pull request #1 from rejas/fix_pretty_quick_too
Fix call to pretty quick, make husky script executable
2021-05-26 19:49:36 +02:00
Daniel Windsor
fc5a438cdc Add flag that removes today entry in forecast 2021-05-26 21:45:24 +12:00
veeck
57fe94f945 Fix call to pretty quick, make husky script executable 2021-05-26 11:39:29 +02:00
Robert Ewald
aa3a3bdf16 server data for newsfeed list. 2021-05-25 09:29:48 +02:00
Kevin G
db89da3daa Ran prettier 2021-05-21 12:11:58 -04:00
Kevin G
d6ba5796ce ran prettier... 2021-05-21 11:48:56 -04:00
Kevin G
3968743b28 ran prettier... 2021-05-21 11:45:54 -04:00
Kevin G
20c6226b84 Update for new weather provider 2021-05-21 10:20:16 -04:00
Kevin G
463ce394fe ran prettier... 2021-05-21 10:13:39 -04:00
Kevin G
1faefebe42 Formatting correction for min/max display 2021-05-21 10:06:54 -04:00
Karsten Hassel
e2c9339ec4 fix husky setup in prod environment 2021-05-19 20:58:29 +02:00
rejas
4b1c7da171 Update jsdoc 2021-05-19 11:12:56 +02:00
rejas
bdfd6e5e9f Fix calendar test 2021-05-19 11:12:56 +02:00
rejas
4c8508b0a9 Fix newsfeed test 2021-05-19 11:12:56 +02:00
veeck
06b3f92963 Refaktor calendar error once again for better messaging to the user 2021-05-19 11:12:56 +02:00
veeck
d43a57af36 Refaktor newsfeed error once again for better messaging to the user 2021-05-19 11:12:56 +02:00
rejas
aeefe28710 Update jsdoc 2021-05-19 11:12:56 +02:00
rejas
e9de961a23 Cleanup prohibited words filter code and test data 2021-05-19 11:12:56 +02:00
rejas
dcec778e02 Update CHANGELOG 2021-05-19 11:12:56 +02:00
rejas
b212641069 Move checkFetchStatus into NodelHelper 2021-05-19 11:12:42 +02:00
rejas
90aa50bb11 Add fetcher_helper for calendar and newsfeed 2021-05-19 11:12:42 +02:00
rejas
a6879e853b Fix tests 2021-05-19 11:12:42 +02:00
rejas
37fab7ac63 Update error handling for newsfeed and calendar 2021-05-19 11:12:42 +02:00
Michael Teeuw
8b01ae08c5 Merge pull request #2566 from khassel/update_deps 2021-05-19 11:07:43 +02:00
Michael Teeuw
b1cdf42790 Merge pull request #2565 from rejas/css_gap 2021-05-19 11:07:10 +02:00
Karsten Hassel
974968d238 update CHANGELOG.md 2021-05-18 20:44:43 +02:00
karsten13
bf467cbba5 add package-lock.json 2021-05-18 20:42:06 +02:00
Karsten Hassel
536aa2e96e update dependencies and migrate husky to v6 2021-05-18 20:36:56 +02:00
rejas
3d84344b75 Revert "Update dependencies, run linter"
This reverts commit 1054ba3b1e.
2021-05-14 22:06:55 +02:00
rejas
1054ba3b1e Update dependencies, run linter 2021-05-14 21:46:18 +02:00
rejas
aa8ddb9a92 Update CHANGELOG 2021-05-14 21:20:33 +02:00
rejas
fcfe57e5e2 Add more properties for finer body margins 2021-05-14 17:42:23 +02:00
Kevin G
c4fd4e0317 New provider - Environment Canada
Added a new provider that pulls weather data - current, forecast, and hourly - from Environment Canada (Canadian gov weather service). This provider supports Canadian locations only. Documentation will be provided that outlines specific behaviours of this provider and required 'weather' modules parms for Env Canada.
2021-05-13 11:12:30 -04:00
Kevin G
3c76933824 Edit Update
Formatting
2021-05-13 11:00:43 -04:00
Kevin G
fa83819bee Edit update
Formatting
2021-05-13 10:59:45 -04:00
Kevin G
b65ae88879 Edit update
Formatting
2021-05-13 10:58:08 -04:00
Kevin G
96db21f9bf Updates to support Environment Canada provider
Code updates to support a new weatherobject element called precipitationUnits. For the 'forecast' and 'hourly' UI, the weather module will use this new element if it is not null (i.e. a provider has pushed some value into this object element). If the element is null, then default 'units' processing will still occur. This allows a provider to have a customn unit of measure for precipitation - e.g. Env Canada will use mm for rain and cm for snow.
2021-05-13 10:56:30 -04:00
veeck
6d356ff770 Introduce font-size-small property and use it in calendar too (clean it up too) 2021-05-08 18:29:53 +02:00
veeck
21790b32bf Use hex notation instead of verbose name like everywhere else 2021-05-08 18:07:38 +02:00
rejas
159f3d0aa2 Replace deprecated symbol with modern solution 2021-05-08 18:05:30 +02:00
veeck
012a7b0678 Convert one px to rem 2021-05-08 18:04:14 +02:00
veeck
6595c85671 Remove useless(?) margins 2021-05-08 18:04:14 +02:00
veeck
cf5c0464fe Use custom property for gaps between modules 2021-05-08 18:04:14 +02:00
Michael Teeuw
e31450f731 Merge pull request #2560 from foundations-design/contribute 2021-05-08 08:50:00 +02:00
Michael Teeuw
3653984a95 Merge pull request #2562 from rejas/config_check 2021-05-08 08:49:10 +02:00
Michael Teeuw
0c0b856c37 Merge pull request #2561 from khassel/develop 2021-05-08 08:48:43 +02:00
Michael Teeuw
69f1b153ea Merge pull request #2559 from rejas/issue_2547 2021-05-08 08:46:44 +02:00
veeck
43ba4bd00e Fix calendar debug 2021-05-07 12:28:55 +02:00
veeck
bf5edcaac6 Fix failing config check when es6 notation is used 2021-05-07 12:23:29 +02:00
Karsten Hassel
ac51709211 update dependencies in package.json, require node >= v12, remove rrule and rrule-alt 2021-05-04 21:49:16 +02:00
Earl Man
e1a578e819 add myself to CHANGELOG.md 2021-05-04 09:25:36 -05:00
rejas
591c9e53b0 Update CHANGELOG 2021-05-02 15:07:49 +02:00
rejas
87d543eb3a Finish test case 2021-05-02 15:07:02 +02:00
rejas
40c1521591 Add test data 2021-05-02 14:55:45 +02:00
Michael Teeuw
b31c2a6264 Add contributors list.
The full list will be checked during release.
2021-04-29 10:32:20 +02:00
Michael Teeuw
66a42f13f1 Merge pull request #2546 from rejas/move 2021-04-27 16:27:55 +02:00
veeck
8de6ebbbd1 Update dependencies 2021-04-27 13:14:27 +02:00
rejas
649de694ed Update CHANGELOG 2021-04-27 13:14:27 +02:00
veeck
8a52fde8fc Move codecov.yml into github dir to other configs 2021-04-27 13:14:07 +02:00
rejas
fb8bd657de Move weatherforecast mock data in better suited directory 2021-04-27 13:14:07 +02:00
Michael Teeuw
b04a0a6b61 Merge pull request #2556 from khassel/updatenotification 2021-04-27 07:21:06 +02:00
Michael Teeuw
f29c911a0f Merge pull request #2557 from khassel/node16 2021-04-27 07:20:31 +02:00
Karsten Hassel
bd908123c2 replace node v10 with v16 in github workflow 2021-04-26 22:29:23 +02:00
Karsten Hassel
cbdb0b67ab fix updatenotification.js increasing timeout 2021-04-26 22:24:07 +02:00
Michael Teeuw
aa3848f420 Merge pull request #2544 from rejas/es6_conversion 2021-04-18 18:14:25 +02:00
rejas
6cf0748172 Update CHANGELOG 2021-04-18 18:07:26 +02:00
rejas
11122d3f81 Use es6 notation in clientonly 2021-04-18 15:36:00 +02:00
rejas
2dea9398f2 Use es6 notation in updatenotification module 2021-04-18 15:35:41 +02:00
rejas
de93b3294f Use es6 notation in main.js 2021-04-18 15:29:10 +02:00
rejas
7accb84eb9 Use es6 notation in config sample 2021-04-18 15:06:28 +02:00
rejas
ea90ed04d6 Use es6 notation in vendor 2021-04-18 15:06:21 +02:00
rejas
838eed2630 Use es6 notation in weatherprovider 2021-04-18 15:06:06 +02:00
rejas
376b65c749 Use es6 notation in serveronly 2021-04-18 14:52:04 +02:00
rejas
3b4432cb00 Use es6 notation in defaults 2021-04-18 14:51:57 +02:00
rejas
7bc71029de Use es6 notation in socketclient 2021-04-18 14:51:50 +02:00
rejas
d736dd92be Use es6 notation in tests 2021-04-18 14:51:28 +02:00
rejas
6eba8d681c Use es6 notation in module 2021-04-17 16:29:38 +02:00
rejas
5fe654c19d Use es6 notation in loader 2021-04-17 16:29:38 +02:00
rejas
dd366f35a8 Use es6 notation in test configs 2021-04-17 16:29:38 +02:00
rejas
2ababa521d Use es6 notation in weather module and ukmet provider 2021-04-17 16:29:38 +02:00
rejas
bda8f26511 Use es6 notation in compliments module and cleanup jsdoc 2021-04-17 16:29:38 +02:00
rejas
7f1a3df25b Use es6 notation in font tests 2021-04-17 16:29:38 +02:00
rejas
ef2ff50089 Use es6 notation in clock module 2021-04-17 16:29:38 +02:00
rejas
0b3964c827 Use es6 notation in helloworld tests 2021-04-17 16:29:38 +02:00
Michael Teeuw
ccf5bb9342 Merge pull request #2540 from rejas/patch-1 2021-04-17 15:52:38 +02:00
Michael Teeuw
4303882c6a Merge pull request #2542 from rejas/codecov 2021-04-17 15:50:49 +02:00
Michael Teeuw
c34028d549 Merge pull request #2541 from rejas/css 2021-04-17 15:50:18 +02:00
rejas
8e76cdcb57 Revert "Remove some tests to see if it still errors"
This reverts commit 0abebc1e32.
2021-04-17 13:26:16 +02:00
rejas
5eb66106b9 Rename yaml file 2021-04-17 13:17:24 +02:00
rejas
0abebc1e32 Remove some tests to see if it still errors 2021-04-17 13:05:06 +02:00
rejas
552e82f44d Update CHANGELOG 2021-04-17 12:35:46 +02:00
rejas
ada40e36db Add codecov yaml to set informational mode 2021-04-17 12:35:10 +02:00
rejas
480f734a06 Update CHANGELOG, undo accidental commit in clock.js 2021-04-17 12:20:25 +02:00
rejas
20bee9c334 Update dependencies 2021-04-17 11:19:39 +02:00
rejas
de8267f41e Fix css comment style 2021-04-17 10:27:36 +02:00
rejas
7bfaf07980 Use custom properties colors more often 2021-04-17 09:50:12 +02:00
rejas
a42fa8e9f9 Adjust default css to match styles from befor custom properties merge 2021-04-17 09:38:33 +02:00
Veeck
5facad683a Add link to subforum to README
As propose din #2539
2021-04-17 06:46:30 +02:00
Michael Teeuw
fd1913a72e Merge pull request #2538 from codac/master 2021-04-15 16:16:51 +02:00
Michael Teeuw
54c98b4250 Merge pull request #2537 from rejas/custom-properties 2021-04-15 16:15:27 +02:00
config
5d99baac21 Fixing fetch option httpsAgent to agent in calendar module (#466) 2021-04-15 14:51:33 +02:00
veeck
cb95bdf6d7 Undo script changes, Update CHANGELOG 2021-04-14 17:03:09 +02:00
veeck
25e803abfc Update custom.css.sample 2021-04-14 16:56:29 +02:00
veeck
7c6073e4ef Undo README changes 2021-04-14 16:45:38 +02:00
veeck
09bcbe8dfc Update dependencies and lock files 2021-04-14 16:45:07 +02:00
veeck
e0a9c7d0bb Merge branch 'develop' into contribute 2021-04-14 16:42:54 +02:00
Michael Teeuw
ac27d05fd5 Merge pull request #2532 from ezeholz/develop
Alert Module not recognizing multiple alerts
2021-04-14 16:17:45 +02:00
Ezequiel Holzweissig
d6ab56252f Merge branch 'develop' into develop 2021-04-13 20:56:32 -03:00
Michael Teeuw
dab178ed50 Merge pull request #2536 from B1gG/develop
Develop
2021-04-13 14:38:17 +02:00
B1gG
6ed50b6a75 updating changelog 2021-04-13 07:39:14 +01:00
B1gG
ee559ec650 to use dateFormat when timeFormat is relative 2021-04-13 07:29:11 +01:00
Michael Teeuw
4310238418 Merge pull request #2527 from rejas/patch-1 2021-04-12 21:03:27 +02:00
veeck
10c47a6c38 Really run prettier 2021-04-12 19:33:23 +02:00
veeck
4b8043086e Merge branch 'develop' into patch-1 2021-04-12 19:28:51 +02:00
Michael Teeuw
fd952b88bf Merge pull request #2535 from rejas/issue_2530 2021-04-12 19:23:07 +02:00
Ezequiel Holzweissig
e262d463c5 Merge branch 'develop' into develop 2021-04-12 14:20:33 -03:00
veeck
b02bce2510 Merge branch 'develop' into issue_2530 2021-04-12 19:11:24 +02:00
Michael Teeuw
58094531c0 Merge pull request #2534 from KristjanESPERANTO/develop 2021-04-12 19:08:36 +02:00
Michael Teeuw
d937e3ca7c Merge pull request #2533 from rejas/alert_test 2021-04-12 19:07:55 +02:00
Kristjan SCHMIDT
9c58413209 Put decimalSymbol at the end of the row like in the Current part 2021-04-12 18:34:33 +02:00
rejas
db65ff12d7 Update CHANGELOG 2021-04-11 22:49:26 +02:00
rejas
f93b819ea6 Fix missing await calls in tests 2021-04-11 22:42:21 +02:00
rejas
e6fea297e5 Add test for decimalSymbol in weatherforecast 2021-04-11 22:25:17 +02:00
Kristjan SCHMIDT
5c2a0e5634 Fix decimalSymbol for forcast 2021-04-11 22:25:15 +02:00
rejas
71aa21a82d Update dependencies 2021-04-11 21:07:26 +02:00
rejas
23821360c7 Update CHANGELOG 2021-04-11 21:03:35 +02:00
rejas
79f5b938f5 Add first test for alert module 2021-04-11 20:59:53 +02:00
Michael Teeuw
f8769fcc2a Merge pull request #2531 from KristjanESPERANTO/patch-1 2021-04-11 12:45:23 +02:00
Ezequiel Holzweissig
a3ed24c766 Prettifier 2021-04-10 20:38:33 -03:00
Ezequiel Holzweissig
82727b825c Fix alerts
Fixes #2522
2021-04-10 20:15:32 -03:00
Kristjan Esperanto
16e98496af Remove duplicate codecov badge
The second badge doesn't make sense, does it?
2021-04-10 23:55:11 +02:00
rejas
62896ce1a3 Merge branch 'develop' into patch-1 2021-04-10 20:37:47 +02:00
Michael Teeuw
7ea5b1ecbf Merge pull request #2526 from FrancoisRmn/add-some-translations 2021-04-10 20:36:11 +02:00
rejas
eecc95f8fb Run linter 2021-04-10 20:34:27 +02:00
FrancoisRmn
85808d85c4 add some translations 2021-04-10 17:56:47 +02:00
Gerardo Gonzalez
12cc670642 Merge branch 'develop' into patch-1 2021-04-10 14:04:53 +01:00
Michael Teeuw
13fcb55df6 Merge pull request #2518 from oemel09/hide-newsfeed-description 2021-04-10 14:56:49 +02:00
oemel09
a4dfd15888 Fixes tests for newsfeed description 2021-04-10 09:13:14 +02:00
Gerardo Gonzalez
7fbd326298 Update CHANGELOG.md
updating the changelog for the fix Fix wrong treatment of `appendLocationNameToHeader` when using `ukmetofficedatahub`
2021-04-10 01:30:12 +01:00
Gerardo Gonzalez
331d147d50 Update ukmetofficedatahub.js
There is a wrong treatment to appendLocationNameToHeader.
The location should be always returned and leave the weater.js to decide if is included or not in the header.
2021-04-10 01:20:52 +01:00
oemel09
acdcdc55bc Adjusts parenthesis in template to be consistent 2021-04-09 08:07:37 +02:00
oemel09
fdb0c0acb3 Adds test to check that newsfeed description does not show up 2021-04-09 08:05:19 +02:00
Earl Man
564bf47fb2 update sample css 2021-04-08 14:56:01 -05:00
Earl Man
eca35b2371 change --base to --font-size 2021-04-08 14:51:07 -05:00
Earl Man
f97d2e2644 revert 'dimming' method 2021-04-08 14:48:23 -05:00
Earl Man
fdd6659139 delete useless file 2021-04-08 14:39:21 -05:00
Earl Man
0d698fb659 Merge remote-tracking branch 'upstream/develop' into contribute 2021-04-08 14:35:39 -05:00
Earl Man
c68b39dda8 Merge branch 'css-properties' 2021-04-08 14:24:39 -05:00
oemel09
cb67286bc3 Adds test to validate that description is shown 2021-04-08 17:00:15 +02:00
oemel09
a49962b8de Moves validation into template 2021-04-08 17:00:15 +02:00
oemel09
256d5ae14f Rebases onto develop 2021-04-08 16:59:46 +02:00
oemel09
799ee8bcfa Check showDescription setting for newsfeed module 2021-04-08 16:59:03 +02:00
Michael Teeuw
fe8a317ef9 Merge pull request #2514 from jupadin/log-consistency
Log consistency
2021-04-08 11:04:11 +02:00
Michael Teeuw
b6d6ee45e0 Fix linter issue. 2021-04-08 10:40:31 +02:00
Michael Teeuw
0151466a28 Merge branch 'develop' into log-consistency 2021-04-08 10:37:18 +02:00
earlman
3588875e28 Update .gitignore 2021-04-04 13:52:18 -05:00
earlman
e8031aec39 add original main.css bak 2021-04-04 13:34:52 -05:00
earlman
1cabe107e6 run prettier 2021-04-04 13:28:56 -05:00
earlman
ba0c59744b update CHANGELOG.md 2021-04-04 13:28:34 -05:00
earlman
50c702c00a adjust new opacities 2021-04-04 13:24:59 -05:00
earlman
ff8c2fe227 add custom.css.sample 2021-04-04 13:22:33 -05:00
earlman
f9c78a5263 use variables for fonts 2021-04-04 13:18:33 -05:00
earlman
d46784a4d5 base font sizes on rem and var(--base) 2021-04-04 13:05:41 -05:00
earlman
ff43d082f2 base line-height on scale 2021-04-04 12:53:35 -05:00
earlman
83801736d7 add color variables 2021-04-04 12:38:17 -05:00
earlman
e16986cf71 Merge branch 'develop' of https://github.com/MichMich/MagicMirror into contribute 2021-04-04 11:55:03 -05:00
earlman
7ad12d954c add original main.css bak 2021-04-04 11:51:04 -05:00
Julian Dinter
fb74fadec2 Run 'npm run lint:prettier'. 2021-04-04 15:37:47 +02:00
Julian Dinter
c00bdf910e Clean up code by adding comments. 2021-04-04 15:22:12 +02:00
Julian Dinter
50cc1c56e5 Consistent log text. 2021-04-04 15:17:42 +02:00
Julian Dinter
0006099758 Changed log type of starting function of updatenotification module. 2021-04-04 14:52:24 +02:00
earlman
162dcec331 Merge branch 'master' of https://github.com/foundations-design/WhiteLight into develop 2021-04-03 11:15:33 -05:00
earlman
3e8bd022e2 add custom variables 2021-04-02 11:28:22 -05:00
earlman
01e5936671 install valid-url 2021-04-02 10:11:38 -05:00
Michael Teeuw
d85e1c70d6 Merge pull request #2512 from jupadin/log-consistency 2021-04-02 12:56:28 +02:00
Julian Dinter
f329770194 Added fix in CHANGELOG. 2021-04-02 12:42:31 +02:00
Julian Dinter
08194925cb Changed log type of starting function of calendar module. 2021-04-02 12:26:34 +02:00
earlman
2b7aa3e810 use yarn 2021-04-01 15:59:48 -05:00
earlman
549417f106 yarn 2021-04-01 15:00:12 -05:00
earlman
f0f370cc7d add *.env to .gitignore 2021-04-01 15:00:06 -05:00
earlman
b842241f8c fix broken start scripts & yarn 2021-04-01 14:08:48 -05:00
earlman
93aa4b7440 update README.md 2021-04-01 14:03:38 -05:00
Michael Teeuw
9e2eef7818 Merge pull request #2511 from khassel/update-node-ical 2021-04-01 20:12:12 +02:00
Karsten Hassel
b29b65851e Bump node-ical to v0.13.0 (now last runtime dependency using deprecated request package is removed). 2021-04-01 20:02:08 +02:00
Michael Teeuw
a6829bff4f Prepare develop: v2.16.0-develop 2021-04-01 14:36:50 +02:00
Michael Teeuw
491f5aa776 Merge pull request #2510 from MichMich/develop
Release v2.15.0
2021-04-01 14:23:02 +02:00
Michael Teeuw
d401e59031 Prepare for release: v2.15.0 2021-04-01 14:09:08 +02:00
Michael Teeuw
63ce69ce82 Merge pull request #2506 from rejas/fix_self 2021-03-28 11:28:13 +02:00
veeck
f15972c823 Update CHANGELOG
just to please the action and my inner OCD ;-)
2021-03-28 10:50:26 +02:00
veeck
b210fcdcf3 Update dependencies 2021-03-28 10:48:21 +02:00
veeck
8ea37a5a45 Fix left over self reference 2021-03-28 10:48:12 +02:00
Michael Teeuw
daf98c36fc Merge pull request #2504 from rejas/translation 2021-03-26 07:08:50 +01:00
rejas
6a0e6eb84e Fix styling issue 2021-03-25 17:35:36 +01:00
rejas
7bcdd2a42c Update CHANGELOG 2021-03-25 17:30:27 +01:00
rejas
670a760404 Use some es6 notation 2021-03-25 17:29:27 +01:00
veeck
a99de1ad13 Sort translation entries consistently 2021-03-25 16:43:27 +01:00
Michael Teeuw
108fe2082d Merge pull request #2503 from sdetweil/fixnews 2021-03-25 08:31:50 +01:00
Sam Detweiler
e67dc38890 fix newsreader template 2021-03-24 15:06:12 -05:00
sam detweiler
20a87656ff Merge pull request #4 from MichMich/develop
sync Develop
2021-03-24 15:03:29 -05:00
Michael Teeuw
70c3b68e67 Add FUNDING.yml 2021-03-24 16:20:36 +01:00
Michael Teeuw
bfcb7dc601 Merge pull request #2500 from sdetweil/daylight_fullday
fix fullday recurring event where start date is before 2007
2021-03-23 15:56:53 +01:00
Michael Teeuw
39e16221fb Merge pull request #2498 from rejas/md
Update markdown files for github
2021-03-23 15:56:34 +01:00
Sam Detweiler
d11e4e5c92 refix #2483 , recurring FULL Day event, start date before 2007 2021-03-23 08:44:14 -05:00
sam detweiler
cccaa1f33d Merge pull request #3 from MichMich/develop
sync Develop
2021-03-23 08:39:03 -05:00
rejas
37d3aa25b1 Update CHANGELOG 2021-03-23 09:55:35 +01:00
rejas
ebfe96d31d Update CONTRIBUTING guidelines 2021-03-23 09:47:55 +01:00
rejas
e902b8c52f Update PR template 2021-03-23 08:15:33 +01:00
veeck
201b36d62c Update README 2021-03-23 07:55:06 +01:00
Michael Teeuw
7b29070516 Merge pull request #2495 from qu1que/develop 2021-03-22 06:50:04 +01:00
qu1que
9ed2e4d557 Update translations.js
added galician language
2021-03-21 22:59:13 +01:00
qu1que
38544f2368 Update CHANGELOG.md
Added galician language
2021-03-21 22:58:09 +01:00
qu1que
df39408411 Create gl.json
gl.json for Galician language
2021-03-21 22:57:23 +01:00
Michael Teeuw
2e01f988f5 Merge pull request #2489 from sdetweil/fixday2 2021-03-19 16:54:44 +01:00
Sam Detweiler
2b940c9cfb fix formatting 2021-03-19 10:42:14 -05:00
Sam Detweiler
b6f4737ecc fix 2488, west of UTC day shift 2021-03-19 10:32:04 -05:00
Michael Teeuw
89d6473052 Merge pull request #2484 from rejas/newsfeed_cleanup 2021-03-18 22:49:20 +01:00
rejas
99d838f9cd Remove console log 2021-03-18 16:55:54 +01:00
rejas
f3bdddfaa9 Final es6 notation stuff 2021-03-18 16:55:54 +01:00
rejas
87f952c87b Update CHANGELOG 2021-03-18 16:55:54 +01:00
rejas
639305ecc8 Add test for prohibited words 2021-03-18 16:55:54 +01:00
rejas
a70bb517e1 Use es6 notation in newsfeed module 2021-03-18 16:55:54 +01:00
rejas
e4f671c898 Update error to use translatable text 2021-03-18 16:55:54 +01:00
rejas
a269b5cd93 Show invalid url error on UI, add test case 2021-03-18 16:55:54 +01:00
Michael Teeuw
2ec275957f Merge pull request #2486 from sdetweil/fixdaylight 2021-03-17 13:52:20 +01:00
Sam Detweiler
4f9fc032e5 fix for issue 2483, calendar shows wrong date, recurring start before 2007 2021-03-17 07:36:03 -05:00
sam detweiler
5c9dbccc10 Merge pull request #2 from MichMich/develop
synch
2021-03-17 07:33:20 -05:00
Michael Teeuw
b2cf470ec9 Merge pull request #2482 from rejas/valid_url 2021-03-16 20:06:25 +01:00
veeck
bceb181b47 Update CHANGELOG 2021-03-16 19:28:38 +01:00
veeck
1a314b741a Remove valid-url from dependencies 2021-03-16 19:27:56 +01:00
rejas
7635dea3e9 Replace valid-url library by standard node method 2021-03-16 19:25:23 +01:00
Michael Teeuw
90112d1a7d Merge pull request #2481 from rejas/fix_basic_auth 2021-03-15 21:03:02 +01:00
rejas
ad0cf12e53 Update dependencies 2021-03-15 12:50:41 +01:00
rejas
a53029f11e Use es6 notation in basic auth server 2021-03-15 12:36:58 +01:00
rejas
848529f9f4 Update CHANGELOG
so the github action is quiet ;-)
2021-03-14 21:12:36 +01:00
rejas
b5dc91fd07 Remove now unnecessary require 2021-03-14 21:04:38 +01:00
rejas
0a2b939514 Undo husky upgrade 2021-03-14 19:43:00 +01:00
rejas
52584f36d7 Fix base64 encoding for basic auth in calendar 2021-03-14 19:38:03 +01:00
Michael Teeuw
30c7a24fc2 Merge pull request #2376 from rejas/calendar_refactor 2021-03-14 12:55:24 +01:00
veeck
0643a103ac Update dependencies 2021-03-14 10:40:14 +01:00
rejas
71bd45dfd4 Fix errors introduced after latest rebase 2021-03-14 10:40:14 +01:00
rejas
85c9d3b331 More es6 notations 2021-03-14 10:40:14 +01:00
rejas
5e6cbeb9ba Convert some code to es6 2021-03-14 10:40:14 +01:00
rejas
3d4429d418 Remove copypasted-function that doesnt exist 2021-03-14 10:40:14 +01:00
rejas
d3d64d3ca0 Cleanups 2021-03-14 10:40:14 +01:00
rejas
6de983aeb2 Update CHANGELOG 2021-03-14 10:40:14 +01:00
rejas
05c3a5bf83 Remove self variable 2021-03-14 10:40:14 +01:00
rejas
c2f5d038de Move filter function into utils class too 2021-03-14 09:03:12 +01:00
rejas
0ac5032db9 Move filter function into seperate method 2021-03-14 08:59:50 +01:00
rejas
9b93066cbe Remove unused variable 2021-03-14 08:53:40 +01:00
rejas
bc60ae21c4 Cleanup node_helper to look more like the one from newsfeed module 2021-03-14 08:53:40 +01:00
rejas
0b37ed072c Refactor fetcher methods into util class 2021-03-14 08:53:40 +01:00
Michael Teeuw
57174f09b9 Merge pull request #2479 from buxxi/updatenotification-async-timeout 2021-03-14 07:23:47 +01:00
buxxi
61057d1a25 fix package-lock merge conflict 2021-03-13 22:58:30 +01:00
buxxi
03964d6f68 Merge with upstream 2021-03-13 22:56:14 +01:00
Michael Teeuw
7515fc10d1 Merge pull request #2478 from thomasrockhu/codecov-badge
Add Codecov badge to README
2021-03-13 19:22:34 +01:00
Michael Teeuw
6583f05858 Merge pull request #2477 from PostLogical/openweathermaplatlon
Allowing openweathermap config lat and lon to function without onecall, overrides locationID and location since more specific.
2021-03-13 19:20:59 +01:00
Michael Teeuw
2b1dbbde68 Merge pull request #2475 from MystaraTheGreat/master
Added hiddenOnStartup flag to module config
2021-03-13 19:20:29 +01:00
Michael Teeuw
49c95a9f46 Merge pull request #2474 from khassel/node-fetch
Node fetch
2021-03-13 19:19:22 +01:00
Michael Teeuw
a6214c8da3 Merge pull request #2473 from khassel/remove-ical
remove ical
2021-03-13 19:18:35 +01:00
buxxi
d2b3414ac6 changelog for zombie processes and fixing linting error 2021-03-13 12:02:56 +01:00
buxxi
401a6f3417 Updating simple-git and using timeout when checking for module updates 2021-03-13 11:58:33 +01:00
buxxi
3ed223a550 Refactoring update notification to use async/await 2021-03-13 11:12:08 +01:00
Tom Hu
47bd48e0a3 Add Codecov badge to README 2021-03-12 23:36:24 -05:00
Robby Griffin
cdd1853369 Fix weather module openweathermap not loading if lat and lon set without onecall. Lat and Lon take precedence over LocationID and Location. 2021-03-11 12:48:41 -05:00
MystaraTheGreat
8f2980c23d Fixed unnecessarily verbose way of looping thanks to @rejas :) 2021-03-07 20:34:26 +00:00
MystaraTheGreat
b72556b9a9 Merge branch 'master' of https://github.com/MystaraTheGreat/MagicMirror 2021-03-07 20:14:36 +00:00
MystaraTheGreat
49be3cbd6b Changed var to let as requested 2021-03-07 20:13:57 +00:00
MystaraTheGreat
fbb0c59b4e Merge branch 'develop' into master 2021-03-07 13:16:18 +00:00
MystaraTheGreat
d83e696a8d Changed variable name to appease tester 2021-03-07 11:23:52 +00:00
MystaraTheGreat
a467d900c9 Changed iterative variable from m to n to appease tester 2021-03-07 11:20:19 +00:00
MystaraTheGreat
96be8d6fea Updated CHANGELOG.md 2021-03-07 11:14:23 +00:00
MystaraTheGreat
8b1ce26fa6 Added hiddenOnStartup flag to module configuration options to cause a module to be iniitally hidden after starting up 2021-03-07 11:05:29 +00:00
Karsten Hassel
514b9453f8 removed getFetcher function 2021-03-06 21:19:14 +01:00
Karsten Hassel
fa0f997928 replace request with node-fetch 2021-03-05 22:17:55 +01:00
Karsten Hassel
504e7cadd7 getFetcher 2021-03-05 00:13:32 +01:00
Karsten Hassel
c80aeff945 fixes calendarfetcher 2021-03-04 21:12:53 +01:00
Karsten Hassel
454206c803 remove ical 2021-03-03 21:41:48 +01:00
Michael Teeuw
e4f47178fc Merge pull request #2466 from rejas/electron_update
Enable contextIsolation in Electron
2021-03-03 10:43:10 +01:00
Karsten Hassel
1f9109f8e4 snapshot 2021-03-03 00:09:32 +01:00
Karsten Hassel
f09c54184a moved tests to fetch, run prettier 2021-03-02 21:26:25 +01:00
Karsten Hassel
92a35692f2 snapshot 2021-03-02 00:40:55 +01:00
Karsten Hassel
53e300bd31 snapshot 2021-03-02 00:17:13 +01:00
veeck
e8be6ad4f1 Update CHANGELOG 2021-02-27 14:01:10 +01:00
rejas
01b9ecb339 Update dependencies 2021-02-27 13:58:59 +01:00
rejas
6ff85ebe54 Enable contextIsolation in electron
Fixes warning when starting MM
2021-02-27 13:19:16 +01:00
Michael Teeuw
ea6eebd809 Merge pull request #2439 from fewieden/feature/add-error-to-callback
Added error to callback
2021-02-23 14:18:07 +01:00
Michael Teeuw
b2a21b937d Change error string. 2021-02-23 14:11:54 +01:00
Michael Teeuw
13010ecaee Merge branch 'develop' into feature/add-error-to-callback 2021-02-23 14:10:35 +01:00
Michael Teeuw
2e2e157017 Merge pull request #2458 from codac/patch-2
This is supposed to make self signed certs work with the calendar module
2021-02-23 14:09:13 +01:00
Michael Teeuw
e23e33852e Merge pull request #2464 from fewieden/feature/expose-logger-for-modules
exposed logger as node module
2021-02-23 14:08:07 +01:00
config
5b270b84b4 linted calendarfetcher.js 2021-02-21 19:56:02 +01:00
Krouty
9094240024 Merge branch 'develop' into patch-2 2021-02-21 11:48:47 +01:00
Krouty
1db0dbf52b Added support for self-signed certificates
Added support for self-signed certificates
2021-02-21 11:45:15 +01:00
Krouty
beb5faef8b Added support for self-signed certificates
Added support for self-signed certificates
2021-02-21 11:32:03 +01:00
Krouty
c6aff8b7dc Added suppoert for self-signed certificates
Added support for self-signed certificates
2021-02-21 11:29:19 +01:00
Krouty
1aacc37c83 Added selfSignedCert as a parameter
Added selfSignedCert as a parameter, so that it can be enabled only when it is required.
2021-02-21 10:29:40 +01:00
Felix Wiedenbach
b18d98f5ea exposed logger as node module 2021-02-18 19:14:53 +01:00
Michael Teeuw
09ddd3d925 Merge pull request #2461 from fewieden/feature/add-locale 2021-02-16 23:55:51 +01:00
Felix Wiedenbach
e2b4823e43 added locale to sample config 2021-02-16 22:06:53 +01:00
Krouty
04491ac5ac This is supposed to make self signed certs work with the calendar module
Many people use Own-/Nextcloud together witht he https protocol. This is supposed to make self-signed certificates work with the calendar module and fix the issue #466.
2021-02-13 17:13:00 +01:00
Michael Teeuw
e3bee5aae7 Merge pull request #2454 from rejas/cleanup_tests 2021-02-13 17:00:58 +01:00
Michael Teeuw
89152e537e Merge pull request #2452 from TheDuffman85/develop 2021-02-13 17:00:07 +01:00
Felix Wiedenbach
652e1a528f Merge branch 'develop' into feature/add-error-to-callback
# Conflicts:
#	CHANGELOG.md
2021-02-13 08:43:23 +01:00
Felix Wiedenbach
9d85baee37 move error callback into options and rename it 2021-02-13 08:29:13 +01:00
rejas
bdc4ed4d86 Add specific change-port test and fix the others 2021-02-12 08:45:54 +01:00
rejas
35f3b315a2 Update CHANGELOG 2021-02-11 21:22:56 +01:00
rejas
efec49bb47 Use es6 notations 2021-02-11 21:22:03 +01:00
rejas
68bc77c81e Cleanup global-setup code 2021-02-11 21:21:08 +01:00
veeck
dbea348779 Cleanup port usage in tests 2021-02-11 21:20:34 +01:00
rejas
b46160bcbc Simplify weather-compliments test, increase timeWaitout for it 2021-02-11 21:20:16 +01:00
TheDuffman85
ddb06ca214 Added translation for today and tomorrow
I had missread the documentation of moment.js. We've to provide the translation for today and tomorrow ourselves. For the translation I use the standard MM² translation mechanism.
2021-02-10 14:24:59 +01:00
Michael Teeuw
ef2fd16b69 Merge pull request #2448 from khassel/electron11
fix e2e tests after spectron update
2021-02-10 10:01:57 +01:00
Michael Teeuw
d874ad9988 Merge pull request #2451 from TheDuffman85/develop 2021-02-09 17:41:05 +01:00
TheDuffman85
57dc349675 Added a new parameter to hide time portion on relative times 2021-02-09 16:06:21 +01:00
TheDuffman85
120f0299b2 Added a new parameter to hide time portion on relative times
Added a new parameter hideTime with default value false. This parameter hides the time portion on relative times.
2021-02-09 16:05:38 +01:00
Michael Teeuw
b5b6df5e48 Merge pull request #2450 from TheDuffman85/develop 2021-02-09 15:37:47 +01:00
TheDuffman85
5ffd20b843 Removed unnecessary tabs 2021-02-09 15:21:14 +01:00
TheDuffman85
3dacce6675 Respect parameter ColoredSymbolOnly also for custom events 2021-02-09 13:05:11 +01:00
TheDuffman85
8e4ba4fe93 Respect parameter ColoredSymbolOnly also for custom events 2021-02-09 13:01:57 +01:00
Michael Teeuw
37d488760f Merge pull request #2449 from rejas/update_jsdoc 2021-02-07 09:09:14 +01:00
veeck
7cdeceedf1 Update lock file 2021-02-07 08:47:18 +01:00
Karsten Hassel
7ba76020d8 fix e2e tests after spectron update 2021-02-07 00:20:30 +01:00
rejas
aab1b97653 Update CHANGELOG 2021-02-06 22:57:31 +01:00
rejas
221eadcc20 Make function callbacks and returns more readable 2021-02-06 22:55:59 +01:00
rejas
4a11cdc564 Update dependencies 2021-02-06 22:48:24 +01:00
rejas
b9333134c7 Fix lint script 2021-02-06 22:21:08 +01:00
veeck
8c83dc9494 Cleanup jsdoc 2021-02-06 22:20:49 +01:00
Felix Wiedenbach
1ed721fb15 updated changelog 2021-02-06 21:32:57 +01:00
Felix Wiedenbach
88ed5ed373 add error separate callback 2021-02-06 21:22:13 +01:00
Felix Wiedenbach
84995c9252 Merge branch 'develop' into feature/add-error-to-callback
# Conflicts:
#	CHANGELOG.md
2021-02-06 11:29:17 +01:00
Michael Teeuw
6fadc76fe3 Merge pull request #2447 from rejas/warn_dom
Warn dom
2021-02-06 11:01:21 +01:00
Michael Teeuw
d240986fdc Merge branch 'develop' into warn_dom 2021-02-06 11:01:08 +01:00
Michael Teeuw
afc73920ad Merge pull request #2443 from khassel/electron11 2021-02-06 10:58:38 +01:00
rejas
16615c3da2 Update CHANGELOG 2021-02-05 22:40:44 +01:00
rejas
88c80973f1 Dont updateDOM when the module is not displayed 2021-02-05 22:40:05 +01:00
Karsten Hassel
e322be2624 Merge branch 'develop' into electron11 2021-01-30 22:58:35 +01:00
Karsten Hassel
059b87bbb4 bump electron to v11 2021-01-30 21:58:49 +01:00
Michael Teeuw
911675f995 Merge pull request #2442 from fewieden/fix/translation-fallbacks
Fix/translation fallbacks
2021-01-30 20:21:39 +01:00
Felix Wiedenbach
e76fe5e25a updated changelog 2021-01-29 22:42:57 +01:00
Felix Wiedenbach
308774c2a6 remove callback hell 2021-01-29 22:34:12 +01:00
Felix Wiedenbach
db24f20289 cleaned up function and added test in case no file should be loaded 2021-01-29 22:25:49 +01:00
Felix Wiedenbach
94bb8e6c03 added sinon, tests for module.loadTranslations 2021-01-29 22:13:44 +01:00
Felix Wiedenbach
41da6f455a use error object for callback to include stack trace 2021-01-28 21:23:48 +01:00
Felix Wiedenbach
d2a7a3b0bb more error like message 2021-01-28 21:13:17 +01:00
Felix Wiedenbach
afbdacf136 updated changelog 2021-01-28 07:46:23 +01:00
Felix Wiedenbach
2324579057 add error to module show callback 2021-01-28 07:33:02 +01:00
Michael Teeuw
3c357f057b Merge pull request #2438 from khassel/fix_socket_v2 2021-01-27 21:15:01 +01:00
Karsten Hassel
5116a2fc82 fix socket.io backward compatibility with socket v2 clients 2021-01-27 20:52:50 +01:00
Michael Teeuw
c0ddc020d5 Merge pull request #2433 from buxxi/deprecate-old-weather 2021-01-24 11:04:13 +01:00
buxxi
0683734d5a Make a sane default for weatherEndpoint based on the type 2021-01-24 10:32:43 +01:00
buxxi
6cbd267384 Merge branch 'develop' into deprecate-old-weather 2021-01-24 10:22:39 +01:00
Michael Teeuw
925113fa20 Merge pull request #2432 from buxxi/weather-provider-hourly 2021-01-23 16:56:03 +01:00
buxxi
3696d45e94 Merge branch 'develop' into deprecate-old-weather 2021-01-23 13:56:13 +01:00
Michael Teeuw
cb3ae66652 Merge pull request #2430 from EdgardosReis/patch-4 2021-01-23 13:31:27 +01:00
buxxi
948b6c8de8 deprecate module currentweather and weatherforecast 2021-01-23 13:12:56 +01:00
buxxi
3c4d7a33e0 Fixing code style issue with no return before default 2021-01-23 12:07:10 +01:00
buxxi
5a421220c9 Updating readme and changelog and fixing typo in method 2021-01-23 11:40:02 +01:00
buxxi
41508931be Moving default values regarding specific providers into the providers themselves 2021-01-23 11:21:56 +01:00
buxxi
e2cfa24686 make weatherprovider have a method for hourly fetching instead of a generic weatherData 2021-01-23 10:45:55 +01:00
buxxi
d48113f2d9 Moving openweathermap specific check for hourly into its provider and make invalid types fail nicer 2021-01-23 10:13:41 +01:00
Edgar dos Reis
16c5bddbb7 Update CHANGELOG.md
updated changelog
2021-01-22 14:07:40 +00:00
Edgar dos Reis
ef7556f6d3 Update pt.json
Added MODULE_CONFIG_CHANGED and PRECIP translations.
2021-01-22 13:02:13 +00:00
Michael Teeuw
a3cb0b7b96 Merge pull request #2428 from rejas/update_defaults
Update documentation and help screen about invalid config files
2021-01-21 15:11:39 +01:00
Michael Teeuw
4d28688f30 Merge branch 'develop' into update_defaults 2021-01-21 15:11:31 +01:00
Michael Teeuw
01ff00fa31 Merge pull request #2427 from dannoh/Issue-TranslateEncodedHtml
Issue translate encoded html
2021-01-21 15:10:28 +01:00
Michael Teeuw
78190d9c7f Merge branch 'develop' into Issue-TranslateEncodedHtml 2021-01-21 15:10:14 +01:00
Michael Teeuw
b1565e4047 Merge pull request #2423 from rejas/add_start_dev
Added start:dev command to npm scripts
2021-01-21 15:08:52 +01:00
Michael Teeuw
db220b7861 Merge branch 'develop' into add_start_dev 2021-01-21 15:08:44 +01:00
Michael Teeuw
eaf27b837e Merge pull request #2422 from buxxi/develop
Refactoring newsfeed-module to use templates instead of dom-generation
2021-01-21 15:07:22 +01:00
Michael Teeuw
74410344af Merge pull request #2421 from klaernie/no-empty-subdirs
prevent empty path components for module main scripts
2021-01-21 15:07:07 +01:00
Michael Teeuw
998f64f983 Merge branch 'develop' into no-empty-subdirs 2021-01-21 15:06:57 +01:00
Michael Teeuw
684dfb643b Merge pull request #2420 from rejas/issue_2416
Add new Notification CURRENTWEATHER_TYPE
2021-01-21 15:05:09 +01:00
Michael Teeuw
314c3cb516 Merge pull request #2417 from ashishtank/Issue2221
Fixed Unit test case error for #2221
2021-01-21 15:04:33 +01:00
veeck
58939bfd8c Update documentation and help screen about invalid config files 2021-01-20 22:44:37 +01:00
Dan Forsyth
33592b3c0e Removed |safe from translates in the default module templates 2021-01-20 14:06:00 -05:00
Dan Forsyth
ad9c2549bc Removed |safe from translates in the default module templates 2021-01-20 13:47:32 -05:00
Dan Forsyth
5152e0b114 Updated changelog 2021-01-20 07:36:26 -05:00
Dan Forsyth
ca48663efd Merge remote-tracking branch 'origin/develop' into Issue-TranslateEncodedHtml 2021-01-20 07:31:11 -05:00
Dan Forsyth
b520b4c37a Marked all translated strings as safe before passing them to the nunjuck template 2021-01-20 07:16:09 -05:00
veeck
90f07295b1 Add extra check for currentweather 2021-01-17 15:00:34 +01:00
veeck
3895c18466 Add test case 2021-01-17 14:57:06 +01:00
veeck
2b6a9fc5bb Update dependencies 2021-01-17 12:41:42 +01:00
rejas
052f0b8709 Added start:dev script 2021-01-16 22:23:55 +01:00
buxxi
a5bb9d962d Fixing eslint issues 2021-01-16 14:06:07 +01:00
buxxi
2d9d28aa0f updating changelog with newsfeed changes 2021-01-16 13:47:16 +01:00
buxxi
69c053a94f refactoring newsfeed to use templates instead of generating dom in the code 2021-01-16 13:37:18 +01:00
buxxi
aaaf1f660c refactoring newsfeed, moving hiding of module while loading away from dom-generation 2021-01-16 12:26:38 +01:00
buxxi
8538d83520 Moving newsfeed styling from js to a new css file 2021-01-16 11:52:55 +01:00
buxxi
132c98b767 refactoring newsfeed, moving tag stripping to loading instead of presentation logic 2021-01-16 11:10:53 +01:00
rejas
42cac81953 Fix tests 2021-01-15 23:12:44 +01:00
Andre Klärner
3ee4bd65c6 prevent empty path components for module main scripts
The module.path component has by definition in line 97 a trailing slash.
Hence adding another is unneeded, but results in an additional folder in the inspector.
2021-01-15 23:09:07 +01:00
rejas
fcc7e80bf9 Update CHANGELOG 2021-01-15 21:49:12 +01:00
rejas
e0d43a4c1e Add new Event CURRENTWEATHER_TYPE
- send it from the weather and currentweather module
- use it in the compliments module directly instead of the data
2021-01-15 21:47:14 +01:00
Ashish Tank
4966d6c920 Fixed Unit test case error for #2221 2021-01-14 19:10:04 +01:00
Michael Teeuw
1fd506f25d Merge pull request #2414 from ashishtank/FeelsLikeCleanup
Feels like translation code cleanup
2021-01-13 09:50:11 +01:00
Michael Teeuw
a99698d1a9 Merge pull request #2413 from ashishtank/Issue2221
Issue 2221 Weather module - Always displays night icons because of fixed day start and end time
2021-01-12 16:14:06 +01:00
Ashish Tank
774b86c7dc Code cleanup for feels like translation 2021-01-10 17:37:10 +01:00
Ashish Tank
2c3e8533c7 Issue #2221 - Weather forecast always shows night icons in day time 2021-01-10 16:24:46 +01:00
ashishtank
3eda8af671 Merge pull request #9 from MichMich/develop
Develop
2021-01-10 16:03:49 +01:00
Michael Teeuw
aa61874848 Merge pull request #2411 from khassel/fix_cors
fix socket.io cors errors
2021-01-10 10:13:16 +01:00
Karsten Hassel
2deab31187 fix socket.io cors errors, see breaking change since socket.io v3 https://socket.io/docs/v3/handling-cors/ 2021-01-09 23:20:36 +01:00
Michael Teeuw
b177a56fa2 Fix wrong placement of changelog item. 2021-01-07 12:45:03 +01:00
Michael Teeuw
6f0f75cf27 Merge pull request #2388 from drewski3420/no_negative_zero
No negative zero
2021-01-07 12:41:30 +01:00
Michael Teeuw
39bb2eb9b0 Add Changelog. 2021-01-07 11:53:21 +01:00
Michael Teeuw
7b36bb025a Improve readabiliy. 2021-01-07 11:51:10 +01:00
Michael Teeuw
aa9a1b7af2 Add CodeCov badge to Readme. 2021-01-06 11:03:15 +01:00
Michael Teeuw
caf3552d6b Merge pull request #2401 from rejas/github_codecov_action
Add github action for uploading codecov results
2021-01-06 10:05:50 +01:00
veeck
6e9897f7fc Remove now unnecessary file 2021-01-06 09:33:06 +01:00
veeck
fa9258761e Change on-trigger 2021-01-06 09:33:06 +01:00
veeck
003e948899 Use codecov action instead of bash command 2021-01-06 09:33:06 +01:00
veeck
9cd998f219 Update CHANGELOG 2021-01-06 09:33:06 +01:00
rejas
d75b894d9a Add lcov reporter 2021-01-06 09:33:06 +01:00
rejas
5a3d3b76a7 Cleanups 2021-01-06 09:33:06 +01:00
rejas
5bc2c207db Add codecov github action 2021-01-06 09:33:06 +01:00
Michael Teeuw
612cf25878 Merge pull request #2407 from fewieden/feature/update-node-js-code
update node js code
2021-01-06 09:01:42 +01:00
Felix Wiedenbach
5ae4912b45 added changelog entry 2021-01-05 20:08:18 +01:00
Felix Wiedenbach
a9a70fd2e9 linting and fix defualt module test 2021-01-05 20:04:02 +01:00
Felix Wiedenbach
f90856808b fix config file path 2021-01-05 19:39:31 +01:00
Felix Wiedenbach
7dbcaa83bc clean up deprecated 2021-01-05 19:36:20 +01:00
Felix Wiedenbach
38f10b6e3e clean up app 2021-01-05 19:35:11 +01:00
Felix Wiedenbach
9c8fa06ce1 clean up config checker 2021-01-05 19:01:59 +01:00
Felix Wiedenbach
4efe04774c clean up electron 2021-01-05 18:48:55 +01:00
Felix Wiedenbach
5d60534dc9 clean up node helper 2021-01-05 18:44:36 +01:00
Felix Wiedenbach
ac141a4316 clean up server 2021-01-05 18:37:16 +01:00
Felix Wiedenbach
d22064c6f4 clean up utils 2021-01-05 18:37:01 +01:00
Michael Teeuw
189721ebba Merge pull request #2406 from rejas/danger
Remove now unused danger library
2021-01-05 16:54:55 +01:00
Michael Teeuw
7aa9c63dba Merge pull request #2405 from rejas/markdown
Update markdown for 2021
2021-01-05 16:53:46 +01:00
veeck
16e894e300 Update CHANGELOG 2021-01-05 15:07:40 +01:00
veeck
d466705ec0 Fix eslint error due to now js files being in root anymore 2021-01-05 15:05:50 +01:00
veeck
0e97d863ce Remove now unused danger library 2021-01-05 14:43:41 +01:00
veeck
5de64d2ae8 Added engine fields into package json 2021-01-05 09:54:03 +01:00
veeck
ced0398e49 Update markdowns 2021-01-05 09:53:47 +01:00
Michael Teeuw
d4b57924a7 Merge pull request #2404 from rejas/issue_2402
Update default log levels
2021-01-04 13:52:37 +01:00
veeck
d2def2bea3 Update CHANGELOG 2021-01-04 10:03:35 +01:00
veeck
e65bb84f9f Add default values for log levels 2021-01-04 10:01:56 +01:00
Michael Teeuw
69b0aa6118 Prepare v2.15.0-develop 2021-01-01 19:27:07 +01:00
drewski3420@gmail.com
e51f6597ed This change prevents returning '-0' (negative zero) when roundTemp is true. 2020-12-30 09:03:19 -05:00
drewski3420@gmail.com
e80a65a3cd This change prevents returning '-0' (negative zero) when roundTemp is true 2020-12-30 08:51:07 -05:00
drewski3420@gmail.com
7c3675c9e1 This change prevents returning '-0' (negative zero) when roundTemp is true. 2020-12-29 21:07:14 -05:00
214 changed files with 21793 additions and 9695 deletions

View File

@@ -1,10 +1,10 @@
{
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:jsdoc/recommended"],
"plugins": ["prettier", "jsdoc"],
"plugins": ["prettier", "jsdoc", "jest"],
"env": {
"browser": true,
"es6": true,
"mocha": true,
"jest/globals": true,
"node": true
},
"globals": {
@@ -25,6 +25,7 @@
"prettier/prettier": "error",
"eqeqeq": "error",
"no-prototype-builtins": "off",
"no-unused-vars": "off"
"no-unused-vars": "off",
"no-useless-return": "error"
}
}

View File

@@ -18,22 +18,23 @@ To run ESLint, use `npm run lint:js`.
We use [StyleLint](https://stylelint.io) to lint our CSS. Our configuration is in our .stylelintrc file.
To run StyleLint, use `npm run lint:style`.
To run StyleLint, use `npm run lint:css`.
### Submitting 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)
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:
**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 12 or later (recommended is 14).
**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**: Please let us know which version of MagicMirror you are running. It can be found in the `package.json` file.
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.

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

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

View File

@@ -1,3 +1,7 @@
Hello and thank you for opening an issue.
**Please make sure that you have read the following lines before submitting your Issue:**
## I'm not sure if this is a bug
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)
@@ -6,6 +10,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)
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
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:
@@ -13,8 +19,10 @@ If you are facing an issue or found a bug while trying to install MagicMirror vi
## I found a bug in the MagicMirror Docker image
If you are facing an issue or found a bug while running MagicMirror inside a Docker container please create an issue in the GitHub repository of the MagicMirror Docker image:
[https://github.com/bastilimbach/docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror)
If you are facing an issue or found a bug while running MagicMirror inside a Docker container please create an issue in the corresponding repository:
- karsten13/magicmirror: [https://gitlab.com/khassel/magicmirror](https://gitlab.com/khassel/magicmirror)
- (deprecated) bastilimbach/docker-magicmirror: [https://github.com/bastilimbach/docker-MagicMirror](https://github.com/bastilimbach/docker-MagicMirror)
---
@@ -23,11 +31,11 @@ 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.
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 12 or later (recommended is 14).
**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 know which version of MagicMirror you are running. It can be found in the `package.json` file.
**Description**: Provide a detailed description about the issue and include specific details to help us understand the problem. Adding screenshots will help describing the problem.

View File

@@ -1,13 +1,25 @@
> Please send your pull requests the develop branch.
> Don't forget to add the change to CHANGELOG.md.
Hello and thank you for wanting to contribute to the MagicMirror project
**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
recommended that you update your branch of `develop` before creating a
pull request to send us your changes. This makes everyone's lives
easier (including yours) and helps us out on the development team.
Thanks!
- Does the pull request solve a **related** issue?
- 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.
Thanks again and have a nice day!

6
.github/codecov.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
coverage:
status:
project:
default:
# advanced settings
informational: true

BIN
.github/header.psd vendored

Binary file not shown.

37
.github/workflows/automated-tests.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: "Run Automated Tests"
on:
push:
branches: [master, develop]
pull_request:
branches: [master, develop]
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies and run tests
run: |
Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99
npm install
touch css/custom.css
npm run test:prettier
npm run test:js
npm run test:css
npm run test:unit
npm run test:e2e
npm run test:electron

View File

@@ -0,0 +1,29 @@
# 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
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies and run coverage
run: |
Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99
npm ci
touch css/custom.css
npm run test:coverage
- name: Upload coverage results to codecov
uses: codecov/codecov-action@v1
with:
file: ./coverage/lcov.info
fail_ci_if_error: true

View File

@@ -1,15 +1,20 @@
# This workflow enforces the update of a changelog file on every pull request
name: "Enforce Changelog"
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
jobs:
# Enforces the update of a changelog file on every pull request
check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- uses: dangoslen/changelog-enforcer@v1.6.1
with:
changeLogPath: 'CHANGELOG.md'
skipLabels: 'Skip Changelog'
- name: Checkout code
uses: actions/checkout@v2
- name: Enforce changelog
uses: dangoslen/changelog-enforcer@v1.6.1
with:
changeLogPath: "CHANGELOG.md"
skipLabels: "Skip Changelog"

View File

@@ -1,35 +0,0 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Automated Tests
on:
push:
branches: [ master, develop ]
pull_request:
branches: [ master, develop ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: |
Xvfb :99 -screen 0 1024x768x16 &
export DISPLAY=:99
npm install
npm run test:prettier
npm run test:js
npm run test:css
npm run test:e2e
npm run test:unit

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint:staged

View File

@@ -1,5 +1,4 @@
/config
/coverage
.nyc_output
package-lock.json
/config/**/*
/vendor/**/*
!/vendor/vendor.js
.github/**/*

View File

@@ -5,6 +5,163 @@ This project adheres to [Semantic Versioning](https://semver.org/).
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror²
## [2.17.1] - 2021-10-01
### Fixed
- Fixed error when accessing letsencrypt certificates
## [2.17.0] - 2021-10-01
Special thanks to the following contributors: @apiontek, @eouia, @jupadin, @khassel and @rejas.
### Added
- Added showTime parameter to clock module for enabling/disabling time display in analog clock.
- Added custom electron switches from user config (`config.electronSwitches`).
- Added unit tests for updatenotification module.
### Updated
- Bump electron to v13 (and spectron to v15) and update other dependencies in package.json.
- Refactor test configs, use default test config for all tests.
- Updated github templates.
- Actually test all js and css files when lint script is run.
- Update jsdocs and print warnings during testing too.
- Update weathergov provider to try fetching not just current, but also foreacst, when API URLs available.
- Refactored clock layout.
- Refactored methods from weatherproviders into weatherobject (isDaytime, updateSunTime).
- Use of `logger.js` in jest tests.
- Run prettier over all relevant files.
- Move tests needing electron in new category `electron`, use `server only` mode in `e2e` tests.
- Update dependencies in package.json.
### Fixed
- Fix undefined error with ignoreToday option in weather module (#2620).
- Fix time zone correction in calendar module when the date hour is equal to the time zone correction value (#2632).
- Fix black cursor on startup when using electron.
- Fix update notification not working for own repository (#2644).
## [2.16.0] - 2021-07-01
Special thanks to the following contributors: @210954, @B1gG, @codac, @Crazylegstoo, @daniel, @earlman, @ezeholz, @FrancoisRmn, @jupadin, @khassel, @KristjanESPERANTO, @njwilliams, @oemel09, @r3wald, @rejas, @rico24, Faizan Ahmed.
### Added
- Added French translations for "MODULE_CONFIG_ERROR" and "PRECIP".
- Added German translation for "PRECIP".
- Added Dutch translation for "WEEK", "PRECIP", "MODULE_CONFIG_CHANGED" and "MODULE_CONFIG_ERROR".
- Added first test for Alert module.
- Added support for `dateFormat` when not using `timeFormat: "absolute"`.
- Added custom-properties for colors and fonts for improved styling experience, see `custom.css.sample` file.
- Added custom-properties for gaps around body and between modules.
- Added test case for recurring calendar events.
- Added new Environment Canada provider for default WEATHER module (weather data for Canadian locations only).
- Added list view for newsfeed module.
- Added dev dependency jest, switching from mocha to jest.
### Updated
- Bump node-ical to v0.13.0 (now last runtime dependency using deprecated `request` package is removed).
- Use codecov in informational mode.
- Refactor code into es6 where possible (e.g. var -> let/const).
- Use node v16 in github workflow (replacing node v10).
- Moved some files into better suited directories.
- Update dependencies in package.json, require node >= v12, remove `rrule-alt` and `rrule`.
- Update dependencies in package.json and migrate husky to v6, fix husky setup in prod environment.
- Cleaned up error handling in newsfeed and calendar modules for real.
- Updated default WEATHER module such that a provider can optionally set a custom unit-of-measure for precipitation (`weatherObject.precipitationUnits`).
- Update documentation.
- Update jest tests: Reset changes on js/logger.js, mock logger.js in global_vars tests.
- Update dependencies in package.json.
### Removed
- Switching from mocha to jest so removed following dev dependencies: chai, chai-as-promised, mocha, mocha-each, mocha-logger.
### Fixed
- Fix calendar start function logging inconsistency.
- Fix updatenotification start function logging inconsistency.
- Checks and applies the showDescription setting for the newsfeed module again.
- Fix issue with openweathermap not showing current or forecast info when using onecall API.
- Fix tests in weather module and add one for decimalPoint in forecast.
- Fix decimalSymbol in the forecast part of the new weather module (#2530).
- Fix wrong treatment of `appendLocationNameToHeader` when using `ukmetofficedatahub`.
- Fix alert not recognizing multiple alerts (#2522).
- Fix fetch option httpsAgent to agent in calendar module (#466).
- Fix module updatenotification which did not work for repos with many refs (#1907).
- Fix config check failing when encountering let syntax ("Parsing error: Unexpected token config").
- Fix calendar debug check.
- Really run prettier over all files.
- Fix logger.js after jest changes, use --forceExit running jest.
- Workaround for dev_console test using getWindowCount.
## [2.15.0] - 2021-04-01
Special thanks to the following contributors: @EdgardosReis, @MystaraTheGreat, @TheDuffman85, @ashishtank, @buxxi, @codac, @fewieden, @khassel, @klaernie, @qu1que, @rejas, @sdetweil & @thomasrockhu.
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`.
### Added
- Added Galician language.
- Added GitHub workflows for automated testing and changelog enforcement.
- Added CodeCov badge to Readme.
- Added CURRENTWEATHER_TYPE notification to currentweather and weather module, use it in compliments module.
- Added `start:dev` command to the npm scripts for starting electron with devTools open.
- Added logging when using deprecated modules weatherforecast or currentweather.
- Added Portuguese translations for "MODULE_CONFIG_CHANGED" and "PRECIP".
- Respect parameter ColoredSymbolOnly also for custom events.
- Added a new parameter to hide time portion on relative times.
- `module.show` has now the option for a callback on error.
- Added locale to sample config file.
- Added support for self-signed certificates for the default calendar module (#466).
- Added hiddenOnStartup flag to module config (#2475).
### Updated
- Updated markdown files for github.
- Cleaned up old code on server side.
- Convert `-0` to `0` when displaying temperature.
- Code cleanup for FEELS like and added {DEGREE} placeholder for FEELSLIKE for each language.
- Converted newsfeed module to use templates.
- Updated documentation and help screen about invalid config files.
- Moving weather provider specific code and configuration into each provider and making hourly part of the interface.
- Bump electron to v11 and enable contextIsolation.
- Don't update the DOM when a module is not displayed.
- Cleaned up jsdoc and tests.
- Exposed logger as node module for easier access for 3rd party modules.
- Replaced deprecated `request` package with `node-fetch` and `digest-fetch`.
- Refactored calendar fetcher.
- Cleaned up newsfeed module.
- Cleaned up translations and translator code.
### Removed
- Removed danger.js library.
- Removed `ical` which was substituted by `node-ical` in release `v2.13.0`. Module developers must install this dependency themselves in the module folder if needed.
- Removed valid-url library.
### Fixed
- Added default log levels to stop calendar log spamming.
- Fix socket.io cors errors, see [breaking change since socket.io v3](https://socket.io/docs/v3/handling-cors/).
- Fix Issue with weather forecast icons due to fixed day start and end time (#2221).
- Fix empty directory for each module's main javascript file in the inspector.
- Fix Issue with weather forecast icons unit tests with different timezones (#2221).
- Fix issue with unencoded characters in translated strings when using nunjuck template (`Loading &hellip;` as an example).
- Fix socket.io backward compatibility with socket v2 clients.
- Fix 3rd party module language loading if language is English.
- Fix e2e tests after spectron update.
- Fix updatenotification creating zombie processes by setting a timeout for the git process.
- Fix weather module openweathermap not loading if lat and lon set without onecall.
- Fix calendar daylight savings offset calculation if recurring start date before 2007.
- Fix calendar time/date adjustment when time with GMT offset is different day (#2488).
- Fix calendar daylight savings offset calculation if recurring FULL DAY start date before 2007 (#2483).
- Fix newsreaders template, for wrong test for nowrap in 2 places (should be if not).
## [2.14.0] - 2021-01-01
Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank, @bluemanos, @flopp999, @jakemulley, @jakobsarwary1, @marvai-vgtu, @mirontoli, @rejas, @sdetweil, @Snille & @Sub028.
@@ -15,17 +172,15 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
- Added new log level "debug" to the logger.
- 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 SMHI as a provider to Weather module.
- Added Hindi & Gujarati translation.
- Added optional support for DEGREE position in Feels like translation.
- Added support for variables in nunjucks templates for translate filter.
- Added Chuvash translation.
- Calendar: new options "limitDays" and "coloredEvents".
- Added new option "limitDays" - limit the number of discreet days displayed.
- Added new option "customEvents" - use custom symbol/color based on keyword in event title.
- Added GitHub workflows for automated testing and changelog enforcement.
### Updated
@@ -44,7 +199,7 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
### Deleted
- Removed Travis CI intergration.
- Removed Travis CI integration.
### Fixed
@@ -61,8 +216,8 @@ Special thanks to the following contributors: @Alvinger, @AndyPoms, @ashishtank,
- Fix non-fullday recurring rule processing. (#2216)
- Catch errors when parsing calendar data with ical. (#2022)
- Fix Default Alert Module does not hide black overlay when alert is dismissed manually. (#2228)
- Weather module - Always displays night icons when local is other then English. (#2221)
- Update Node-ical 0.12.4 , fix invalid RRULE format in cal entries
- 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
- Fix package.json for optional electron dependency (2378)
- Update node-ical version again, 0.12.5, change RRULE fix (#2371, #2379)
- Remove undefined objects from modules array (#2382)
@@ -77,11 +232,11 @@ Special thanks to the following contributors: @bryanzzhu, @bugsounet, @chamakura
### 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
for updates to MagicMirror and/or MagicMirror modules.
- 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 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.
@@ -145,7 +300,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)
- 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)
- 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)
## [2.11.0] - 2020-04-01
@@ -295,6 +450,7 @@ Special thanks to @sdetweil for all his great contributions!
- Update `ical.js` to solve various calendar issues.
- Update weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
- Only update clock once per minute when seconds aren't shown
- Update weatherprovider documentation.
### Fixed
@@ -439,7 +595,7 @@ A huge, huge, huge thanks to user @fewieden for all his hard work on the new `we
### Fixed
- 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 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)

View File

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

View File

@@ -1,9 +1,10 @@
![MagicMirror²: The open source modular smart mirror platform. ](.github/header.png)
<p align="center">
<p style="text-align: center">
<a href="https://david-dm.org/MichMich/MagicMirror"><img src="https://david-dm.org/MichMich/MagicMirror.svg" alt="Dependency Status"></a>
<a href="https://david-dm.org/MichMich/MagicMirror#info=devDependencies"><img src="https://david-dm.org/MichMich/MagicMirror/dev-status.svg" alt="devDependency Status"></a>
<a href="https://bestpractices.coreinfrastructure.org/projects/347"><img src="https://bestpractices.coreinfrastructure.org/projects/347/badge"></a>
<a href="https://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" 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://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>
</p>
@@ -21,13 +22,20 @@ For the full documentation including **[installation instructions](https://docs.
- Website: [https://magicmirror.builders](https://magicmirror.builders)
- Documentation: [https://docs.magicmirror.builders](https://docs.magicmirror.builders)
- Forum: [https://forum.magicmirror.builders](https://forum.magicmirror.builders)
- Technical discussions: https://forum.magicmirror.builders/category/11/core-system
- Discord: [https://discord.gg/J5BAtvx](https://discord.gg/J5BAtvx)
- Blog: [https://michaelteeuw.nl/tagged/magicmirror](https://michaelteeuw.nl/tagged/magicmirror)
- Donations: [https://magicmirror.builders/#donate](https://magicmirror.builders/#donate)
## 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!
@@ -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.
<p align="center">
<br>
<p style="text-align: center">
<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>

View File

@@ -2,7 +2,7 @@
// Use separate scope to prevent global scope pollution
(function () {
var config = {};
const config = {};
/**
* Helper function to get server address/hostname from either the commandline or env
@@ -14,12 +14,11 @@
*
* @param {string} key key to look for at the command line
* @param {string} defaultValue value if no key is given at the command line
*
* @returns {string} the value of the parameter
*/
function getCommandLineParameter(key, defaultValue = undefined) {
var index = process.argv.indexOf(`--${key}`);
var value = index > -1 ? process.argv[index + 1] : undefined;
const index = process.argv.indexOf(`--${key}`);
const value = index > -1 ? process.argv[index + 1] : undefined;
return value !== undefined ? String(value) : defaultValue;
}
@@ -36,7 +35,6 @@
* Gets the config from the specified server url
*
* @param {string} url location where the server is running.
*
* @returns {Promise} the config
*/
function getServerConfig(url) {
@@ -45,7 +43,7 @@
// Select http or https module, depending on requested url
const lib = url.startsWith("https") ? require("https") : require("http");
const request = lib.get(url, (response) => {
var configData = "";
let configData = "";
// Gather incoming data
response.on("data", function (chunk) {
@@ -66,7 +64,7 @@
/**
* 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
*/
function fail(message, code = 1) {
@@ -81,15 +79,15 @@
getServerAddress();
(config.address && config.port) || fail();
var prefix = config.tls ? "https://" : "http://";
const prefix = config.tls ? "https://" : "http://";
// Only start the client if a non-local server was provided
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) === -1) {
getServerConfig(`${prefix}${config.address}:${config.port}/config/`)
.then(function (configReturn) {
// Pass along the server config via an environment variable
var env = Object.create(process.env);
var options = { env: env };
const env = Object.create(process.env);
const options = { env: env };
configReturn.address = config.address;
configReturn.port = config.port;
configReturn.tls = config.tls;

View File

@@ -4,11 +4,10 @@
* MIT Licensed.
*
* For more information on how you can configure this file
* See https://github.com/MichMich/MagicMirror#configuration
*
* see https://docs.magicmirror.builders/getting-started/configuration.html#general
* and https://docs.magicmirror.builders/modules/configuration.html
*/
var config = {
let config = {
address: "localhost", // Address to listen on, can be:
// - "localhost", "127.0.0.1", "::1" to listen on loopback interface
// - another specific IPv4/6 to listen on a specific interface
@@ -28,6 +27,7 @@ var config = {
httpsCertificate: "", // HTTPS Certificate path, only require when useHttps is true
language: "en",
locale: "en-US",
logLevel: ["INFO", "LOG", "WARN", "ERROR"], // Add "DEBUG" for even more logging
timeFormat: 24,
units: "metric",
@@ -57,7 +57,8 @@ var config = {
calendars: [
{
symbol: "calendar-check",
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics" }
url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics"
}
]
}
},
@@ -66,22 +67,26 @@ var config = {
position: "lower_third"
},
{
module: "currentweather",
module: "weather",
position: "top_right",
config: {
weatherProvider: "openweathermap",
type: "current",
location: "New York",
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",
header: "Weather Forecast",
config: {
weatherProvider: "openweathermap",
type: "forecast",
location: "New York",
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"
}
},
{

31
css/custom.css.sample Normal file
View File

@@ -0,0 +1,31 @@
/* Magic Mirror Custom CSS Sample
*
* Change color and fonts here.
*
* Beware that properties cannot be unitless, so for example write '--gap-body: 0px;' instead of just '--gap-body: 0;'
*
* MIT Licensed.
*/
/* Uncomment and adjust accordingly if you want to import another font from the google-fonts-api: */
/* @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;300;400;700&display=swap'); */
:root {
--color-text: #999;
--color-text-dimmed: #666;
--color-text-bright: #fff;
--color-background: black;
--font-primary: "Roboto Condensed";
--font-secondary: "Roboto";
--font-size: 20px;
--font-size-small: 0.75rem;
--gap-body-top: 60px;
--gap-body-right: 60px;
--gap-body-bottom: 60px;
--gap-body-left: 60px;
--gap-modules: 30px;
}

View File

@@ -1,8 +1,29 @@
:root {
--color-text: #999;
--color-text-dimmed: #666;
--color-text-bright: #fff;
--color-background: #000;
--font-primary: "Roboto Condensed";
--font-secondary: "Roboto";
--font-size: 20px;
--font-size-small: 0.75rem;
--gap-body-top: 60px;
--gap-body-right: 60px;
--gap-body-bottom: 60px;
--gap-body-left: 60px;
--gap-modules: 30px;
}
html {
cursor: none;
overflow: hidden;
background: #000;
background: var(--color-background);
user-select: none;
font-size: var(--font-size);
}
::-webkit-scrollbar {
@@ -10,16 +31,15 @@ html {
}
body {
margin: 60px;
margin: var(--gap-body-top) var(--gap-body-right) var(--gap-body-bottom) var(--gap-body-left);
position: absolute;
height: calc(100% - 120px);
width: calc(100% - 120px);
background: #000;
color: #aaa;
font-family: "Roboto Condensed", sans-serif;
height: calc(100% - var(--gap-body-top) - var(--gap-body-bottom));
width: calc(100% - var(--gap-body-right) - var(--gap-body-left));
background: var(--color-background);
color: var(--color-text);
font-family: var(--font-primary), sans-serif;
font-weight: 400;
font-size: 2em;
line-height: 1.5em;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
@@ -28,60 +48,60 @@ body {
*/
.dimmed {
color: #666;
color: var(--color-text-dimmed);
}
.normal {
color: #999;
color: var(--color-text);
}
.bright {
color: #fff;
color: var(--color-text-bright);
}
.xsmall {
font-size: 15px;
line-height: 20px;
font-size: var(--font-size-small);
line-height: 1.275;
}
.small {
font-size: 20px;
line-height: 25px;
font-size: 1rem;
line-height: 1.25;
}
.medium {
font-size: 30px;
line-height: 35px;
font-size: 1.5rem;
line-height: 1.225;
}
.large {
font-size: 65px;
line-height: 65px;
font-size: 3.25rem;
line-height: 1;
}
.xlarge {
font-size: 75px;
line-height: 75px;
font-size: 3.75rem;
line-height: 1;
letter-spacing: -3px;
}
.thin {
font-family: Roboto, sans-serif;
font-family: var(--font-secondary), sans-serif;
font-weight: 100;
}
.light {
font-family: "Roboto Condensed", sans-serif;
font-family: var(--font-primary), sans-serif;
font-weight: 300;
}
.regular {
font-family: "Roboto Condensed", sans-serif;
font-family: var(--font-primary), sans-serif;
font-weight: 400;
}
.bold {
font-family: "Roboto Condensed", sans-serif;
font-family: var(--font-primary), sans-serif;
font-weight: 700;
}
@@ -95,14 +115,14 @@ body {
header {
text-transform: uppercase;
font-size: 15px;
font-family: "Roboto Condensed", Arial, Helvetica, sans-serif;
font-size: var(--font-size-small);
font-family: var(--font-primary), Arial, Helvetica, sans-serif;
font-weight: 400;
border-bottom: 1px solid #666;
border-bottom: 1px solid var(--color-text-dimmed);
line-height: 15px;
padding-bottom: 5px;
margin-bottom: 10px;
color: #999;
color: var(--color-text);
}
sup {
@@ -115,11 +135,11 @@ sup {
*/
.module {
margin-bottom: 30px;
margin-bottom: var(--gap-modules);
}
.region.bottom .module {
margin-top: 30px;
margin-top: var(--gap-modules);
margin-bottom: 0;
}
@@ -143,10 +163,10 @@ sup {
.region.fullscreen {
position: absolute;
top: -60px;
left: -60px;
right: -60px;
bottom: -60px;
top: calc(-1 * var(--gap-body-top));
left: calc(-1 * var(--gap-body-left));
right: calc(-1 * var(--gap-body-right));
bottom: calc(-1 * var(--gap-body-bottom));
pointer-events: none;
}
@@ -163,18 +183,6 @@ sup {
top: 0;
}
.region.top .container {
margin-bottom: 25px;
}
.region.bottom .container {
margin-top: 25px;
}
.region.top .container:empty {
margin-bottom: 0;
}
.region.top.center,
.region.bottom.center {
left: 50%;
@@ -191,10 +199,6 @@ sup {
bottom: 0;
}
.region.bottom .container:empty {
margin-top: 0;
}
.region.bottom.right,
.region.bottom.center,
.region.bottom.left {

View File

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

View File

@@ -1,12 +1,26 @@
{
"name": "magicmirror-fonts",
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"roboto-fontface": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz",
"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g=="
}
}
"name": "magicmirror-fonts",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "magicmirror-fonts",
"license": "MIT",
"dependencies": {
"roboto-fontface": "^0.10.0"
}
},
"node_modules/roboto-fontface": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz",
"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g=="
}
},
"dependencies": {
"roboto-fontface": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz",
"integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g=="
}
}
}

View File

@@ -1,55 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<title>MagicMirror²</title>
<meta name="google" content="notranslate" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<head>
<title>MagicMirror²</title>
<meta name="google" content="notranslate" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="format-detection" content="telephone=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<link rel="stylesheet" type="text/css" href="css/main.css">
<link rel="stylesheet" type="text/css" href="fonts/roboto.css">
<!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<link rel="stylesheet" type="text/css" href="css/main.css" />
<link rel="stylesheet" type="text/css" href="fonts/roboto.css" />
<!-- custom.css is loaded by the loader.js to make sure it's loaded after the module css files. -->
<script type="text/javascript">
var version = "#VERSION#";
</script>
</head>
<body>
<div class="region fullscreen below"><div class="container"></div></div>
<div class="region top bar">
<div class="container"></div>
<div class="region top left"><div class="container"></div></div>
<div class="region top center"><div class="container"></div></div>
<div class="region top right"><div class="container"></div></div>
</div>
<div class="region upper third"><div class="container"></div></div>
<div class="region middle center"><div class="container"></div></div>
<div class="region lower third"><div class="container"><br/></div></div>
<div class="region bottom bar">
<div class="container"></div>
<div class="region bottom left"><div class="container"></div></div>
<div class="region bottom center"><div class="container"></div></div>
<div class="region bottom right"><div class="container"></div></div>
</div>
<div class="region fullscreen above"><div class="container"></div></div>
<script type="text/javascript" src="socket.io/socket.io.js"></script>
<script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script>
<script type="text/javascript" src="js/defaults.js"></script>
<script type="text/javascript" src="#CONFIG_FILE#"></script>
<script type="text/javascript" src="vendor/vendor.js"></script>
<script type="text/javascript" src="modules/default/defaultmodules.js"></script>
<script type="text/javascript" src="js/logger.js"></script>
<script type="text/javascript" src="translations/translations.js"></script>
<script type="text/javascript" src="js/translator.js"></script>
<script type="text/javascript" src="js/class.js"></script>
<script type="text/javascript" src="js/module.js"></script>
<script type="text/javascript" src="js/loader.js"></script>
<script type="text/javascript" src="js/socketclient.js"></script>
<script type="text/javascript" src="js/main.js"></script>
</body>
<script type="text/javascript">
window.mmVersion = "#VERSION#";
</script>
</head>
<body>
<div class="region fullscreen below"><div class="container"></div></div>
<div class="region top bar">
<div class="container"></div>
<div class="region top left"><div class="container"></div></div>
<div class="region top center"><div class="container"></div></div>
<div class="region top right"><div class="container"></div></div>
</div>
<div class="region upper third"><div class="container"></div></div>
<div class="region middle center"><div class="container"></div></div>
<div class="region lower third">
<div class="container"><br /></div>
</div>
<div class="region bottom bar">
<div class="container"></div>
<div class="region bottom left"><div class="container"></div></div>
<div class="region bottom center"><div class="container"></div></div>
<div class="region bottom right"><div class="container"></div></div>
</div>
<div class="region fullscreen above"><div class="container"></div></div>
<script type="text/javascript" src="socket.io/socket.io.js"></script>
<script type="text/javascript" src="vendor/node_modules/nunjucks/browser/nunjucks.min.js"></script>
<script type="text/javascript" src="js/defaults.js"></script>
<script type="text/javascript" src="#CONFIG_FILE#"></script>
<script type="text/javascript" src="vendor/vendor.js"></script>
<script type="text/javascript" src="modules/default/defaultmodules.js"></script>
<script type="text/javascript" src="js/logger.js"></script>
<script type="text/javascript" src="translations/translations.js"></script>
<script type="text/javascript" src="js/translator.js"></script>
<script type="text/javascript" src="js/class.js"></script>
<script type="text/javascript" src="js/module.js"></script>
<script type="text/javascript" src="js/loader.js"></script>
<script type="text/javascript" src="js/socketclient.js"></script>
<script type="text/javascript" src="js/main.js"></script>
</body>
</html>

135
js/app.js
View File

@@ -4,22 +4,23 @@
* By Michael Teeuw https://michaelteeuw.nl
* 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.
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.
global.version = JSON.parse(fs.readFileSync("package.json", "utf8")).version;
global.version = require(`${__dirname}/../package.json`).version;
Log.log("Starting MagicMirror: v" + global.version);
// global absolute root path
global.root_path = path.resolve(__dirname + "/../");
global.root_path = path.resolve(`${__dirname}/../`);
if (process.env.MM_CONFIG_FILE) {
global.configuration_file = process.env.MM_CONFIG_FILE;
@@ -45,43 +46,41 @@ process.on("uncaughtException", function (err) {
*
* @class
*/
var App = function () {
var nodeHelpers = [];
function App() {
let nodeHelpers = [];
let httpServer;
/**
* 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.
*
* @param {Function} callback Function to be called after loading the config
*/
var loadConfig = function (callback) {
function loadConfig(callback) {
Log.log("Loading config ...");
var defaults = require(__dirname + "/defaults.js");
const defaults = require(`${__dirname}/defaults`);
// For this check proposed to TestSuite
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
var configFilename = path.resolve(global.root_path + "/config/config.js");
if (typeof global.configuration_file !== "undefined") {
configFilename = path.resolve(global.configuration_file);
}
const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`);
try {
fs.accessSync(configFilename, fs.F_OK);
var c = require(configFilename);
const c = require(configFilename);
checkDeprecatedOptions(c);
var config = Object.assign(defaults, c);
const config = Object.assign(defaults, c);
callback(config);
} catch (e) {
if (e.code === "ENOENT") {
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) {
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 {
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);
}
};
}
/**
* Checks the config for deprecated options and throws a warning in the logs
@@ -89,21 +88,15 @@ var App = function () {
*
* @param {object} userConfig The user config
*/
var checkDeprecatedOptions = function (userConfig) {
var deprecated = require(global.root_path + "/js/deprecated.js");
var deprecatedOptions = deprecated.configs;
function checkDeprecatedOptions(userConfig) {
const deprecated = require(`${global.root_path}/js/deprecated`);
const deprecatedOptions = deprecated.configs;
var usedDeprecated = [];
deprecatedOptions.forEach(function (option) {
if (userConfig.hasOwnProperty(option)) {
usedDeprecated.push(option);
}
});
const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option));
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.
@@ -111,35 +104,35 @@ var App = function () {
* @param {string} module The name of the module (including subpath).
* @param {Function} callback Function to be called after loading
*/
var loadModule = function (module, callback) {
var elements = module.split("/");
var moduleName = elements[elements.length - 1];
var moduleFolder = __dirname + "/../modules/" + module;
function loadModule(module, callback) {
const elements = module.split("/");
const moduleName = elements[elements.length - 1];
let moduleFolder = `${__dirname}/../modules/${module}`;
if (defaultModules.indexOf(moduleName) !== -1) {
moduleFolder = __dirname + "/../modules/default/" + module;
if (defaultModules.includes(moduleName)) {
moduleFolder = `${__dirname}/../modules/default/${module}`;
}
var helperPath = moduleFolder + "/node_helper.js";
const helperPath = `${moduleFolder}/node_helper.js`;
var loadModule = true;
let loadHelper = true;
try {
fs.accessSync(helperPath, fs.R_OK);
} catch (e) {
loadModule = false;
Log.log("No helper found for module: " + moduleName + ".");
loadHelper = false;
Log.log(`No helper found for module: ${moduleName}.`);
}
if (loadModule) {
var Module = require(helperPath);
var m = new Module();
if (loadHelper) {
const Module = require(helperPath);
let m = new Module();
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) {
Log.log("Version is ok!");
} else {
Log.log("Version is incorrect. Skip module: '" + moduleName + "'");
Log.warn(`Version is incorrect. Skip module: '${moduleName}'`);
return;
}
}
@@ -152,7 +145,7 @@ var App = function () {
} else {
callback();
}
};
}
/**
* Loads all modules.
@@ -160,12 +153,15 @@ var App = function () {
* @param {Module[]} modules All modules to be loaded
* @param {Function} callback Function to be called after loading
*/
var loadModules = function (modules, callback) {
function loadModules(modules, callback) {
Log.log("Loading module helpers ...");
var loadNextModule = function () {
/**
*
*/
function loadNextModule() {
if (modules.length > 0) {
var nextModule = modules[0];
const nextModule = modules[0];
loadModule(nextModule, function () {
modules = modules.slice(1);
loadNextModule();
@@ -175,26 +171,25 @@ var App = function () {
Log.log("All module helpers loaded.");
callback();
}
};
}
loadNextModule();
};
}
/**
* Compare two semantic version numbers and return the difference.
*
* @param {string} a Version number a.
* @param {string} b Version number b.
*
* @returns {number} A positive number if a is larger than b, a negative
* number if a is smaller and 0 if they are the same
*/
function cmpVersions(a, b) {
var i, diff;
var regExStrip0 = /(\.0+)+$/;
var segmentsA = a.replace(regExStrip0, "").split(".");
var segmentsB = b.replace(regExStrip0, "").split(".");
var l = Math.min(segmentsA.length, segmentsB.length);
let i, diff;
const regExStrip0 = /(\.0+)+$/;
const segmentsA = a.replace(regExStrip0, "").split(".");
const segmentsB = b.replace(regExStrip0, "").split(".");
const l = Math.min(segmentsA.length, segmentsB.length);
for (i = 0; i < l; i++) {
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
@@ -219,21 +214,19 @@ var App = function () {
Log.setLogLevel(config.logLevel);
var modules = [];
let modules = [];
for (var m in config.modules) {
var module = config.modules[m];
if (modules.indexOf(module.module) === -1 && !module.disabled) {
for (const module of config.modules) {
if (!modules.includes(module.module) && !module.disabled) {
modules.push(module.module);
}
}
loadModules(modules, function () {
var server = new Server(config, function (app, io) {
httpServer = new Server(config, function (app, io) {
Log.log("Server started ...");
for (var h in nodeHelpers) {
var nodeHelper = nodeHelpers[h];
for (let nodeHelper of nodeHelpers) {
nodeHelper.setExpressApp(app);
nodeHelper.setSocketIO(io);
nodeHelper.start();
@@ -256,12 +249,12 @@ var App = function () {
* Added to fix #1056
*/
this.stop = function () {
for (var h in nodeHelpers) {
var nodeHelper = nodeHelpers[h];
for (const nodeHelper of nodeHelpers) {
if (typeof nodeHelper.stop === "function") {
nodeHelper.stop();
}
}
httpServer.close();
};
/**
@@ -292,6 +285,6 @@ var App = function () {
this.stop();
process.exit(0);
});
};
}
module.exports = new App();

View File

@@ -11,9 +11,9 @@ const linter = new Linter();
const path = require("path");
const fs = require("fs");
const rootPath = path.resolve(__dirname + "/../");
const Log = require(rootPath + "/js/logger.js");
const Utils = require(rootPath + "/js/utils.js");
const rootPath = path.resolve(`${__dirname}/../`);
const Log = require(`${rootPath}/js/logger.js`);
const Utils = require(`${rootPath}/js/utils.js`);
/**
* Returns a string with path of configuration file.
@@ -23,11 +23,7 @@ const Utils = require(rootPath + "/js/utils.js");
*/
function getConfigFile() {
// FIXME: This function should be in core. Do you want refactor me ;) ?, be good!
let configFileName = path.resolve(rootPath + "/config/config.js");
if (process.env.MM_CONFIG_FILE) {
configFileName = path.resolve(process.env.MM_CONFIG_FILE);
}
return configFileName;
return path.resolve(process.env.MM_CONFIG_FILE || `${rootPath}/config/config.js`);
}
/**
@@ -54,21 +50,24 @@ function checkConfigFile() {
Log.info(Utils.colors.info("Checking file... "), configFileName);
// I'm not sure if all ever is utf-8
fs.readFile(configFileName, "utf-8", function (err, data) {
if (err) {
throw err;
}
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);
});
const configFile = fs.readFileSync(configFileName, "utf-8");
// Explicitly tell linter that he might encounter es6 syntax ("let config = {...}")
const errors = linter.verify(configFile, {
env: {
es6: true
}
});
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}`);
}
}
}
checkConfigFile();

View File

@@ -8,8 +8,8 @@
* MIT Licensed.
*/
(function () {
var initializing = false;
var fnTest = /xyz/.test(function () {
let initializing = false;
const fnTest = /xyz/.test(function () {
xyz;
})
? /\b_super\b/
@@ -20,27 +20,27 @@
// Create a new Class that inherits from this class
Class.extend = function (prop) {
var _super = this.prototype;
let _super = this.prototype;
// Instantiate a base class (but only create the instance,
// don't run the init constructor)
initializing = true;
var prototype = new this();
const prototype = new this();
initializing = false;
// Make a copy of all prototype properties, to prevent reference issues.
for (var p in prototype) {
for (const p in prototype) {
prototype[p] = cloneObject(prototype[p]);
}
// Copy the properties over onto the new prototype
for (var name in prop) {
for (const name in prop) {
// Check if we're overwriting an existing function
prototype[name] =
typeof prop[name] === "function" && typeof _super[name] === "function" && fnTest.test(prop[name])
? (function (name, fn) {
return function () {
var tmp = this._super;
const tmp = this._super;
// Add a new ._super() method that is the same method
// but on the super-class
@@ -48,7 +48,7 @@
// The method only need to be bound temporarily, so we
// remove it when we're done executing
var ret = fn.apply(this, arguments);
const ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
@@ -84,7 +84,6 @@
* Define the clone method for later use. Helper Method.
*
* @param {object} obj Object to be cloned
*
* @returns {object} the cloned object
*/
function cloneObject(obj) {
@@ -92,8 +91,8 @@ function cloneObject(obj) {
return obj;
}
var temp = obj.constructor(); // give temp the original obj's constructor
for (var key in obj) {
const temp = obj.constructor(); // give temp the original obj's constructor
for (const key in obj) {
temp[key] = cloneObject(obj[key]);
if (key === "lockStrings") {

View File

@@ -6,12 +6,12 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
var address = "localhost";
var port = 8080;
const address = "localhost";
let port = 8080;
if (typeof mmPort !== "undefined") {
port = mmPort;
}
var defaults = {
const defaults = {
address: address,
port: port,
basePath: "/",
@@ -20,6 +20,7 @@ var defaults = {
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
logLevel: ["INFO", "LOG", "WARN", "ERROR"],
timeFormat: 24,
units: "metric",
zoom: 1,
@@ -42,7 +43,7 @@ var defaults = {
module: "helloworld",
position: "middle_center",
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",
classes: "xsmall",
config: {
text: "If you get this message while your config file is already<br>created, your config file probably contains an error.<br>Use a JavaScript linter to validate your file."
text: "If you get this message while your config file is already created,<br>" + "it probably contains an error. To validate your config file run in your MagicMirror directory<br>" + "<pre>npm run config:check</pre>"
}
},
{

View File

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

View File

@@ -2,10 +2,10 @@
const electron = require("electron");
const core = require("./app.js");
const Log = require("./logger.js");
const Log = require("logger");
// 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.
const app = electron.app;
// Module to create native browser window.
@@ -19,14 +19,16 @@ let mainWindow;
*
*/
function createWindow() {
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
var electronOptionsDefaults = {
let electronSwitchesDefaults = ["autoplay-policy", "no-user-gesture-required"];
app.commandLine.appendSwitch(...new Set(electronSwitchesDefaults, config.electronSwitches));
let electronOptionsDefaults = {
width: 800,
height: 600,
x: 0,
y: 0,
darkTheme: true,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
zoomFactor: config.zoom
},
@@ -42,7 +44,7 @@ function createWindow() {
electronOptionsDefaults.autoHideMenuBar = true;
}
var electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
const electronOptions = Object.assign({}, electronOptionsDefaults, config.electronOptions);
// Create the browser window.
mainWindow = new BrowserWindow(electronOptions);
@@ -50,21 +52,31 @@ function createWindow() {
// 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
var prefix;
let prefix;
if (config["tls"] !== null && config["tls"]) {
prefix = "https://";
} else {
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}`);
// Open the DevTools if run with "npm start dev"
if (process.argv.includes("dev")) {
if (process.env.JEST_WORKER_ID !== undefined) {
// if we are running with jest
const devtools = new BrowserWindow(electronOptions);
mainWindow.webContents.setDevToolsWebContents(devtools.webContents);
}
mainWindow.webContents.openDevTools();
}
// simulate mouse move to hide black cursor on start
mainWindow.webContents.on("dom-ready", (event) => {
mainWindow.webContents.sendInputEvent({ type: "mouseMove", x: 0, y: 0 });
});
// Set responders for window events.
mainWindow.on("closed", function () {
mainWindow = null;
@@ -96,7 +108,12 @@ app.on("ready", function () {
// Quit when all windows are closed.
app.on("window-all-closed", function () {
createWindow();
if (process.env.JEST_WORKER_ID !== undefined) {
// if we are running with jest
app.quit();
} else {
createWindow();
}
});
app.on("activate", function () {
@@ -125,7 +142,7 @@ app.on("before-quit", (event) => {
// Start the core application if server is run on localhost
// This starts all node helpers and starts the webserver.
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].indexOf(config.address) > -1) {
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {
core.start(function (c) {
config = c;
});

View File

@@ -6,24 +6,24 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
var Loader = (function () {
const Loader = (function () {
/* Create helper variables */
var loadedModuleFiles = [];
var loadedFiles = [];
var moduleObjects = [];
const loadedModuleFiles = [];
const loadedFiles = [];
const moduleObjects = [];
/* Private Methods */
/**
* Loops thru all modules and requests load for every module.
*/
var loadModules = function () {
var moduleData = getModuleData();
const loadModules = function () {
let moduleData = getModuleData();
var loadNextModule = function () {
const loadNextModule = function () {
if (moduleData.length > 0) {
var nextModule = moduleData[0];
const nextModule = moduleData[0];
loadModule(nextModule, function () {
moduleData = moduleData.slice(1);
loadNextModule();
@@ -46,14 +46,21 @@ var Loader = (function () {
/**
* Loops thru all modules and requests start for every module.
*/
var startModules = function () {
for (var m in moduleObjects) {
var module = moduleObjects[m];
const startModules = function () {
for (const module of moduleObjects) {
module.start();
}
// Notify core of loaded modules.
MM.modulesStarted(moduleObjects);
// Starting modules also hides any modules that have requested to be initially hidden
for (const thisModule of moduleObjects) {
if (thisModule.data.hiddenOnStartup) {
Log.info("Initially hiding " + thisModule.name);
thisModule.hide();
}
}
};
/**
@@ -61,7 +68,7 @@ var Loader = (function () {
*
* @returns {object[]} module data as configured in config
*/
var getAllModules = function () {
const getAllModules = function () {
return config.modules;
};
@@ -70,39 +77,39 @@ var Loader = (function () {
*
* @returns {object[]} Module information.
*/
var getModuleData = function () {
var modules = getAllModules();
var moduleFiles = [];
const getModuleData = function () {
const modules = getAllModules();
const moduleFiles = [];
for (var m in modules) {
var moduleData = modules[m];
var module = moduleData.module;
modules.forEach(function (moduleData, index) {
const module = moduleData.module;
var elements = module.split("/");
var moduleName = elements[elements.length - 1];
var moduleFolder = config.paths.modules + "/" + module;
const elements = module.split("/");
const moduleName = elements[elements.length - 1];
let moduleFolder = config.paths.modules + "/" + module;
if (defaultModules.indexOf(moduleName) !== -1) {
moduleFolder = config.paths.modules + "/default/" + module;
}
if (moduleData.disabled === true) {
continue;
return;
}
moduleFiles.push({
index: m,
identifier: "module_" + m + "_" + module,
index: index,
identifier: "module_" + index + "_" + module,
name: moduleName,
path: moduleFolder + "/",
file: moduleName + ".js",
position: moduleData.position,
hiddenOnStartup: moduleData.hiddenOnStartup,
header: moduleData.header,
configDeepMerge: typeof moduleData.configDeepMerge === "boolean" ? moduleData.configDeepMerge : false,
config: moduleData.config,
classes: typeof moduleData.classes !== "undefined" ? moduleData.classes + " " + module : module
});
}
});
return moduleFiles;
};
@@ -113,11 +120,11 @@ var Loader = (function () {
* @param {object} module Information about the module we want to load.
* @param {Function} callback Function called when done.
*/
var loadModule = function (module, callback) {
var url = module.path + "/" + module.file;
const loadModule = function (module, callback) {
const url = module.path + module.file;
var afterLoad = function () {
var moduleObject = Module.create(module.name);
const afterLoad = function () {
const moduleObject = Module.create(module.name);
if (moduleObject) {
bootstrapModule(module, moduleObject, function () {
callback();
@@ -144,7 +151,7 @@ var Loader = (function () {
* @param {Module} mObj Modules instance.
* @param {Function} callback Function called when done.
*/
var bootstrapModule = function (module, mObj, callback) {
const bootstrapModule = function (module, mObj, callback) {
Log.info("Bootstrapping module: " + module.name);
mObj.setData(module);
@@ -168,13 +175,14 @@ var Loader = (function () {
* @param {string} fileName Path of the file we want to load.
* @param {Function} callback Function called when done.
*/
var loadFile = function (fileName, callback) {
var extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
const loadFile = function (fileName, callback) {
const extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1);
let script, stylesheet;
switch (extension.toLowerCase()) {
case "js":
Log.log("Load script: " + fileName);
var script = document.createElement("script");
script = document.createElement("script");
script.type = "text/javascript";
script.src = fileName;
script.onload = function () {
@@ -193,7 +201,7 @@ var Loader = (function () {
break;
case "css":
Log.log("Load stylesheet: " + fileName);
var stylesheet = document.createElement("link");
stylesheet = document.createElement("link");
stylesheet.rel = "stylesheet";
stylesheet.type = "text/css";
stylesheet.href = fileName;

View File

@@ -9,12 +9,13 @@
*/
(function (root, factory) {
if (typeof exports === "object") {
// add timestamps in front of log messages
require("console-stamp")(console, {
pattern: "yyyy-mm-dd HH:MM:ss.l",
include: ["debug", "log", "info", "warn", "error"]
});
if (process.env.JEST_WORKER_ID === undefined) {
// add timestamps in front of log messages
require("console-stamp")(console, {
pattern: "yyyy-mm-dd HH:MM:ss.l",
include: ["debug", "log", "info", "warn", "error"]
});
}
// Node, CommonJS-like
module.exports = factory(root.config);
} else {
@@ -22,29 +23,57 @@
root.Log = factory(root.config);
}
})(this, function (config) {
const logLevel = {
debug: Function.prototype.bind.call(console.debug, console),
log: Function.prototype.bind.call(console.log, console),
info: Function.prototype.bind.call(console.info, console),
warn: Function.prototype.bind.call(console.warn, console),
error: Function.prototype.bind.call(console.error, console),
group: Function.prototype.bind.call(console.group, console),
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
time: Function.prototype.bind.call(console.time, console),
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
};
let logLevel;
let enableLog;
if (typeof exports === "object") {
// in nodejs and not running with jest
enableLog = process.env.JEST_WORKER_ID === undefined;
} else {
// in browser and not running with jsdom
enableLog = typeof window === "object" && window.name !== "jsdom";
}
logLevel.setLogLevel = function (newLevel) {
if (newLevel) {
Object.keys(logLevel).forEach(function (key, index) {
if (!newLevel.includes(key.toLocaleUpperCase())) {
logLevel[key] = function () {};
}
});
}
};
if (enableLog) {
logLevel = {
debug: Function.prototype.bind.call(console.debug, console),
log: Function.prototype.bind.call(console.log, console),
info: Function.prototype.bind.call(console.info, console),
warn: Function.prototype.bind.call(console.warn, console),
error: Function.prototype.bind.call(console.error, console),
group: Function.prototype.bind.call(console.group, console),
groupCollapsed: Function.prototype.bind.call(console.groupCollapsed, console),
groupEnd: Function.prototype.bind.call(console.groupEnd, console),
time: Function.prototype.bind.call(console.time, console),
timeEnd: Function.prototype.bind.call(console.timeEnd, console),
timeStamp: Function.prototype.bind.call(console.timeStamp, console)
};
logLevel.setLogLevel = function (newLevel) {
if (newLevel) {
Object.keys(logLevel).forEach(function (key, index) {
if (!newLevel.includes(key.toLocaleUpperCase())) {
logLevel[key] = function () {};
}
});
}
};
} else {
logLevel = {
debug: function () {},
log: function () {},
info: function () {},
warn: function () {},
error: function () {},
group: function () {},
groupCollapsed: function () {},
groupEnd: function () {},
time: function () {},
timeEnd: function () {},
timeStamp: function () {}
};
logLevel.setLogLevel = function () {};
}
return logLevel;
});

View File

@@ -6,25 +6,25 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
var MM = (function () {
var modules = [];
const MM = (function () {
let modules = [];
/* Private Methods */
/**
* Create dom objects for all modules that are configured for a specific position.
*/
var createDomObjects = function () {
var domCreationPromises = [];
const createDomObjects = function () {
const domCreationPromises = [];
modules.forEach(function (module) {
if (typeof module.data.position !== "string") {
return;
}
var wrapper = selectWrapper(module.data.position);
const wrapper = selectWrapper(module.data.position);
var dom = document.createElement("div");
const dom = document.createElement("div");
dom.id = module.identifier;
dom.className = module.name;
@@ -35,7 +35,7 @@ var MM = (function () {
dom.opacity = 0;
wrapper.appendChild(dom);
var moduleHeader = document.createElement("header");
const moduleHeader = document.createElement("header");
moduleHeader.innerHTML = module.getHeader();
moduleHeader.className = "module-header";
dom.appendChild(moduleHeader);
@@ -46,11 +46,11 @@ var MM = (function () {
moduleHeader.style.display = "block;";
}
var moduleContent = document.createElement("div");
const moduleContent = document.createElement("div");
moduleContent.className = "module-content";
dom.appendChild(moduleContent);
var domCreationPromise = updateDom(module, 0);
const domCreationPromise = updateDom(module, 0);
domCreationPromises.push(domCreationPromise);
domCreationPromise
.then(function () {
@@ -70,14 +70,13 @@ var MM = (function () {
* Select the wrapper dom object for a specific position.
*
* @param {string} position The name of the position.
*
* @returns {HTMLElement} the wrapper element
*/
var selectWrapper = function (position) {
var classes = position.replace("_", " ");
var parentWrapper = document.getElementsByClassName(classes);
const selectWrapper = function (position) {
const classes = position.replace("_", " ");
const parentWrapper = document.getElementsByClassName(classes);
if (parentWrapper.length > 0) {
var wrapper = parentWrapper[0].getElementsByClassName("container");
const wrapper = parentWrapper[0].getElementsByClassName("container");
if (wrapper.length > 0) {
return wrapper[0];
}
@@ -92,9 +91,9 @@ var MM = (function () {
* @param {Module} sender The module that sent the notification.
* @param {Module} [sendTo] The (optional) module to send the notification to.
*/
var sendNotification = function (notification, payload, sender, sendTo) {
for (var m in modules) {
var module = modules[m];
const sendNotification = function (notification, payload, sender, sendTo) {
for (const m in modules) {
const module = modules[m];
if (module !== sender && (!sendTo || module === sendTo)) {
module.notificationReceived(notification, payload, sender);
}
@@ -106,13 +105,12 @@ var MM = (function () {
*
* @param {Module} module The module that needs an update.
* @param {number} [speed] The (optional) number of microseconds for the animation.
*
* @returns {Promise} Resolved when the dom is fully updated.
*/
var updateDom = function (module, speed) {
const updateDom = function (module, speed) {
return new Promise(function (resolve) {
var newContentPromise = module.getDom();
var newHeader = module.getHeader();
const newHeader = module.getHeader();
let newContentPromise = module.getDom();
if (!(newContentPromise instanceof Promise)) {
// convert to a promise if not already one to avoid if/else's everywhere
@@ -121,7 +119,7 @@ var MM = (function () {
newContentPromise
.then(function (newContent) {
var updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
const updatePromise = updateDomWithContent(module, speed, newHeader, newContent);
updatePromise.then(resolve).catch(Log.error);
})
@@ -136,10 +134,9 @@ var MM = (function () {
* @param {number} [speed] The (optional) number of microseconds for the animation.
* @param {string} newHeader The new header that is generated.
* @param {HTMLElement} newContent The new content that is generated.
*
* @returns {Promise} Resolved when the module dom has been updated.
*/
var updateDomWithContent = function (module, speed, newHeader, newContent) {
const updateDomWithContent = function (module, speed, newHeader, newContent) {
return new Promise(function (resolve) {
if (module.hidden || !speed) {
updateModuleContent(module, newHeader, newContent);
@@ -174,26 +171,25 @@ var MM = (function () {
* @param {Module} module The module to check.
* @param {string} newHeader The new header that is generated.
* @param {HTMLElement} newContent The new content that is generated.
*
* @returns {boolean} True if the module need an update, false otherwise
*/
var moduleNeedsUpdate = function (module, newHeader, newContent) {
var moduleWrapper = document.getElementById(module.identifier);
const moduleNeedsUpdate = function (module, newHeader, newContent) {
const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper === null) {
return false;
}
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
var headerNeedsUpdate = false;
var contentNeedsUpdate = false;
let headerNeedsUpdate = false;
let contentNeedsUpdate;
if (headerWrapper.length > 0) {
headerNeedsUpdate = newHeader !== headerWrapper[0].innerHTML;
}
var tempContentWrapper = document.createElement("div");
const tempContentWrapper = document.createElement("div");
tempContentWrapper.appendChild(newContent);
contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML;
@@ -207,13 +203,13 @@ var MM = (function () {
* @param {string} newHeader The new header that is generated.
* @param {HTMLElement} newContent The new content that is generated.
*/
var updateModuleContent = function (module, newHeader, newContent) {
var moduleWrapper = document.getElementById(module.identifier);
const updateModuleContent = function (module, newHeader, newContent) {
const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper === null) {
return;
}
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");
const headerWrapper = moduleWrapper.getElementsByClassName("module-header");
const contentWrapper = moduleWrapper.getElementsByClassName("module-content");
contentWrapper[0].innerHTML = "";
contentWrapper[0].appendChild(newContent);
@@ -234,7 +230,7 @@ var MM = (function () {
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the hide method.
*/
var hideModule = function (module, speed, callback, options) {
const hideModule = function (module, speed, callback, options) {
options = options || {};
// set lockString if set in options.
@@ -245,7 +241,7 @@ var MM = (function () {
}
}
var moduleWrapper = document.getElementById(module.identifier);
const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) {
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
moduleWrapper.style.opacity = 0;
@@ -280,12 +276,12 @@ var MM = (function () {
* @param {Function} callback Called when the animation is done.
* @param {object} [options] Optional settings for the show method.
*/
var showModule = function (module, speed, callback, options) {
const showModule = function (module, speed, callback, options) {
options = options || {};
// remove lockString if set in options.
if (options.lockString) {
var index = module.lockStrings.indexOf(options.lockString);
const index = module.lockStrings.indexOf(options.lockString);
if (index !== -1) {
module.lockStrings.splice(index, 1);
}
@@ -295,6 +291,9 @@ var MM = (function () {
// Otherwise cancel show action.
if (module.lockStrings.length !== 0 && options.force !== true) {
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;
}
@@ -306,7 +305,7 @@ var MM = (function () {
module.lockStrings = [];
}
var moduleWrapper = document.getElementById(module.identifier);
const moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper !== null) {
moduleWrapper.style.transition = "opacity " + speed / 1000 + "s";
// Restore the position. See hideModule() for more info.
@@ -315,7 +314,7 @@ var MM = (function () {
updateWrapperStates();
// Waiting for DOM-changes done in updateWrapperStates before we can start the animation.
var dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
const dummy = moduleWrapper.parentElement.parentElement.offsetHeight;
moduleWrapper.style.opacity = 1;
clearTimeout(module.showHideTimer);
@@ -343,14 +342,14 @@ var MM = (function () {
* an ugly top margin. By using this function, the top bar will be hidden if the
* update notification is not visible.
*/
var updateWrapperStates = function () {
var positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
const updateWrapperStates = function () {
const positions = ["top_bar", "top_left", "top_center", "top_right", "upper_third", "middle_center", "lower_third", "bottom_left", "bottom_center", "bottom_right", "bottom_bar", "fullscreen_above", "fullscreen_below"];
positions.forEach(function (position) {
var wrapper = selectWrapper(position);
var moduleWrappers = wrapper.getElementsByClassName("module");
const wrapper = selectWrapper(position);
const moduleWrappers = wrapper.getElementsByClassName("module");
var showWrapper = false;
let showWrapper = false;
Array.prototype.forEach.call(moduleWrappers, function (moduleWrapper) {
if (moduleWrapper.style.position === "" || moduleWrapper.style.position === "static") {
showWrapper = true;
@@ -364,7 +363,7 @@ var MM = (function () {
/**
* Loads the core config and combines it with the system defaults.
*/
var loadConfig = function () {
const loadConfig = function () {
// FIXME: Think about how to pass config around without breaking tests
/* eslint-disable */
if (typeof config === "undefined") {
@@ -382,15 +381,14 @@ var MM = (function () {
*
* @param {Module[]} modules Array of modules.
*/
var setSelectionMethodsForModules = function (modules) {
const setSelectionMethodsForModules = function (modules) {
/**
* Filter modules with the specified classes.
*
* @param {string|string[]} className one or multiple classnames (array or space divided).
*
* @returns {Module[]} Filtered collection of modules.
*/
var withClass = function (className) {
const withClass = function (className) {
return modulesByClass(className, true);
};
@@ -398,10 +396,9 @@ var MM = (function () {
* Filter modules without the specified classes.
*
* @param {string|string[]} className one or multiple classnames (array or space divided).
*
* @returns {Module[]} Filtered collection of modules.
*/
var exceptWithClass = function (className) {
const exceptWithClass = function (className) {
return modulesByClass(className, false);
};
@@ -410,20 +407,18 @@ var MM = (function () {
*
* @param {string|string[]} className one or multiple classnames (array or space divided).
* @param {boolean} include if the filter should include or exclude the modules with the specific classes.
*
* @returns {Module[]} Filtered collection of modules.
*/
var modulesByClass = function (className, include) {
var searchClasses = className;
const modulesByClass = function (className, include) {
let searchClasses = className;
if (typeof className === "string") {
searchClasses = className.split(" ");
}
var newModules = modules.filter(function (module) {
var classes = module.data.classes.toLowerCase().split(" ");
const newModules = modules.filter(function (module) {
const classes = module.data.classes.toLowerCase().split(" ");
for (var c in searchClasses) {
var searchClass = searchClasses[c];
for (const searchClass of searchClasses) {
if (classes.indexOf(searchClass.toLowerCase()) !== -1) {
return include;
}
@@ -440,11 +435,10 @@ var MM = (function () {
* Removes a module instance from the collection.
*
* @param {object} module The module instance to remove from the collection.
*
* @returns {Module[]} Filtered collection of modules.
*/
var exceptModule = function (module) {
var newModules = modules.filter(function (mod) {
const exceptModule = function (module) {
const newModules = modules.filter(function (mod) {
return mod.identifier !== module.identifier;
});
@@ -457,7 +451,7 @@ var MM = (function () {
*
* @param {Function} callback The function to execute with the module as an argument.
*/
var enumerate = function (callback) {
const enumerate = function (callback) {
modules.map(function (module) {
callback(module);
});
@@ -547,6 +541,11 @@ var MM = (function () {
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.
updateDom(module, speed);
},
@@ -597,11 +596,11 @@ if (typeof Object.assign !== "function") {
if (target === undefined || target === null) {
throw new TypeError("Cannot convert undefined or null to object");
}
var output = Object(target);
for (var index = 1; index < arguments.length; index++) {
var source = arguments[index];
const output = Object(target);
for (let index = 1; index < arguments.length; index++) {
const source = arguments[index];
if (source !== undefined && source !== null) {
for (var nextKey in source) {
for (const nextKey in source) {
if (source.hasOwnProperty(nextKey)) {
output[nextKey] = source[nextKey];
}

View File

@@ -6,9 +6,8 @@
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*
*/
var Module = Class.extend({
const Module = Class.extend({
/*********************************************************
* All methods (and properties) below can be subclassed. *
*********************************************************/
@@ -82,16 +81,15 @@ var Module = Class.extend({
* @returns {HTMLElement|Promise} The dom or a promise with the dom to display.
*/
getDom: function () {
var self = this;
return new Promise(function (resolve) {
var div = document.createElement("div");
var template = self.getTemplate();
var templateData = self.getTemplateData();
return new Promise((resolve) => {
const div = document.createElement("div");
const template = this.getTemplate();
const templateData = this.getTemplateData();
// Check to see if we need to render a template string or a file.
if (/^.*((\.html)|(\.njk))$/.test(template)) {
// the template is a filename
self.nunjucksEnvironment().render(template, templateData, function (err, res) {
this.nunjucksEnvironment().render(template, templateData, function (err, res) {
if (err) {
Log.error(err);
}
@@ -102,7 +100,7 @@ var Module = Class.extend({
});
} else {
// the template is a template string.
div.innerHTML = self.nunjucksEnvironment().renderString(template, templateData);
div.innerHTML = this.nunjucksEnvironment().renderString(template, templateData);
resolve(div);
}
@@ -168,15 +166,13 @@ var Module = Class.extend({
return this._nunjucksEnvironment;
}
var self = this;
this._nunjucksEnvironment = new nunjucks.Environment(new nunjucks.WebLoader(this.file(""), { async: true }), {
trimBlocks: true,
lstripBlocks: true
});
this._nunjucksEnvironment.addFilter("translate", function (str, variables) {
return self.translate(str, variables);
this._nunjucksEnvironment.addFilter("translate", (str, variables) => {
return nunjucks.runtime.markSafe(this.translate(str, variables));
});
return this._nunjucksEnvironment;
@@ -192,14 +188,14 @@ var Module = Class.extend({
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
},
/*
/**
* Called when the module is hidden.
*/
suspend: function () {
Log.log(this.name + " is suspended.");
},
/*
/**
* Called when the module is shown.
*/
resume: function () {
@@ -213,7 +209,7 @@ var Module = Class.extend({
/**
* Set the module data.
*
* @param {Module} data The module data
* @param {object} data The module data
*/
setData: function (data) {
this.data = data;
@@ -245,9 +241,8 @@ var Module = Class.extend({
this._socket = new MMSocket(this.name);
}
var self = this;
this._socket.setNotificationCallback(function (notification, payload) {
self.socketNotificationReceived(notification, payload);
this._socket.setNotificationCallback((notification, payload) => {
this.socketNotificationReceived(notification, payload);
});
return this._socket;
@@ -288,13 +283,12 @@ var Module = Class.extend({
* @param {Function} callback Function called when done.
*/
loadDependencies: function (funcName, callback) {
var self = this;
var dependencies = this[funcName]();
let dependencies = this[funcName]();
var loadNextDependency = function () {
const loadNextDependency = () => {
if (dependencies.length > 0) {
var nextDependency = dependencies[0];
Loader.loadFile(nextDependency, self, function () {
const nextDependency = dependencies[0];
Loader.loadFile(nextDependency, this, () => {
dependencies = dependencies.slice(1);
loadNextDependency();
});
@@ -311,33 +305,33 @@ var Module = Class.extend({
*
* @param {Function} callback Function called when done.
*/
loadTranslations: function (callback) {
var self = this;
var translations = this.getTranslations();
var lang = config.language.toLowerCase();
loadTranslations(callback) {
const translations = this.getTranslations() || {};
const language = config.language.toLowerCase();
// The variable `first` will contain the first
// defined translation after the following line.
for (var first in translations) {
break;
}
const languages = Object.keys(translations);
const fallbackLanguage = languages[0];
if (translations) {
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 {
if (languages.length === 0) {
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();
}
});
},
/**
@@ -400,12 +394,11 @@ var Module = Class.extend({
callback = callback || function () {};
options = options || {};
var self = this;
MM.hideModule(
self,
this,
speed,
function () {
self.suspend();
() => {
this.suspend();
callback();
},
options
@@ -428,12 +421,11 @@ var Module = Class.extend({
callback = callback || function () {};
options = options || {};
var self = this;
MM.showModule(
this,
speed,
function () {
self.resume();
() => {
this.resume();
callback();
},
options
@@ -465,9 +457,9 @@ var Module = Class.extend({
* @returns {object} the merged config
*/
function configMerge(result) {
var stack = Array.prototype.slice.call(arguments, 1);
var item;
var key;
const stack = Array.prototype.slice.call(arguments, 1);
let item, key;
while (stack.length) {
item = stack.shift();
for (key in item) {
@@ -495,19 +487,19 @@ Module.create = function (name) {
return;
}
var moduleDefinition = Module.definitions[name];
var clonedDefinition = cloneObject(moduleDefinition);
const moduleDefinition = Module.definitions[name];
const clonedDefinition = cloneObject(moduleDefinition);
// Note that we clone the definition. Otherwise the objects are shared, which gives problems.
var ModuleClass = Module.extend(clonedDefinition);
const ModuleClass = Module.extend(clonedDefinition);
return new ModuleClass();
};
Module.register = function (name, moduleDefinition) {
if (moduleDefinition.requiresVersion) {
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.version);
if (cmpVersions(window.version, moduleDefinition.requiresVersion) >= 0) {
Log.log("Check MagicMirror version for module '" + name + "' - Minimum version: " + moduleDefinition.requiresVersion + " - Current version: " + window.mmVersion);
if (cmpVersions(window.mmVersion, moduleDefinition.requiresVersion) >= 0) {
Log.log("Version is ok!");
} else {
Log.warn("Version is incorrect. Skip module: '" + name + "'");
@@ -518,6 +510,8 @@ Module.register = function (name, moduleDefinition) {
Module.definitions[name] = moduleDefinition;
};
window.Module = Module;
/**
* Compare two semantic version numbers and return the difference.
*
@@ -527,14 +521,13 @@ Module.register = function (name, moduleDefinition) {
* number if a is smaller and 0 if they are the same
*/
function cmpVersions(a, b) {
var i, diff;
var regExStrip0 = /(\.0+)+$/;
var segmentsA = a.replace(regExStrip0, "").split(".");
var segmentsB = b.replace(regExStrip0, "").split(".");
var l = Math.min(segmentsA.length, segmentsB.length);
const regExStrip0 = /(\.0+)+$/;
const segmentsA = a.replace(regExStrip0, "").split(".");
const segmentsB = b.replace(regExStrip0, "").split(".");
const l = Math.min(segmentsA.length, segmentsB.length);
for (i = 0; i < l; i++) {
diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
for (let i = 0; i < l; i++) {
let diff = parseInt(segmentsA[i], 10) - parseInt(segmentsB[i], 10);
if (diff) {
return diff;
}

View File

@@ -5,21 +5,21 @@
* MIT Licensed.
*/
const Class = require("./class.js");
const Log = require("./logger.js");
const Log = require("logger");
const express = require("express");
var NodeHelper = Class.extend({
init: function () {
const NodeHelper = Class.extend({
init() {
Log.log("Initializing new module helper ...");
},
loaded: function (callback) {
Log.log("Module helper loaded: " + this.name);
loaded(callback) {
Log.log(`Module helper loaded: ${this.name}`);
callback();
},
start: function () {
Log.log("Starting module helper: " + this.name);
start() {
Log.log(`Starting module helper: ${this.name}`);
},
/* stop()
@@ -28,8 +28,8 @@ var NodeHelper = Class.extend({
* gracefully exit the module.
*
*/
stop: function () {
Log.log("Stopping module helper: " + this.name);
stop() {
Log.log(`Stopping module helper: ${this.name}`);
},
/* socketNotificationReceived(notification, payload)
@@ -38,8 +38,8 @@ var NodeHelper = Class.extend({
* argument notification string - The identifier of the notification.
* argument payload mixed - The payload of the notification.
*/
socketNotificationReceived: function (notification, payload) {
Log.log(this.name + " received a socket notification: " + notification + " - Payload: " + payload);
socketNotificationReceived(notification, payload) {
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
},
/* setName(name)
@@ -47,7 +47,7 @@ var NodeHelper = Class.extend({
*
* argument name string - Module name.
*/
setName: function (name) {
setName(name) {
this.name = name;
},
@@ -56,7 +56,7 @@ var NodeHelper = Class.extend({
*
* argument path string - Module path.
*/
setPath: function (path) {
setPath(path) {
this.path = path;
},
@@ -66,7 +66,7 @@ var NodeHelper = Class.extend({
* argument notification string - The identifier 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);
},
@@ -76,11 +76,10 @@ var NodeHelper = Class.extend({
*
* argument app Express app - The Express app object.
*/
setExpressApp: function (app) {
setExpressApp(app) {
this.expressApp = app;
var publicPath = this.path + "/public";
app.use("/" + this.name, express.static(publicPath));
app.use(`/${this.name}`, express.static(`${this.path}/public`));
},
/* setSocketIO(io)
@@ -89,38 +88,59 @@ var NodeHelper = Class.extend({
*
* argument io Socket.io - The Socket io object.
*/
setSocketIO: function (io) {
var self = this;
self.io = io;
setSocketIO(io) {
this.io = io;
Log.log("Connecting socket for: " + this.name);
var namespace = this.name;
io.of(namespace).on("connection", function (socket) {
Log.log(`Connecting socket for: ${this.name}`);
io.of(this.name).on("connection", (socket) => {
// add a catch all event.
var onevent = socket.onevent;
const onevent = socket.onevent;
socket.onevent = function (packet) {
var args = packet.data || [];
const args = packet.data || [];
onevent.call(this, packet); // original call
packet.data = ["*"].concat(args);
onevent.call(this, packet); // additional call to catch-all
};
// register catch all.
socket.on("*", function (notification, payload) {
socket.on("*", (notification, payload) => {
if (notification !== "*") {
//Log.log('received message in namespace: ' + namespace);
self.socketNotificationReceived(notification, payload);
this.socketNotificationReceived(notification, payload);
}
});
});
}
});
NodeHelper.checkFetchStatus = function (response) {
// response.status >= 200 && response.status < 300
if (response.ok) {
return response;
} else {
throw Error(response.statusText);
}
};
/**
* Look at the specified error and return an appropriate error type, that
* can be translated to a detailed error message
*
* @param {Error} error the error from fetching something
* @returns {string} the string of the detailed error message in the translations
*/
NodeHelper.checkFetchError = function (error) {
let error_type = "MODULE_ERROR_UNSPECIFIED";
if (error.code === "EAI_AGAIN") {
error_type = "MODULE_ERROR_NO_CONNECTION";
} else if (error.message === "Unauthorized") {
error_type = "MODULE_ERROR_UNAUTHORIZED";
}
return error_type;
};
NodeHelper.create = function (moduleDefinition) {
return NodeHelper.extend(moduleDefinition);
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = NodeHelper;
}
module.exports = NodeHelper;

View File

@@ -4,25 +4,30 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
var express = require("express");
var app = require("express")();
var path = require("path");
var ipfilter = require("express-ipfilter").IpFilter;
var fs = require("fs");
var helmet = require("helmet");
const express = require("express");
const app = require("express")();
const path = require("path");
const ipfilter = require("express-ipfilter").IpFilter;
const fs = require("fs");
const helmet = require("helmet");
var Log = require("./logger.js");
var Utils = require("./utils.js");
const Log = require("logger");
const Utils = require("./utils.js");
var Server = function (config, callback) {
var port = config.port;
if (process.env.MM_PORT) {
port = process.env.MM_PORT;
}
/**
* Server
*
* @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;
const serverSockets = new Set();
var server = null;
let server = null;
if (config.useHttps) {
var options = {
const options = {
key: fs.readFileSync(config.httpsPrivateKey),
cert: fs.readFileSync(config.httpsCertificate)
};
@@ -30,18 +35,31 @@ var Server = function (config, callback) {
} else {
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 + " ... ");
server.on("connection", (socket) => {
serverSockets.add(socket);
socket.on("close", () => {
serverSockets.delete(socket);
});
});
server.listen(port, config.address ? config.address : "localhost");
Log.log(`Starting server on port ${port} ... `);
server.listen(port, config.address || "localhost");
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"));
}
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) {
return next();
}
@@ -52,10 +70,9 @@ var Server = function (config, callback) {
app.use(helmet({ contentSecurityPolicy: false }));
app.use("/js", express.static(__dirname));
var directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
var directory;
for (var i in directories) {
directory = directories[i];
const directories = ["/config", "/css", "/fonts", "/modules", "/vendor", "/translations", "/tests/configs"];
for (const directory of directories) {
app.use(directory, express.static(path.resolve(global.root_path + directory)));
}
@@ -68,10 +85,10 @@ var Server = function (config, callback) {
});
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);
var configFile = "config/config.js";
let configFile = "config/config.js";
if (typeof global.configuration_file !== "undefined") {
configFile = global.configuration_file;
}
@@ -83,6 +100,13 @@ var Server = function (config, callback) {
if (typeof callback === "function") {
callback(app, io);
}
};
this.close = function () {
for (const socket of serverSockets.values()) {
socket.destroy();
}
server.close();
};
}
module.exports = Server;

View File

@@ -6,49 +6,48 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
var MMSocket = function (moduleName) {
var self = this;
const MMSocket = function (moduleName) {
if (typeof moduleName !== "string") {
throw new Error("Please set the module name for the MMSocket.");
}
self.moduleName = moduleName;
this.moduleName = moduleName;
// Private Methods
var base = "/";
let base = "/";
if (typeof config !== "undefined" && typeof config.basePath !== "undefined") {
base = config.basePath;
}
self.socket = io("/" + self.moduleName, {
this.socket = io("/" + this.moduleName, {
path: base + "socket.io"
});
var notificationCallback = function () {};
var onevent = self.socket.onevent;
self.socket.onevent = function (packet) {
var args = packet.data || [];
onevent.call(this, packet); // original call
let notificationCallback = function () {};
const onevent = this.socket.onevent;
this.socket.onevent = (packet) => {
const args = packet.data || [];
onevent.call(this.socket, packet); // original call
packet.data = ["*"].concat(args);
onevent.call(this, packet); // additional call to catch-all
onevent.call(this.socket, packet); // additional call to catch-all
};
// register catch all.
self.socket.on("*", function (notification, payload) {
this.socket.on("*", (notification, payload) => {
if (notification !== "*") {
notificationCallback(notification, payload);
}
});
// Public Methods
this.setNotificationCallback = function (callback) {
this.setNotificationCallback = (callback) => {
notificationCallback = callback;
};
this.sendNotification = function (notification, payload) {
this.sendNotification = (notification, payload) => {
if (typeof payload === "undefined") {
payload = {};
}
self.socket.emit(notification, payload);
this.socket.emit(notification, payload);
};
};

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
type ModuleProperties = {
defaults?: object,
start?(): void,
getHeader?(): string,
getTemplate?(): string,
getTemplateData?(): object,
notificationReceived?(notification: string, payload: any, sender: object): void,
socketNotificationReceived?(notification: string, payload: any): void,
suspend?(): void,
resume?(): void,
getDom?(): HTMLElement,
getStyles?(): string[],
[key: string]: any,
defaults?: object;
start?(): void;
getHeader?(): string;
getTemplate?(): string;
getTemplateData?(): object;
notificationReceived?(notification: string, payload: any, sender: object): void;
socketNotificationReceived?(notification: string, payload: any): void;
suspend?(): void;
resume?(): void;
getDom?(): HTMLElement;
getStyles?(): string[];
[key: string]: any;
};
export declare const Module: {
@@ -18,14 +18,14 @@ export declare const Module: {
};
export declare const Log: {
info(message?: any, ...optionalParams: any[]): void,
log(message?: any, ...optionalParams: any[]): void,
error(message?: any, ...optionalParams: any[]): void,
warn(message?: any, ...optionalParams: any[]): void,
group(groupTitle?: string, ...optionalParams: any[]): void,
groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void,
groupEnd(): void,
time(timerName?: string): void,
timeEnd(timerName?: string): void,
timeStamp(timerName?: string): void,
};
info(message?: any, ...optionalParams: any[]): void;
log(message?: any, ...optionalParams: any[]): void;
error(message?: any, ...optionalParams: any[]): void;
warn(message?: any, ...optionalParams: any[]): void;
group(groupTitle?: string, ...optionalParams: any[]): void;
groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void;
groupEnd(): void;
time(timerName?: string): void;
timeEnd(timerName?: string): void;
timeStamp(timerName?: string): void;
};

View File

@@ -79,7 +79,7 @@ Module.register("alert", {
//If module already has an open alert close it
if (this.alerts[sender.name]) {
this.hide_alert(sender);
this.hide_alert(sender, false);
}
//Display title and message only if they are provided in notification parameters
@@ -114,10 +114,10 @@ Module.register("alert", {
}, params.timer);
}
},
hide_alert: function (sender) {
hide_alert: function (sender, close = true) {
//Dismiss alert and remove from this.alerts
if (this.alerts[sender.name]) {
this.alerts[sender.name].dismiss();
this.alerts[sender.name].dismiss(close);
this.alerts[sender.name] = null;
//Remove overlay
const overlay = document.getElementById("overlay");

View File

@@ -6,7 +6,6 @@
line-height: 1.4;
margin-bottom: 10px;
z-index: 1;
color: black;
font-size: 70%;
position: relative;
display: table;
@@ -15,17 +14,17 @@
border-width: 1px;
border-radius: 5px;
border-style: solid;
border-color: #666;
border-color: var(--color-text-dimmed);
}
.ns-alert {
border-style: solid;
border-color: #fff;
border-color: var(--color-text-bright);
padding: 17px;
line-height: 1.4;
margin-bottom: 10px;
z-index: 3;
color: white;
color: var(--color-text-bright);
font-size: 70%;
position: fixed;
text-align: center;

View File

@@ -122,8 +122,10 @@
/**
* Dismiss the notification
*
* @param {boolean} [close] call the onClose callback at the end
*/
NotificationFx.prototype.dismiss = function () {
NotificationFx.prototype.dismiss = function (close = true) {
this.active = false;
clearTimeout(this.dismissttl);
this.ntf.classList.remove("ns-show");
@@ -131,7 +133,7 @@
this.ntf.classList.add("ns-hide");
// callback
this.options.onClose();
if (close) this.options.onClose();
}, 25);
// after animation ends remove ntf from the DOM

View File

@@ -1,13 +1,14 @@
.calendar .symbol {
display: flex;
flex-direction: row;
justify-content: flex-end;
padding-left: 0;
padding-right: 10px;
font-size: 80%;
vertical-align: top;
font-size: var(--font-size-small);
}
.calendar .symbol span {
display: inline-block;
transform: translate(0, 2px);
padding-top: 4px;
}
.calendar .title {

View File

@@ -36,6 +36,7 @@ Module.register("calendar", {
fadePoint: 0.25, // Start on 1/4th of the list.
hidePrivate: false,
hideOngoing: false,
hideTime: false,
colored: false,
coloredSymbolOnly: false,
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: [],
sliceMultiDayEvents: false,
broadcastPastEvents: false,
nextDaysRelative: false
nextDaysRelative: false,
selfSignedCert: false
},
requiresVersion: "2.1.0",
@@ -75,14 +77,14 @@ Module.register("calendar", {
// Define required translations.
getTranslations: function () {
// 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.
return false;
},
// Override start method.
start: function () {
Log.log("Starting module: " + this.name);
Log.info("Starting module: " + this.name);
// Set locale.
moment.updateLocale(config.language, this.getLocaleSpecification(config.timeFormat));
@@ -93,15 +95,16 @@ Module.register("calendar", {
// indicate no data available yet
this.loaded = false;
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
this.config.calendars.forEach((calendar) => {
calendar.url = calendar.url.replace("webcal://", "http://");
var calendarConfig = {
const calendarConfig = {
maximumEntries: calendar.maximumEntries,
maximumNumberOfDays: calendar.maximumNumberOfDays,
broadcastPastEvents: calendar.broadcastPastEvents
broadcastPastEvents: calendar.broadcastPastEvents,
selfSignedCert: calendar.selfSignedCert
};
if (calendar.symbolClass === "undefined" || calendar.symbolClass === null) {
calendarConfig.symbolClass = "";
}
@@ -125,7 +128,7 @@ Module.register("calendar", {
// tell helper to start a fetcher for this calendar
// fetcher till cycle
this.addCalendar(calendar.url, calendar.auth, calendarConfig);
}
});
},
// Override socket notification handler.
@@ -137,17 +140,17 @@ Module.register("calendar", {
if (notification === "CALENDAR_EVENTS") {
if (this.hasCalendarURL(payload.url)) {
this.calendarData[payload.url] = payload.events;
this.error = null;
this.loaded = true;
if (this.config.broadcastEvents) {
this.broadcastEvents();
}
}
} else if (notification === "FETCH_ERROR") {
Log.error("Calendar Error. Could not fetch calendar: " + payload.url);
} else if (notification === "CALENDAR_ERROR") {
let error_message = this.translate(payload.error_type);
this.error = this.translate("MODULE_CONFIG_ERROR", { MODULE_NAME: this.name, ERROR: error_message });
this.loaded = true;
} else if (notification === "INCORRECT_URL") {
Log.error("Calendar Error. Incorrect url: " + payload.url);
}
this.updateDom(this.config.animationSpeed);
@@ -161,47 +164,53 @@ Module.register("calendar", {
const oneHour = oneMinute * 60;
const oneDay = oneHour * 24;
var events = this.createEventList();
var wrapper = document.createElement("table");
const events = this.createEventList();
const wrapper = document.createElement("table");
wrapper.className = this.config.tableClass;
if (this.error) {
wrapper.innerHTML = this.error;
wrapper.className = this.config.tableClass + " dimmed";
return wrapper;
}
if (events.length === 0) {
wrapper.innerHTML = this.loaded ? this.translate("EMPTY") : this.translate("LOADING");
wrapper.className = this.config.tableClass + " dimmed";
return wrapper;
}
let currentFadeStep = 0;
let startFade;
let fadeSteps;
if (this.config.fade && this.config.fadePoint < 1) {
if (this.config.fadePoint < 0) {
this.config.fadePoint = 0;
}
var startFade = events.length * this.config.fadePoint;
var fadeSteps = events.length - startFade;
startFade = events.length * this.config.fadePoint;
fadeSteps = events.length - startFade;
}
var currentFadeStep = 0;
var lastSeenDate = "";
var ev;
var needle;
let lastSeenDate = "";
for (var e in events) {
var event = events[e];
var dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
events.forEach((event, index) => {
const dateAsString = moment(event.startDate, "x").format(this.config.dateFormat);
if (this.config.timeFormat === "dateheaders") {
if (lastSeenDate !== dateAsString) {
var dateRow = document.createElement("tr");
const dateRow = document.createElement("tr");
dateRow.className = "normal";
var dateCell = document.createElement("td");
const dateCell = document.createElement("td");
dateCell.colSpan = "3";
dateCell.innerHTML = dateAsString;
dateCell.style.paddingTop = "10px";
dateRow.appendChild(dateCell);
wrapper.appendChild(dateRow);
if (e >= startFade) {
if (this.config.fade && index >= startFade) {
//fading
currentFadeStep = e - startFade;
currentFadeStep = index - startFade;
dateRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
}
@@ -209,7 +218,7 @@ Module.register("calendar", {
}
}
var eventWrapper = document.createElement("tr");
const eventWrapper = document.createElement("tr");
if (this.config.colored && !this.config.coloredSymbolOnly) {
eventWrapper.style.cssText = "color:" + this.colorForUrl(event.url);
@@ -217,22 +226,22 @@ Module.register("calendar", {
eventWrapper.className = "normal event";
if (this.config.displaySymbol) {
var symbolWrapper = document.createElement("td");
const symbolWrapper = document.createElement("td");
if (this.config.displaySymbol) {
if (this.config.colored && this.config.coloredSymbolOnly) {
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;
var symbols = this.symbolsForEvent(event);
const symbols = this.symbolsForEvent(event);
// If symbols are displayed and custom symbol is set, replace event symbol
if (this.config.displaySymbol && this.config.customEvents.length > 0) {
for (ev in this.config.customEvents) {
for (let ev in this.config.customEvents) {
if (typeof this.config.customEvents[ev].symbol !== "undefined" && this.config.customEvents[ev].symbol !== "") {
needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
if (needle.test(event.title)) {
symbols[0] = this.config.customEvents[ev].symbol;
break;
@@ -240,31 +249,29 @@ Module.register("calendar", {
}
}
}
for (var i = 0; i < symbols.length; i++) {
var symbol = document.createElement("span");
symbol.className = "fa fa-fw fa-" + symbols[i];
if (i > 0) {
symbols.forEach((s, index) => {
const symbol = document.createElement("span");
symbol.className = "fa fa-fw fa-" + s;
if (index > 0) {
symbol.style.paddingLeft = "5px";
}
symbolWrapper.appendChild(symbol);
}
});
eventWrapper.appendChild(symbolWrapper);
} else if (this.config.timeFormat === "dateheaders") {
var blankCell = document.createElement("td");
const blankCell = document.createElement("td");
blankCell.innerHTML = "&nbsp;&nbsp;&nbsp;";
eventWrapper.appendChild(blankCell);
}
var titleWrapper = document.createElement("td"),
repeatingCountTitle = "";
const titleWrapper = document.createElement("td");
let repeatingCountTitle = "";
if (this.config.displayRepeatingCountTitle && event.firstYear !== undefined) {
repeatingCountTitle = this.countTitleForUrl(event.url);
if (repeatingCountTitle !== "") {
var thisYear = new Date(parseInt(event.startDate)).getFullYear(),
const thisYear = new Date(parseInt(event.startDate)).getFullYear(),
yearDiff = thisYear - event.firstYear;
repeatingCountTitle = ", " + yearDiff + ". " + repeatingCountTitle;
@@ -273,12 +280,15 @@ Module.register("calendar", {
// Color events if custom color is specified
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 !== "") {
needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
let needle = new RegExp(this.config.customEvents[ev].keyword, "gi");
if (needle.test(event.title)) {
eventWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
titleWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
// Respect parameter ColoredSymbolOnly also for custom events
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) {
symbolWrapper.style.cssText = "color:" + this.config.customEvents[ev].color;
}
@@ -290,7 +300,7 @@ Module.register("calendar", {
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) {
titleWrapper.className = "title bright " + titleClass;
@@ -298,28 +308,25 @@ Module.register("calendar", {
titleWrapper.className = "title " + titleClass;
}
var timeWrapper;
if (this.config.timeFormat === "dateheaders") {
if (event.fullDayEvent) {
titleWrapper.colSpan = "2";
titleWrapper.align = "left";
titleWrapper.classList.add("align-left");
} else {
timeWrapper = document.createElement("td");
timeWrapper.className = "time light " + this.timeClassForUrl(event.url);
timeWrapper.align = "left";
const timeWrapper = document.createElement("td");
timeWrapper.className = "time light align-left " + this.timeClassForUrl(event.url);
timeWrapper.style.paddingLeft = "2px";
timeWrapper.innerHTML = moment(event.startDate, "x").format("LT");
eventWrapper.appendChild(timeWrapper);
titleWrapper.align = "right";
titleWrapper.classList.add("align-right");
}
eventWrapper.appendChild(titleWrapper);
} else {
timeWrapper = document.createElement("td");
const timeWrapper = document.createElement("td");
eventWrapper.appendChild(titleWrapper);
var now = new Date();
const now = new Date();
if (this.config.timeFormat === "absolute") {
// Use dateFormat
@@ -363,7 +370,18 @@ Module.register("calendar", {
// Show relative times
if (event.startDate >= now) {
// 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(null, { sameElse: this.config.dateFormat }));
} else {
timeWrapper.innerHTML = this.capFirst(
moment(event.startDate, "x").calendar(null, {
sameDay: "[" + this.translate("TODAY") + "]",
nextDay: "[" + this.translate("TOMORROW") + "]",
nextWeek: "dddd",
sameElse: this.config.dateFormat
})
);
}
if (event.startDate - now < this.config.getRelative * oneHour) {
// If event is within getRelative hours, display 'in xxx' time format or moment.fromNow()
timeWrapper.innerHTML = this.capFirst(moment(event.startDate, "x").fromNow());
@@ -385,22 +403,22 @@ Module.register("calendar", {
wrapper.appendChild(eventWrapper);
// Create fade effect.
if (e >= startFade) {
currentFadeStep = e - startFade;
if (index >= startFade) {
currentFadeStep = index - startFade;
eventWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
}
if (this.config.showLocation) {
if (event.location !== false) {
var locationRow = document.createElement("tr");
const locationRow = document.createElement("tr");
locationRow.className = "normal xsmall light";
if (this.config.displaySymbol) {
var symbolCell = document.createElement("td");
const symbolCell = document.createElement("td");
locationRow.appendChild(symbolCell);
}
var descCell = document.createElement("td");
const descCell = document.createElement("td");
descCell.className = "location";
descCell.colSpan = "2";
descCell.innerHTML = this.titleTransform(event.location, this.config.locationTitleReplace, this.config.wrapLocationEvents, this.config.maxLocationTitleLength, this.config.maxEventTitleLines);
@@ -408,13 +426,13 @@ Module.register("calendar", {
wrapper.appendChild(locationRow);
if (e >= startFade) {
currentFadeStep = e - startFade;
if (index >= startFade) {
currentFadeStep = index - startFade;
locationRow.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep;
}
}
}
}
});
return wrapper;
},
@@ -448,8 +466,7 @@ Module.register("calendar", {
* @returns {boolean} True if the calendar config contains the url, False otherwise
*/
hasCalendarURL: function (url) {
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
for (const calendar of this.config.calendars) {
if (calendar.url === url) {
return true;
}
@@ -464,14 +481,15 @@ Module.register("calendar", {
* @returns {object[]} Array with events.
*/
createEventList: function () {
var events = [];
var today = moment().startOf("day");
var now = new Date();
var future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
for (var c in this.calendarData) {
var calendar = this.calendarData[c];
for (var e in calendar) {
var event = JSON.parse(JSON.stringify(calendar[e])); // clone object
const now = new Date();
const today = moment().startOf("day");
const future = moment().startOf("day").add(this.config.maximumNumberOfDays, "days").toDate();
let events = [];
for (const calendarUrl in this.calendarData) {
const calendar = this.calendarData[calendarUrl];
for (const e in calendar) {
const event = JSON.parse(JSON.stringify(calendar[e])); // clone object
if (event.endDate < now) {
continue;
@@ -490,19 +508,19 @@ Module.register("calendar", {
if (this.listContainsEvent(events, event)) {
continue;
}
event.url = c;
event.url = calendarUrl;
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,
* 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) {
var splitEvents = [];
var midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
var count = 1;
const splitEvents = [];
let midnight = moment(event.startDate, "x").clone().startOf("day").add(1, "day").format("x");
let count = 1;
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.endDate = midnight;
thisEvent.title += " (" + count + "/" + maxCount + ")";
@@ -516,9 +534,9 @@ Module.register("calendar", {
event.title += " (" + count + "/" + maxCount + ")";
splitEvents.push(event);
for (event of splitEvents) {
if (event.endDate > now && event.endDate <= future) {
events.push(event);
for (let splitEvent of splitEvents) {
if (splitEvent.endDate > now && splitEvent.endDate <= future) {
events.push(splitEvent);
}
}
} else {
@@ -534,12 +552,11 @@ Module.register("calendar", {
// Limit the number of days displayed
// If limitDays is set > 0, limit display to that number of days
if (this.config.limitDays > 0) {
var newEvents = [];
var lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
var days = 0;
var eventDate;
for (var ev of events) {
eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
let newEvents = [];
let lastDate = today.clone().subtract(1, "days").format("YYYYMMDD");
let days = 0;
for (const ev of events) {
let eventDate = moment(ev.startDate, "x").format("YYYYMMDD");
// if date of event is later than lastdate
// check if we already are showing max unique days
if (eventDate > lastDate) {
@@ -563,7 +580,7 @@ Module.register("calendar", {
},
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)) {
return true;
}
@@ -579,8 +596,6 @@ Module.register("calendar", {
* @param {object} calendarConfig The config of the specific calendar
*/
addCalendar: function (url, auth, calendarConfig) {
var self = this;
this.sendSocketNotification("ADD_CALENDAR", {
id: this.identifier,
url: url,
@@ -592,7 +607,8 @@ Module.register("calendar", {
titleClass: calendarConfig.titleClass,
timeClass: calendarConfig.timeClass,
auth: auth,
broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents
broadcastPastEvents: calendarConfig.broadcastPastEvents || this.config.broadcastPastEvents,
selfSignedCert: calendarConfig.selfSignedCert || this.config.selfSignedCert
});
},
@@ -693,8 +709,7 @@ Module.register("calendar", {
* @returns {*} The property
*/
getCalendarProperty: function (url, property, defaultValue) {
for (var c in this.config.calendars) {
var calendar = this.config.calendars[c];
for (const calendar of this.config.calendars) {
if (calendar.url === url && calendar.hasOwnProperty(property)) {
return calendar[property];
}
@@ -728,13 +743,13 @@ Module.register("calendar", {
}
if (wrapEvents === true) {
var temp = "";
var currentLine = "";
var words = string.split(" ");
var line = 0;
const words = string.split(" ");
let temp = "";
let currentLine = "";
let line = 0;
for (var i = 0; i < words.length; i++) {
var word = words[i];
for (let i = 0; i < words.length; i++) {
const word = words[i];
if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) {
// max - 1 to account for a space
currentLine += word + " ";
@@ -789,10 +804,10 @@ Module.register("calendar", {
* @returns {string} The transformed title.
*/
titleTransform: function (title, titleReplace, wrapEvents, maxTitleLength, maxTitleLines) {
for (var needle in titleReplace) {
var replacement = titleReplace[needle];
for (let needle in titleReplace) {
const replacement = titleReplace[needle];
var regParts = needle.match(/^\/(.+)\/([gim]*)$/);
const regParts = needle.match(/^\/(.+)\/([gim]*)$/);
if (regParts) {
// the parsed pattern is a regexp.
needle = new RegExp(regParts[1], regParts[2]);
@@ -810,11 +825,10 @@ Module.register("calendar", {
* The all events available in one array, sorted on startdate.
*/
broadcastEvents: function () {
var eventList = [];
for (var url in this.calendarData) {
var calendar = this.calendarData[url];
for (var e in calendar) {
var event = cloneObject(calendar[e]);
const eventList = [];
for (const url in this.calendarData) {
for (const ev of this.calendarData[url]) {
const event = cloneObject(ev);
event.symbol = this.symbolsForEvent(event);
event.calendarName = this.calendarNameForUrl(url);
event.color = this.colorForUrl(url);

View File

@@ -4,17 +4,13 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const Log = require("../../../js/logger.js");
const CalendarUtils = require("./calendarutils");
const Log = require("logger");
const NodeHelper = require("node_helper");
const ical = require("node-ical");
const request = require("request");
/**
* Moment date
*
* @external Moment
* @see {@link http://momentjs.com}
*/
const moment = require("moment");
const fetch = require("node-fetch");
const digest = require("digest-fetch");
const https = require("https");
/**
*
@@ -25,11 +21,10 @@ const moment = require("moment");
* @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 {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
*/
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents) {
const self = this;
const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents, selfSignedCert) {
let reloadTimer = null;
let events = [];
@@ -39,492 +34,61 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
/**
* Initiates calendar fetch.
*/
const fetchCalendar = function () {
const fetchCalendar = () => {
clearTimeout(reloadTimer);
reloadTimer = null;
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
const opts = {
headers: {
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
},
gzip: true
let fetcher = null;
let httpsAgent = null;
let headers = {
"User-Agent": "Mozilla/5.0 (Node.js " + nodeVersion + ") MagicMirror/" + global.version + " (https://github.com/MichMich/MagicMirror/)"
};
if (selfSignedCert) {
httpsAgent = new https.Agent({
rejectUnauthorized: false
});
}
if (auth) {
if (auth.method === "bearer") {
opts.auth = {
bearer: auth.pass
};
headers.Authorization = "Bearer " + auth.pass;
} else if (auth.method === "digest") {
fetcher = new digest(auth.user, auth.pass).fetch(url, { headers: headers, agent: httpsAgent });
} else {
opts.auth = {
user: auth.user,
pass: auth.pass,
sendImmediately: auth.method !== "digest"
};
headers.Authorization = "Basic " + Buffer.from(auth.user + ":" + auth.pass).toString("base64");
}
}
if (fetcher === null) {
fetcher = fetch(url, { headers: headers, agent: httpsAgent });
}
request(url, opts, function (err, r, requestData) {
if (err) {
fetchFailedCallback(self, err);
scheduleTimer();
return;
} else if (r.statusCode !== 200) {
fetchFailedCallback(self, r.statusCode + ": " + r.statusMessage);
scheduleTimer();
return;
}
fetcher
.then(NodeHelper.checkFetchStatus)
.then((response) => response.text())
.then((responseData) => {
let data = [];
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
});
}
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);
scheduleTimer();
return;
}
this.broadcastEvents();
scheduleTimer();
})
.catch((error) => {
fetchFailedCallback(this, error);
scheduleTimer();
});
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 +101,6 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
}, 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 */
/**
@@ -627,7 +115,7 @@ const CalendarFetcher = function (url, reloadInterval, excludedEvents, maximumEn
*/
this.broadcastEvents = function () {
Log.info("Calendar-Fetcher: Broadcasting " + events.length + " events.");
eventsReceivedCallback(self);
eventsReceivedCallback(this);
};
/**

View File

@@ -0,0 +1,623 @@
/* 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 the event which needs adjustement
* @param {Date} date the date on which this event happens
* @returns {number} the necessary adjustment in hours
*/
calculateTimezoneAdjustment: function (event, date) {
let adjustHours = 0;
// if a timezone was specified
if (!event.start.tz) {
Log.debug(" if no tz, guess based on now");
event.start.tz = moment.tz.guess();
}
Log.debug("initial tz=" + event.start.tz);
// if there is a start date specified
if (event.start.tz) {
// if this is a windows timezone
if (event.start.tz.includes(" ")) {
// use the lookup table to get theIANA name as moment and date don't know MS timezones
let tz = 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;
},
/**
* Filter the events from ical according to the given config
*
* @param {object} data the calendar data from ical
* @param {object} config The configuration object
* @returns {string[]} the filtered events
*/
filterEvents: function (data, config) {
const newEvents = [];
// limitFunction doesn't do much limiting, see comment re: the dates
// array in rrule section below as to why we need to do the filtering
// ourselves
const limitFunction = function (date, i) {
return true;
};
const eventDate = function (event, time) {
return CalendarUtils.isFullDayEvent(event) ? moment(event[time], "YYYYMMDD") : moment(new Date(event[time]));
};
Log.debug("There are " + Object.entries(data).length + " calendar entries.");
Object.entries(data).forEach(([key, event]) => {
Log.debug("Processing entry...");
const now = new Date();
const today = moment().startOf("day").toDate();
const future = moment().startOf("day").add(config.maximumNumberOfDays, "days").subtract(1, "seconds").toDate(); // Subtract 1 second so that events that start on the middle of the night will not repeat.
let past = today;
if (config.includePastEvents) {
past = moment().startOf("day").subtract(config.maximumNumberOfDays, "days").toDate();
}
// FIXME: Ugly fix to solve the facebook birthday issue.
// Otherwise, the recurring events only show the birthday for next year.
let isFacebookBirthday = false;
if (typeof event.uid !== "undefined") {
if (event.uid.indexOf("@facebook.com") !== -1) {
isFacebookBirthday = true;
}
}
if (event.type === "VEVENT") {
Log.debug("\nEvent: " + JSON.stringify(event));
let startDate = eventDate(event, "start");
let endDate;
if (typeof event.end !== "undefined") {
endDate = eventDate(event, "end");
} else if (typeof event.duration !== "undefined") {
endDate = startDate.clone().add(moment.duration(event.duration));
} else {
if (!isFacebookBirthday) {
// make copy of start date, separate storage area
endDate = moment(startDate.format("x"), "x");
} else {
endDate = moment(startDate).add(1, "days");
}
}
Log.debug("startDate (local): " + startDate.toDate());
Log.debug("endDate (local): " + endDate.toDate());
// Calculate the duration of the event for use with recurring events.
let duration = parseInt(endDate.format("x")) - parseInt(startDate.format("x"));
Log.debug("duration: " + duration);
// FIXME: Since the parsed json object from node-ical comes with time information
// this check could be removed (?)
if (event.start.length === 8) {
startDate = startDate.startOf("day");
}
const title = CalendarUtils.getTitleFromEvent(event);
Log.debug("title: " + title);
let excluded = false,
dateFilter = null;
for (let f in config.excludedEvents) {
let filter = config.excludedEvents[f],
testTitle = title.toLowerCase(),
until = null,
useRegex = false,
regexFlags = "g";
if (filter instanceof Object) {
if (typeof filter.until !== "undefined") {
until = filter.until;
}
if (typeof filter.regex !== "undefined") {
useRegex = filter.regex;
}
// If additional advanced filtering is added in, this section
// must remain last as we overwrite the filter object with the
// filterBy string
if (filter.caseSensitive) {
filter = filter.filterBy;
testTitle = title;
} else if (useRegex) {
filter = filter.filterBy;
testTitle = title;
regexFlags += "i";
} else {
filter = filter.filterBy.toLowerCase();
}
} else {
filter = filter.toLowerCase();
}
if (CalendarUtils.titleFilterApplies(testTitle, filter, useRegex, regexFlags)) {
if (until) {
dateFilter = until;
} else {
excluded = true;
}
break;
}
}
if (excluded) {
return;
}
const location = event.location || false;
const geo = event.geo || false;
const description = event.description || false;
if (typeof event.rrule !== "undefined" && event.rrule !== null && !isFacebookBirthday) {
const rule = event.rrule;
let addedEvents = 0;
const pastMoment = moment(past);
const futureMoment = moment(future);
// can cause problems with e.g. birthdays before 1900
if ((rule.options && rule.origOptions && rule.origOptions.dtstart && rule.origOptions.dtstart.getFullYear() < 1900) || (rule.options && rule.options.dtstart && rule.options.dtstart.getFullYear() < 1900)) {
rule.origOptions.dtstart.setYear(1900);
rule.options.dtstart.setYear(1900);
}
// For recurring events, get the set of start dates that fall within the range
// of dates we're looking for.
// kblankenship1989 - to fix issue #1798, converting all dates to locale time first, then converting back to UTC time
let pastLocal = 0;
let futureLocal = 0;
if (CalendarUtils.isFullDayEvent(event)) {
Log.debug("fullday");
// if full day event, only use the date part of the ranges
pastLocal = pastMoment.toDate();
futureLocal = futureMoment.toDate();
Log.debug("pastLocal: " + pastLocal);
Log.debug("futureLocal: " + futureLocal);
} else {
// if we want past events
if (config.includePastEvents) {
// use the calculated past time for the between from
pastLocal = pastMoment.toDate();
} else {
// otherwise use NOW.. cause we shouldn't use any before now
pastLocal = moment().toDate(); //now
}
futureLocal = futureMoment.toDate(); // future
}
Log.debug("Search for recurring events between: " + pastLocal + " and " + futureLocal);
const dates = rule.between(pastLocal, futureLocal, true, limitFunction);
Log.debug("Title: " + event.summary + ", with dates: " + JSON.stringify(dates));
// The "dates" array contains the set of dates within our desired date range range that are valid
// for the recurrence rule. *However*, it's possible for us to have a specific recurrence that
// had its date changed from outside the range to inside the range. For the time being,
// we'll handle this by adding *all* recurrence entries into the set of dates that we check,
// because the logic below will filter out any recurrences that don't actually belong within
// our display range.
// Would be great if there was a better way to handle this.
Log.debug("event.recurrences: " + event.recurrences);
if (event.recurrences !== undefined) {
for (let r in event.recurrences) {
// Only add dates that weren't already in the range we added from the rrule so that
// we don"t double-add those events.
if (moment(new Date(r)).isBetween(pastMoment, futureMoment) !== true) {
dates.push(new Date(r));
}
}
}
// Loop through the set of date entries to see which recurrences should be added to our event list.
for (let d in dates) {
let date = dates[d];
// Remove the time information of each date by using its substring, using the following method:
// .toISOString().substring(0,10).
// since the date is given as ISOString with YYYY-MM-DDTHH:MM:SS.SSSZ
// (see https://momentjs.com/docs/#/displaying/as-iso-string/).
const dateKey = date.toISOString().substring(0, 10);
let curEvent = event;
let showRecurrence = true;
// Get the offset of today where we are processing
// This will be the correction, we need to apply.
let nowOffset = new Date().getTimezoneOffset();
// For full day events, the time might be off from RRULE/Luxon problem
// Get time zone offset of the rule calculated event
let dateoffset = date.getTimezoneOffset();
// Reduce the time by the following offset.
Log.debug(" recurring date is " + date + " offset is " + dateoffset);
let dh = moment(date).format("HH");
Log.debug(" recurring date is " + date + " offset is " + dateoffset / 60 + " Hour is " + dh);
if (CalendarUtils.isFullDayEvent(event)) {
Log.debug("Fullday");
// If the offset is negative (east of GMT), where the problem is
if (dateoffset < 0) {
// Remove the offset, independently of the comparison between the date hour and the offset,
// since in the case that *date houre < offset*, the *new Date* command will handle this by
// representing the day before.
// Reduce the time by the offset:
// 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:
// 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);
Log.debug("Corrected startDate (local): " + startDate.toDate());
let adjustDays = CalendarUtils.calculateTimezoneAdjustment(event, date);
// For each date that we're checking, it's possible that there is a recurrence override for that one day.
if (curEvent.recurrences !== undefined && curEvent.recurrences[dateKey] !== undefined) {
// We found an override, so for this recurrence, use a potentially different title, start date, and duration.
curEvent = curEvent.recurrences[dateKey];
startDate = moment(curEvent.start);
duration = parseInt(moment(curEvent.end).format("x")) - parseInt(startDate.format("x"));
}
// If there's no recurrence override, check for an exception date. Exception dates represent exceptions to the rule.
else if (curEvent.exdate !== undefined && curEvent.exdate[dateKey] !== undefined) {
// This date is an exception date, which means we should skip it in the recurrence pattern.
showRecurrence = false;
}
Log.debug("duration: " + duration);
endDate = moment(parseInt(startDate.format("x")) + duration, "x");
if (startDate.format("x") === endDate.format("x")) {
endDate = endDate.endOf("day");
}
const recurrenceTitle = CalendarUtils.getTitleFromEvent(curEvent);
// If this recurrence ends before the start of the date range, or starts after the end of the date range, don"t add
// it to the event list.
if (endDate.isBefore(past) || startDate.isAfter(future)) {
showRecurrence = false;
}
if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
showRecurrence = false;
}
if (showRecurrence === true) {
Log.debug("saving event: " + description);
addedEvents++;
newEvents.push({
title: recurrenceTitle,
startDate: (adjustDays ? (adjustDays > 0 ? startDate.add(adjustDays, "hours") : startDate.subtract(Math.abs(adjustDays), "hours")) : startDate).format("x"),
endDate: (adjustDays ? (adjustDays > 0 ? endDate.add(adjustDays, "hours") : endDate.subtract(Math.abs(adjustDays), "hours")) : endDate).format("x"),
fullDayEvent: CalendarUtils.isFullDayEvent(event),
recurringEvent: true,
class: event.class,
firstYear: event.start.getFullYear(),
location: location,
geo: geo,
description: description
});
}
}
// End recurring event parsing.
} else {
// Single event.
const fullDayEvent = isFacebookBirthday ? true : CalendarUtils.isFullDayEvent(event);
// Log.debug("full day event")
if (config.includePastEvents) {
// Past event is too far in the past, so skip.
if (endDate < past) {
return;
}
} else {
// It's not a fullday event, and it is in the past, so skip.
if (!fullDayEvent && endDate < new Date()) {
return;
}
// It's a fullday event, and it is before today, So skip.
if (fullDayEvent && endDate <= today) {
return;
}
}
// It exceeds the maximumNumberOfDays limit, so skip.
if (startDate > future) {
return;
}
if (CalendarUtils.timeFilterApplies(now, endDate, dateFilter)) {
return;
}
// 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 {string} msTZName the timezone name to lookup
* @returns {string|null} the iana name or null of none is found
*/
getIanaTZFromMS: function (msTZName) {
// Get hash entry
const he = zoneTable[msTZName];
// If found return iana name, else null
return he ? he.iana[0] : null;
},
/**
* Gets the title from the event.
*
* @param {object} event The event object to check.
* @returns {string} The title of the event, or "Event" if no title is found.
*/
getTitleFromEvent: function (event) {
let title = "Event";
if (event.summary) {
title = typeof event.summary.val !== "undefined" ? event.summary.val : event.summary;
} else if (event.description) {
title = event.description;
}
return title;
},
/**
* Checks if an event is a fullday event.
*
* @param {object} event The event object to check.
* @returns {boolean} True if the event is a fullday event, false otherwise
*/
isFullDayEvent: function (event) {
if (event.start.length === 8 || event.start.dateOnly || event.datetype === "date") {
return true;
}
const start = event.start || 0;
const startDate = new Date(start);
const end = event.end || 0;
if ((end - start) % (24 * 60 * 60 * 1000) === 0 && startDate.getHours() === 0 && startDate.getMinutes() === 0) {
// Is 24 hours, and starts on the middle of the night.
return true;
}
return false;
},
/**
* Determines if the user defined time filter should apply
*
* @param {Date} now Date object using previously created object for consistency
* @param {Moment} endDate Moment object representing the event end date
* @param {string} filter The time to subtract from the end date to determine if an event should be shown
* @returns {boolean} True if the event should be filtered out, false otherwise
*/
timeFilterApplies: function (now, endDate, filter) {
if (filter) {
const until = filter.split(" "),
value = parseInt(until[0]),
increment = until[1].slice(-1) === "s" ? until[1] : until[1] + "s", // Massage the data for moment js
filterUntil = moment(endDate.format()).subtract(value, increment);
return now < filterUntil.format("x");
}
return false;
},
/**
* Determines if the user defined title filter should apply
*
* @param {string} title the title of the event
* @param {string} filter the string to look for, can be a regex also
* @param {boolean} useRegex true if a regex should be used, otherwise it just looks for the filter as a string
* @param {string} regexFlags flags that should be applied to the regex
* @returns {boolean} True if the title should be filtered out, false otherwise
*/
titleFilterApplies: function (title, filter, useRegex, regexFlags) {
if (useRegex) {
// Assume if leading slash, there is also trailing slash
if (filter[0] === "/") {
// Strip leading and trailing slashes
filter = filter.substr(1).slice(0, -1);
}
filter = new RegExp(filter, regexFlags);
return filter.test(title);
} else {
return title.includes(filter);
}
}
};
if (typeof module !== "undefined") {
module.exports = CalendarUtils;
}

View File

@@ -5,6 +5,9 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
// Alias modules mentioned in package.js under _moduleAliases.
require("module-alias/register");
const CalendarFetcher = require("./calendarfetcher.js");
const url = "https://calendar.google.com/calendar/ical/pkm1t2uedjbp0uvq1o7oj1jouo%40group.calendar.google.com/private-08ba559f89eec70dd74bbd887d0a3598/basic.ics"; // Standard test URL
@@ -26,11 +29,13 @@ const fetcher = new CalendarFetcher(url, fetchInterval, [], maximumEntries, maxi
fetcher.onReceive(function (fetcher) {
console.log(fetcher.events());
console.log("------------------------------------------------------------");
process.exit(0);
});
fetcher.onError(function (fetcher, error) {
console.log("Fetcher error:");
console.log(error);
process.exit(1);
});
fetcher.startFetch();

View File

@@ -5,9 +5,8 @@
* MIT Licensed.
*/
const NodeHelper = require("node_helper");
const validUrl = require("valid-url");
const CalendarFetcher = require("./calendarfetcher.js");
const Log = require("../../../js/logger");
const Log = require("logger");
module.exports = NodeHelper.create({
// Override start method.
@@ -19,7 +18,7 @@ module.exports = NodeHelper.create({
// Override socketNotificationReceived method.
socketNotificationReceived: function (notification, payload) {
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,56 @@ module.exports = NodeHelper.create({
* @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 {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
*/
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, identifier) {
var self = this;
if (!validUrl.isUri(url)) {
self.sendSocketNotification("INCORRECT_URL", { id: identifier, url: url });
createFetcher: function (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
try {
new URL(url);
} catch (error) {
Log.error("Calendar Error. Malformed calendar url: ", url, error);
this.sendSocketNotification("CALENDAR_ERROR", { error_type: "MODULE_ERROR_MALFORMED_URL" });
return;
}
var fetcher;
if (typeof self.fetchers[identifier + url] === "undefined") {
Log.log("Create new calendar fetcher for url: " + url + " - Interval: " + fetchInterval);
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents);
let fetcher;
if (typeof this.fetchers[identifier + url] === "undefined") {
Log.log("Create new calendarfetcher for url: " + url + " - Interval: " + fetchInterval);
fetcher = new CalendarFetcher(url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
fetcher.onReceive(function (fetcher) {
self.sendSocketNotification("CALENDAR_EVENTS", {
id: identifier,
url: fetcher.url(),
events: fetcher.events()
});
fetcher.onReceive((fetcher) => {
this.broadcastEvents(fetcher, identifier);
});
fetcher.onError(function (fetcher, error) {
fetcher.onError((fetcher, error) => {
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url(), error);
self.sendSocketNotification("FETCH_ERROR", {
let error_type = NodeHelper.checkFetchError(error);
this.sendSocketNotification("CALENDAR_ERROR", {
id: identifier,
url: fetcher.url(),
error: error
error_type
});
});
self.fetchers[identifier + url] = fetcher;
this.fetchers[identifier + url] = fetcher;
} else {
Log.log("Use existing calendar fetcher for url: " + url);
fetcher = self.fetchers[identifier + url];
Log.log("Use existing calendarfetcher for url: " + url);
fetcher = this.fetchers[identifier + url];
fetcher.broadcastEvents();
}
fetcher.startFetch();
},
/**
*
* @param {object} fetcher the fetcher associated with the calendar
* @param {string} identifier the identifier of the calendar
*/
broadcastEvents: function (fetcher, identifier) {
this.sendSocketNotification("CALENDAR_EVENTS", {
id: identifier,
url: fetcher.url(),
events: fetcher.events()
});
}
});

View File

@@ -12,11 +12,14 @@ Module.register("clock", {
displayType: "digital", // options: digital, analog, both
timeFormat: config.timeFormat,
timezone: null,
displaySeconds: true,
showPeriod: true,
showPeriodUpper: false,
clockBold: false,
showDate: true,
showTime: true,
showWeek: false,
dateFormat: "dddd, LL",
@@ -24,9 +27,8 @@ Module.register("clock", {
analogSize: "200px",
analogFace: "simple", // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive)
analogPlacement: "bottom", // options: 'top', 'bottom', 'left', 'right'
analogShowDate: "top", // options: false, 'top', or 'bottom'
analogShowDate: "top", // OBSOLETE, can be replaced with analogPlacement and showTime, options: false, 'top', or 'bottom'
secondsColor: "#888888",
timezone: null,
showSunTimes: false,
showMoonTimes: false,
@@ -46,66 +48,75 @@ Module.register("clock", {
Log.info("Starting module: " + this.name);
// Schedule update interval.
var self = this;
self.second = moment().second();
self.minute = moment().minute();
this.second = moment().second();
this.minute = moment().minute();
//Calculate how many ms should pass until next update depending on if seconds is displayed or not
var delayCalculator = function (reducedSeconds) {
var EXTRA_DELAY = 50; //Deliberate imperceptable delay to prevent off-by-one timekeeping errors
// Calculate how many ms should pass until next update depending on if seconds is displayed or not
const delayCalculator = (reducedSeconds) => {
const EXTRA_DELAY = 50; // Deliberate imperceptible delay to prevent off-by-one timekeeping errors
if (self.config.displaySeconds) {
if (this.config.displaySeconds) {
return 1000 - moment().milliseconds() + EXTRA_DELAY;
} else {
return (60 - reducedSeconds) * 1000 - moment().milliseconds() + EXTRA_DELAY;
}
};
//A recursive timeout function instead of interval to avoid drifting
var notificationTimer = function () {
self.updateDom();
// A recursive timeout function instead of interval to avoid drifting
const notificationTimer = () => {
this.updateDom();
//If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
if (self.config.displaySeconds) {
self.second = moment().second();
if (self.second !== 0) {
self.sendNotification("CLOCK_SECOND", self.second);
// If seconds is displayed CLOCK_SECOND-notification should be sent (but not when CLOCK_MINUTE-notification is sent)
if (this.config.displaySeconds) {
this.second = moment().second();
if (this.second !== 0) {
this.sendNotification("CLOCK_SECOND", this.second);
setTimeout(notificationTimer, delayCalculator(0));
return;
}
}
//If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
self.minute = moment().minute();
self.sendNotification("CLOCK_MINUTE", self.minute);
// If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
this.minute = moment().minute();
this.sendNotification("CLOCK_MINUTE", this.minute);
setTimeout(notificationTimer, delayCalculator(0));
};
//Set the initial timeout with the amount of seconds elapsed as reducedSeconds so it will trigger when the minute changes
setTimeout(notificationTimer, delayCalculator(self.second));
// Set the initial timeout with the amount of seconds elapsed as reducedSeconds so it will trigger when the minute changes
setTimeout(notificationTimer, delayCalculator(this.second));
// Set locale.
moment.locale(config.language);
},
// Override dom generator.
getDom: function () {
var wrapper = document.createElement("div");
const wrapper = document.createElement("div");
wrapper.classList.add("clockGrid");
/************************************
* Create wrappers for analog and digital clock
*/
const analogWrapper = document.createElement("div");
analogWrapper.className = "clockCircle";
const digitalWrapper = document.createElement("div");
digitalWrapper.className = "digital";
digitalWrapper.style.gridArea = "center";
/************************************
* Create wrappers for DIGITAL clock
*/
const dateWrapper = document.createElement("div");
const timeWrapper = document.createElement("div");
const secondsWrapper = document.createElement("sup");
const periodWrapper = document.createElement("span");
const sunWrapper = document.createElement("div");
const moonWrapper = document.createElement("div");
const weekWrapper = document.createElement("div");
var dateWrapper = document.createElement("div");
var timeWrapper = document.createElement("div");
var secondsWrapper = document.createElement("sup");
var periodWrapper = document.createElement("span");
var sunWrapper = document.createElement("div");
var moonWrapper = document.createElement("div");
var weekWrapper = document.createElement("div");
// Style Wrappers
dateWrapper.className = "date normal medium";
timeWrapper.className = "time bright large light";
secondsWrapper.className = "dimmed";
secondsWrapper.className = "seconds dimmed";
sunWrapper.className = "sun dimmed small";
moonWrapper.className = "moon dimmed small";
weekWrapper.className = "week dimmed medium";
@@ -114,19 +125,18 @@ Module.register("clock", {
// The moment().format("h") method has a bug on the Raspberry Pi.
// So we need to generate the timestring manually.
// See issue: https://github.com/MichMich/MagicMirror/issues/181
var timeString;
var now = moment();
this.lastDisplayedMinute = now.minute();
let timeString;
const now = moment();
if (this.config.timezone) {
now.tz(this.config.timezone);
}
var hourSymbol = "HH";
let hourSymbol = "HH";
if (this.config.timeFormat !== 24) {
hourSymbol = "h";
}
if (this.config.clockBold === true) {
if (this.config.clockBold) {
timeString = now.format(hourSymbol + '[<span class="bold">]mm[</span>]');
} else {
timeString = now.format(hourSymbol + ":mm");
@@ -134,22 +144,24 @@ Module.register("clock", {
if (this.config.showDate) {
dateWrapper.innerHTML = now.format(this.config.dateFormat);
digitalWrapper.appendChild(dateWrapper);
}
if (this.config.showWeek) {
weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() });
}
timeWrapper.innerHTML = timeString;
secondsWrapper.innerHTML = now.format("ss");
if (this.config.showPeriodUpper) {
periodWrapper.innerHTML = now.format("A");
} else {
periodWrapper.innerHTML = now.format("a");
}
if (this.config.displaySeconds) {
timeWrapper.appendChild(secondsWrapper);
}
if (this.config.showPeriod && this.config.timeFormat !== 24) {
timeWrapper.appendChild(periodWrapper);
if (this.config.displayType !== "analog" && this.config.showTime) {
timeWrapper.innerHTML = timeString;
secondsWrapper.innerHTML = now.format("ss");
if (this.config.showPeriodUpper) {
periodWrapper.innerHTML = now.format("A");
} else {
periodWrapper.innerHTML = now.format("a");
}
if (this.config.displaySeconds) {
timeWrapper.appendChild(secondsWrapper);
}
if (this.config.showPeriod && this.config.timeFormat !== 24) {
timeWrapper.appendChild(periodWrapper);
}
digitalWrapper.appendChild(timeWrapper);
}
/**
@@ -160,17 +172,20 @@ Module.register("clock", {
* @returns {string} The formatted time string
*/
function formatTime(config, time) {
var formatString = hourSymbol + ":mm";
let formatString = hourSymbol + ":mm";
if (config.showPeriod && config.timeFormat !== 24) {
formatString += config.showPeriodUpper ? "A" : "a";
}
return moment(time).format(formatString);
}
/****************************************************************
* Create wrappers for Sun Times, only if specified in config
*/
if (this.config.showSunTimes) {
const sunTimes = SunCalc.getTimes(now, this.config.lat, this.config.lon);
const isVisible = now.isBetween(sunTimes.sunrise, sunTimes.sunset);
var nextEvent;
let nextEvent;
if (now.isBefore(sunTimes.sunrise)) {
nextEvent = sunTimes.sunrise;
} else if (now.isBefore(sunTimes.sunset)) {
@@ -193,12 +208,17 @@ Module.register("clock", {
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
formatTime(this.config, sunTimes.sunset) +
"</span>";
digitalWrapper.appendChild(sunWrapper);
}
/****************************************************************
* Create wrappers for Moon Times, only if specified in config
*/
if (this.config.showMoonTimes) {
const moonIllumination = SunCalc.getMoonIllumination(now.toDate());
const moonTimes = SunCalc.getMoonTimes(now, this.config.lat, this.config.lon);
const moonRise = moonTimes.rise;
var moonSet;
let moonSet;
if (moment(moonTimes.set).isAfter(moonTimes.rise)) {
moonSet = moonTimes.set;
} else {
@@ -219,12 +239,17 @@ Module.register("clock", {
'<span><i class="fa fa-arrow-down" aria-hidden="true"></i> ' +
(moonSet ? formatTime(this.config, moonSet) : "...") +
"</span>";
digitalWrapper.appendChild(moonWrapper);
}
if (this.config.showWeek) {
weekWrapper.innerHTML = this.translate("WEEK", { weekNumber: now.week() });
digitalWrapper.appendChild(weekWrapper);
}
/****************************************************************
* Create wrappers for ANALOG clock, only if specified in config
*/
if (this.config.displayType !== "digital") {
// If it isn't 'digital', then an 'analog' clock was also requested
@@ -232,34 +257,32 @@ Module.register("clock", {
if (this.config.timezone) {
now.tz(this.config.timezone);
}
var second = now.seconds() * 6,
const second = now.seconds() * 6,
minute = now.minute() * 6 + second / 60,
hour = ((now.hours() % 12) / 12) * 360 + 90 + minute / 12;
// Create wrappers
var clockCircle = document.createElement("div");
clockCircle.className = "clockCircle";
clockCircle.style.width = this.config.analogSize;
clockCircle.style.height = this.config.analogSize;
analogWrapper.style.width = this.config.analogSize;
analogWrapper.style.height = this.config.analogSize;
if (this.config.analogFace !== "" && this.config.analogFace !== "simple" && this.config.analogFace !== "none") {
clockCircle.style.background = "url(" + this.data.path + "faces/" + this.config.analogFace + ".svg)";
clockCircle.style.backgroundSize = "100%";
analogWrapper.style.background = "url(" + this.data.path + "faces/" + this.config.analogFace + ".svg)";
analogWrapper.style.backgroundSize = "100%";
// The following line solves issue: https://github.com/MichMich/MagicMirror/issues/611
// clockCircle.style.border = "1px solid black";
clockCircle.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used
// analogWrapper.style.border = "1px solid black";
analogWrapper.style.border = "rgba(0, 0, 0, 0.1)"; //Updated fix for Issue 611 where non-black backgrounds are used
} else if (this.config.analogFace !== "none") {
clockCircle.style.border = "2px solid white";
analogWrapper.style.border = "2px solid white";
}
var clockFace = document.createElement("div");
const clockFace = document.createElement("div");
clockFace.className = "clockFace";
var clockHour = document.createElement("div");
const clockHour = document.createElement("div");
clockHour.id = "clockHour";
clockHour.style.transform = "rotate(" + hour + "deg)";
clockHour.className = "clockHour";
var clockMinute = document.createElement("div");
const clockMinute = document.createElement("div");
clockMinute.id = "clockMinute";
clockMinute.style.transform = "rotate(" + minute + "deg)";
clockMinute.className = "clockMinute";
@@ -269,91 +292,35 @@ Module.register("clock", {
clockFace.appendChild(clockMinute);
if (this.config.displaySeconds) {
var clockSecond = document.createElement("div");
const clockSecond = document.createElement("div");
clockSecond.id = "clockSecond";
clockSecond.style.transform = "rotate(" + second + "deg)";
clockSecond.className = "clockSecond";
clockSecond.style.backgroundColor = this.config.secondsColor;
clockFace.appendChild(clockSecond);
}
clockCircle.appendChild(clockFace);
analogWrapper.appendChild(clockFace);
}
/*******************************************
* Combine wrappers, check for .displayType
* Update placement, respect old analogShowDate even if its not needed anymore
*/
if (this.config.displayType === "digital") {
// Display only a digital clock
wrapper.appendChild(dateWrapper);
wrapper.appendChild(timeWrapper);
wrapper.appendChild(sunWrapper);
wrapper.appendChild(moonWrapper);
wrapper.appendChild(weekWrapper);
} else if (this.config.displayType === "analog") {
if (this.config.displayType === "analog") {
// Display only an analog clock
if (this.config.showWeek) {
weekWrapper.style.paddingBottom = "15px";
} else {
dateWrapper.style.paddingBottom = "15px";
}
if (this.config.analogShowDate === "top") {
wrapper.appendChild(dateWrapper);
wrapper.appendChild(weekWrapper);
wrapper.appendChild(clockCircle);
wrapper.classList.add("clockGrid--bottom");
} else if (this.config.analogShowDate === "bottom") {
wrapper.appendChild(clockCircle);
wrapper.appendChild(dateWrapper);
wrapper.appendChild(weekWrapper);
wrapper.classList.add("clockGrid--top");
} else {
wrapper.appendChild(clockCircle);
}
} else {
// Both clocks have been configured, check position
var placement = this.config.analogPlacement;
var analogWrapper = document.createElement("div");
analogWrapper.id = "analog";
analogWrapper.style.cssFloat = "none";
analogWrapper.appendChild(clockCircle);
var digitalWrapper = document.createElement("div");
digitalWrapper.id = "digital";
digitalWrapper.style.cssFloat = "none";
digitalWrapper.appendChild(dateWrapper);
digitalWrapper.appendChild(timeWrapper);
digitalWrapper.appendChild(sunWrapper);
digitalWrapper.appendChild(moonWrapper);
digitalWrapper.appendChild(weekWrapper);
var appendClocks = function (condition, pos1, pos2) {
var padding = [0, 0, 0, 0];
padding[placement === condition ? pos1 : pos2] = "20px";
analogWrapper.style.padding = padding.join(" ");
if (placement === condition) {
wrapper.appendChild(analogWrapper);
wrapper.appendChild(digitalWrapper);
} else {
wrapper.appendChild(digitalWrapper);
wrapper.appendChild(analogWrapper);
}
};
if (placement === "left" || placement === "right") {
digitalWrapper.style.display = "inline-block";
digitalWrapper.style.verticalAlign = "top";
analogWrapper.style.display = "inline-block";
appendClocks("left", 1, 3);
} else {
digitalWrapper.style.textAlign = "center";
appendClocks("top", 2, 0);
//analogWrapper.style.gridArea = "center";
}
} else if (this.config.displayType === "both") {
wrapper.classList.add("clockGrid--" + this.config.analogPlacement);
}
wrapper.appendChild(analogWrapper);
wrapper.appendChild(digitalWrapper);
// Return the wrapper to the dom.
return wrapper;
}

View File

@@ -1,5 +1,26 @@
.clockGrid {
display: inline-flex;
gap: 15px;
}
.clockGrid--left {
flex-direction: row;
}
.clockGrid--right {
flex-direction: row-reverse;
}
.clockGrid--top {
flex-direction: column;
}
.clockGrid--bottom {
flex-direction: column-reverse;
}
.clockCircle {
margin: 0 auto;
place-self: center;
position: relative;
border-radius: 50%;
background-size: 100%;
@@ -17,7 +38,7 @@
width: 6px;
height: 6px;
margin: -3px 0 0 -3px;
background: white;
background: var(--color-text-bright);
border-radius: 3px;
content: "";
display: block;
@@ -29,9 +50,9 @@
position: absolute;
top: 50%;
left: 50%;
margin: -2px 0 -2px -25%; /* numbers much match negative length & thickness */
margin: -2px 0 -2px -25%; /* numbers must match negative length & thickness */
padding: 2px 0 2px 25%; /* indicator length & thickness */
background: white;
background: var(--color-text-bright);
transform-origin: 100% 50%;
border-radius: 3px 0 0 3px;
}
@@ -44,7 +65,7 @@
left: 50%;
margin: -35% -2px 0; /* numbers must match negative length & thickness */
padding: 35% 2px 0; /* indicator length & thickness */
background: white;
background: var(--color-text-bright);
transform-origin: 50% 100%;
border-radius: 3px 0 0 3px;
}
@@ -57,7 +78,7 @@
left: 50%;
margin: -38% -1px 0 0; /* numbers must match negative length & thickness */
padding: 38% 1px 0 0; /* indicator length & thickness */
background: #888;
background: var(--color-text);
transform-origin: 50% 100%;
}

View File

@@ -39,37 +39,35 @@ Module.register("compliments", {
this.lastComplimentIndex = -1;
var self = this;
if (this.config.remoteFile !== null) {
this.complimentFile(function (response) {
self.config.compliments = JSON.parse(response);
self.updateDom();
this.complimentFile((response) => {
this.config.compliments = JSON.parse(response);
this.updateDom();
});
}
// Schedule update timer.
setInterval(function () {
self.updateDom(self.config.fadeSpeed);
setInterval(() => {
this.updateDom(this.config.fadeSpeed);
}, this.config.updateInterval);
},
/* randomIndex(compliments)
/**
* Generate a random index for a list of compliments.
*
* argument compliments Array<String> - Array with compliments.
*
* return Number - Random index.
* @param {string[]} compliments Array with compliments.
* @returns {number} a random index of given array
*/
randomIndex: function (compliments) {
if (compliments.length === 1) {
return 0;
}
var generate = function () {
const generate = function () {
return Math.floor(Math.random() * compliments.length);
};
var complimentIndex = generate();
let complimentIndex = generate();
while (complimentIndex === this.lastComplimentIndex) {
complimentIndex = generate();
@@ -80,15 +78,15 @@ Module.register("compliments", {
return complimentIndex;
},
/* complimentArray()
/**
* Retrieve an array of compliments for the time of the day.
*
* return compliments Array<String> - Array with compliments for the time of the day.
* @returns {string[]} array with compliments for the time of the day.
*/
complimentArray: function () {
var hour = moment().hour();
var date = this.config.mockDate ? this.config.mockDate : moment().format("YYYY-MM-DD");
var compliments;
const hour = moment().hour();
const date = this.config.mockDate ? this.config.mockDate : moment().format("YYYY-MM-DD");
let compliments;
if (hour >= this.config.morningStartTime && hour < this.config.morningEndTime && this.config.compliments.hasOwnProperty("morning")) {
compliments = this.config.compliments.morning.slice(0);
@@ -99,7 +97,7 @@ Module.register("compliments", {
}
if (typeof compliments === "undefined") {
compliments = new Array();
compliments = [];
}
if (this.currentWeatherType in this.config.compliments) {
@@ -108,7 +106,7 @@ Module.register("compliments", {
compliments.push.apply(compliments, this.config.compliments.anytime);
for (var entry in this.config.compliments) {
for (let entry in this.config.compliments) {
if (new RegExp(entry).test(date)) {
compliments.push.apply(compliments, this.config.compliments[entry]);
}
@@ -117,11 +115,13 @@ Module.register("compliments", {
return compliments;
},
/* complimentFile(callback)
/**
* Retrieve a file from the local filesystem
*
* @param {Function} callback Called when the file is retrieved.
*/
complimentFile: function (callback) {
var xobj = new XMLHttpRequest(),
const xobj = new XMLHttpRequest(),
isRemote = this.config.remoteFile.indexOf("http://") === 0 || this.config.remoteFile.indexOf("https://") === 0,
path = isRemote ? this.config.remoteFile : this.file(this.config.remoteFile);
xobj.overrideMimeType("application/json");
@@ -134,16 +134,16 @@ Module.register("compliments", {
xobj.send(null);
},
/* complimentArray()
/**
* Retrieve a random compliment.
*
* return compliment string - A compliment.
* @returns {string} a compliment
*/
randomCompliment: function () {
// get the current time of day compliments list
var compliments = this.complimentArray();
const compliments = this.complimentArray();
// variable for index to next message to display
let index = 0;
let index;
// are we randomizing
if (this.config.random) {
// yes
@@ -159,16 +159,16 @@ Module.register("compliments", {
// Override dom generator.
getDom: function () {
var wrapper = document.createElement("div");
const wrapper = document.createElement("div");
wrapper.className = this.config.classes ? this.config.classes : "thin xlarge bright pre-line";
// get the compliment text
var complimentText = this.randomCompliment();
const complimentText = this.randomCompliment();
// split it into parts on newline text
var parts = complimentText.split("\n");
const parts = complimentText.split("\n");
// create a span to hold it all
var compliment = document.createElement("span");
const compliment = document.createElement("span");
// process all the parts of the compliment text
for (var part of parts) {
for (const part of parts) {
// create a text element for each part
compliment.appendChild(document.createTextNode(part));
// add a break `
@@ -182,34 +182,14 @@ Module.register("compliments", {
},
// From data currentweather set weather type
setCurrentWeatherType: function (data) {
var weatherIconTable = {
"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];
setCurrentWeatherType: function (type) {
this.currentWeatherType = type;
},
// Override notification handler.
notificationReceived: function (notification, payload, sender) {
if (notification === "CURRENTWEATHER_DATA") {
this.setCurrentWeatherType(payload.data);
if (notification === "CURRENTWEATHER_TYPE") {
this.setCurrentWeatherType(payload.type);
}
}
});

View File

@@ -1,5 +1,7 @@
# 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.
This module displays the current weather, including the windspeed, the sunset or sunrise time, the temperature and an icon to display the current conditions.

View File

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

View File

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

View File

@@ -1,13 +1,10 @@
/* Magic Mirror
* Default Modules List
/* Magic Mirror Default Modules List
* Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name.
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
// Modules listed below can be loaded without the 'default/' prefix. Omitting the default folder name.
var defaultModules = ["alert", "calendar", "clock", "compliments", "currentweather", "helloworld", "newsfeed", "weatherforecast", "updatenotification", "weather"];
const defaultModules = ["alert", "calendar", "clock", "compliments", "currentweather", "helloworld", "newsfeed", "weatherforecast", "updatenotification", "weather"];
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

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

View File

@@ -0,0 +1,23 @@
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;
}
.newsfeed-list {
list-style: none;
}
.newsfeed-list li {
text-align: justify;
margin-bottom: 0.5em;
}

View File

@@ -14,6 +14,7 @@ Module.register("newsfeed", {
encoding: "UTF-8" //ISO-8859-1
}
],
showAsList: false,
showSourceTitle: true,
showPublishDate: true,
broadcastNewsFeeds: true,
@@ -44,6 +45,11 @@ Module.register("newsfeed", {
return ["moment.js"];
},
//Define required styles.
getStyles: function () {
return ["newsfeed.css"];
},
// Define required translations.
getTranslations: function () {
// The translations for the default modules are defined in the core translation files.
@@ -61,6 +67,7 @@ Module.register("newsfeed", {
this.newsItems = [];
this.loaded = false;
this.error = null;
this.activeItem = 0;
this.scrollPosition = 0;
@@ -75,130 +82,67 @@ Module.register("newsfeed", {
this.generateFeed(payload);
if (!this.loaded) {
if (this.config.hideLoading) {
this.show();
}
this.scheduleUpdateInterval();
}
this.loaded = true;
this.error = null;
} else if (notification === "NEWSFEED_ERROR") {
this.error = this.translate(payload.error_type);
this.scheduleUpdateInterval();
}
},
// Override dom generator.
getDom: function () {
const wrapper = document.createElement("div");
//Override fetching of template name
getTemplate: function () {
if (this.config.feedUrl) {
wrapper.className = "small bright";
wrapper.innerHTML = this.translate("MODULE_CONFIG_CHANGED", { MODULE_NAME: "Newsfeed" });
return wrapper;
return "oldconfig.njk";
} else if (this.config.showFullArticle) {
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) {
this.activeItem = 0;
}
if (this.newsItems.length > 0) {
// 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";
const item = this.newsItems[this.activeItem];
const items = this.newsItems.map(function (item) {
item.publishDate = moment(new Date(item.pubdate)).fromNow();
return item;
});
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "") {
sourceAndTimestamp.innerHTML = this.newsItems[this.activeItem].sourceTitle;
}
if (this.config.showSourceTitle && this.newsItems[this.activeItem].sourceTitle !== "" && this.config.showPublishDate) {
sourceAndTimestamp.innerHTML += ", ";
}
if (this.config.showPublishDate) {
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;
return {
loaded: true,
config: this.config,
sourceTitle: item.sourceTitle,
publishDate: moment(new Date(item.pubdate)).fromNow(),
title: item.title,
description: item.description,
items: items
};
},
getActiveItemURL: function () {
@@ -209,8 +153,7 @@ Module.register("newsfeed", {
* Registers the feeds to be used by the backend.
*/
registerFeeds: function () {
for (var f in this.config.feeds) {
var feed = this.config.feeds[f];
for (let feed of this.config.feeds) {
this.sendSocketNotification("ADD_FEED", {
feed: feed,
config: this.config
@@ -224,12 +167,11 @@ Module.register("newsfeed", {
* @param {object} feeds An object with feeds returned by the node helper.
*/
generateFeed: function (feeds) {
var newsItems = [];
for (var feed in feeds) {
var feedItems = feeds[feed];
let newsItems = [];
for (let feed in feeds) {
const feedItems = feeds[feed];
if (this.subscribedToFeed(feed)) {
for (var i in feedItems) {
var item = feedItems[i];
for (let item of feedItems) {
item.sourceTitle = this.titleForFeed(feed);
if (!(this.config.ignoreOldItems && Date.now() - new Date(item.pubdate) > this.config.ignoreOlderThan)) {
newsItems.push(item);
@@ -238,8 +180,8 @@ Module.register("newsfeed", {
}
}
newsItems.sort(function (a, b) {
var dateA = new Date(a.pubdate);
var dateB = new Date(b.pubdate);
const dateA = new Date(a.pubdate);
const dateB = new Date(b.pubdate);
return dateB - dateA;
});
if (this.config.maxNewsItems > 0) {
@@ -247,18 +189,56 @@ Module.register("newsfeed", {
}
if (this.config.prohibitedWords.length > 0) {
newsItems = newsItems.filter(function (value) {
for (var i = 0; i < this.config.prohibitedWords.length; i++) {
if (value["title"].toLowerCase().indexOf(this.config.prohibitedWords[i].toLowerCase()) > -1) {
newsItems = newsItems.filter(function (item) {
for (let word of this.config.prohibitedWords) {
if (item.title.toLowerCase().indexOf(word.toLowerCase()) > -1) {
return false;
}
}
return true;
}, 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
var updatedItems = [];
const updatedItems = [];
newsItems.forEach((value) => {
if (this.newsItems.findIndex((value1) => value1 === value) === -1) {
// Add item to updated items list
@@ -281,8 +261,7 @@ Module.register("newsfeed", {
* @returns {boolean} True if it is subscribed, false otherwise
*/
subscribedToFeed: function (feedUrl) {
for (var f in this.config.feeds) {
var feed = this.config.feeds[f];
for (let feed of this.config.feeds) {
if (feed.url === feedUrl) {
return true;
}
@@ -297,8 +276,7 @@ Module.register("newsfeed", {
* @returns {string} The title of the feed
*/
titleForFeed: function (feedUrl) {
for (var f in this.config.feeds) {
var feed = this.config.feeds[f];
for (let feed of this.config.feeds) {
if (feed.url === feedUrl) {
return feed.title || "";
}
@@ -310,22 +288,20 @@ Module.register("newsfeed", {
* Schedule visual update.
*/
scheduleUpdateInterval: function () {
var self = this;
self.updateDom(self.config.animationSpeed);
this.updateDom(this.config.animationSpeed);
// Broadcast NewsFeed if needed
if (self.config.broadcastNewsFeeds) {
self.sendNotification("NEWS_FEED", { items: self.newsItems });
if (this.config.broadcastNewsFeeds) {
this.sendNotification("NEWS_FEED", { items: this.newsItems });
}
this.timer = setInterval(function () {
self.activeItem++;
self.updateDom(self.config.animationSpeed);
this.timer = setInterval(() => {
this.activeItem++;
this.updateDom(this.config.animationSpeed);
// Broadcast NewsFeed if needed
if (self.config.broadcastNewsFeeds) {
self.sendNotification("NEWS_FEED", { items: self.newsItems });
if (this.config.broadcastNewsFeeds) {
this.sendNotification("NEWS_FEED", { items: this.newsItems });
}
}, this.config.updateInterval);
},
@@ -335,8 +311,7 @@ Module.register("newsfeed", {
this.config.showFullArticle = false;
this.scrollPosition = 0;
// reset bottom bar alignment
document.getElementsByClassName("region bottom bar")[0].style.bottom = "0";
document.getElementsByClassName("region bottom bar")[0].style.top = "inherit";
document.getElementsByClassName("region bottom bar")[0].classList.remove("newsfeed-fullarticle");
if (!this.timer) {
this.scheduleUpdateInterval();
}
@@ -344,7 +319,9 @@ Module.register("newsfeed", {
notificationReceived: function (notification, payload, sender) {
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++;
if (this.activeItem >= this.newsItems.length) {
this.activeItem = 0;
@@ -406,8 +383,7 @@ Module.register("newsfeed", {
this.config.showFullArticle = !this.isShowingDescription;
// make bottom bar align to top to allow scrolling
if (this.config.showFullArticle === true) {
document.getElementsByClassName("region bottom bar")[0].style.bottom = "inherit";
document.getElementsByClassName("region bottom bar")[0].style.top = "-90px";
document.getElementsByClassName("region bottom bar")[0].classList.add("newsfeed-fullarticle");
}
clearInterval(this.timer);
this.timer = null;

View File

@@ -0,0 +1,65 @@
{% if loaded %}
{% if config.showAsList %}
<ul class="newsfeed-list">
{% for item in items %}
<li>
{% if (config.showSourceTitle and item.sourceTitle) or config.showPublishDate %}
<div class="newsfeed-source light small dimmed">
{% if item.sourceTitle and config.showSourceTitle %}
{{ item.sourceTitle }}{% if config.showPublishDate %}, {% else %}: {% endif %}
{% endif %}
{% if config.showPublishDate %}
{{ item.publishDate }}:
{% endif %}
</div>
{% endif %}
<div class="newsfeed-title bright medium light{{ ' no-wrap' if not config.wrapTitle }}">
{{ item.title }}
</div>
{% if config.showDescription %}
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
{% if config.truncDescription %}
{{ item.description | truncate(config.lengthDescription) }}
{% else %}
{{ item.description }}
{% endif %}
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<div>
{% if (config.showSourceTitle and sourceTitle) or config.showPublishDate %}
<div class="newsfeed-source light small dimmed">
{% if sourceTitle and config.showSourceTitle %}
{{ 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>
{% if config.showDescription %}
<div class="newsfeed-desc small light{{ ' no-wrap' if not config.wrapDescription }}">
{% if config.truncDescription %}
{{ description | truncate(config.lengthDescription) }}
{% else %}
{{ description }}
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
{% elseif error %}
<div class="small dimmed">
{{ "MODULE_CONFIG_ERROR" | translate({MODULE_NAME: "Newsfeed", ERROR: error}) | safe }}
</div>
{% else %}
<div class="small dimmed">
{{ "LOADING" | translate | safe }}
</div>
{% endif %}

View File

@@ -4,9 +4,10 @@
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
const Log = require("../../../js/logger.js");
const Log = require("logger");
const FeedMe = require("feedme");
const request = require("request");
const NodeHelper = require("node_helper");
const fetch = require("node-fetch");
const iconv = require("iconv-lite");
/**
@@ -19,8 +20,6 @@ const iconv = require("iconv-lite");
* @class
*/
const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings) {
const self = this;
let reloadTimer = null;
let items = [];
@@ -36,14 +35,14 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
/**
* Request the new items.
*/
const fetchNews = function () {
const fetchNews = () => {
clearTimeout(reloadTimer);
reloadTimer = null;
items = [];
const parser = new FeedMe();
parser.on("item", function (item) {
parser.on("item", (item) => {
const title = item.title;
let description = item.description || item.summary || item.content || "";
const pubdate = item.pubdate || item.published || item.updated || item["dc:date"];
@@ -68,33 +67,32 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
}
});
parser.on("end", function () {
self.broadcastItems();
parser.on("end", () => {
this.broadcastItems();
scheduleTimer();
});
parser.on("error", function (error) {
fetchFailedCallback(self, error);
parser.on("error", (error) => {
fetchFailedCallback(this, error);
scheduleTimer();
});
const nodeVersion = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
const opts = {
headers: {
"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",
Pragma: "no-cache"
},
encoding: null
const headers = {
"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",
Pragma: "no-cache"
};
request(url, opts)
.on("error", function (error) {
fetchFailedCallback(self, error);
scheduleTimer();
fetch(url, { headers: headers })
.then(NodeHelper.checkFetchStatus)
.then((response) => {
response.body.pipe(iconv.decodeStream(encoding)).pipe(parser);
})
.pipe(iconv.decodeStream(encoding))
.pipe(parser);
.catch((error) => {
fetchFailedCallback(this, error);
scheduleTimer();
});
};
/**
@@ -136,7 +134,7 @@ const NewsfeedFetcher = function (url, reloadInterval, encoding, logFeedWarnings
return;
}
Log.info("Newsfeed-Fetcher: Broadcasting " + items.length + " items.");
itemsReceivedCallback(self);
itemsReceivedCallback(this);
};
this.onReceive = function (callback) {

View File

@@ -6,9 +6,8 @@
*/
const NodeHelper = require("node_helper");
const validUrl = require("valid-url");
const NewsfeedFetcher = require("./newsfeedfetcher.js");
const Log = require("../../../js/logger");
const Log = require("logger");
module.exports = NodeHelper.create({
// Override start method.
@@ -28,22 +27,25 @@ module.exports = NodeHelper.create({
* Creates a fetcher for a new feed if it doesn't exist yet.
* Otherwise it reuses the existing one.
*
* @param {object} feed The feed object.
* @param {object} config The configuration object.
* @param {object} feed The feed object
* @param {object} config The configuration object
*/
createFetcher: function (feed, config) {
const url = feed.url || "";
const encoding = feed.encoding || "UTF-8";
const reloadInterval = feed.reloadInterval || config.reloadInterval || 5 * 60 * 1000;
if (!validUrl.isUri(url)) {
this.sendSocketNotification("INCORRECT_URL", url);
try {
new URL(url);
} catch (error) {
Log.error("Newsfeed Error. Malformed newsfeed url: ", url, error);
this.sendSocketNotification("NEWSFEED_ERROR", { error_type: "MODULE_ERROR_MALFORMED_URL" });
return;
}
let fetcher;
if (typeof this.fetchers[url] === "undefined") {
Log.log("Create new news fetcher for url: " + url + " - Interval: " + reloadInterval);
Log.log("Create new newsfetcher for url: " + url + " - Interval: " + reloadInterval);
fetcher = new NewsfeedFetcher(url, reloadInterval, encoding, config.logFeedWarnings);
fetcher.onReceive(() => {
@@ -51,15 +53,16 @@ module.exports = NodeHelper.create({
});
fetcher.onError((fetcher, error) => {
this.sendSocketNotification("FETCH_ERROR", {
url: fetcher.url(),
error: error
Log.error("Newsfeed Error. Could not fetch newsfeed: ", url, error);
let error_type = NodeHelper.checkFetchError(error);
this.sendSocketNotification("NEWSFEED_ERROR", {
error_type
});
});
this.fetchers[url] = fetcher;
} else {
Log.log("Use existing news fetcher for url: " + url);
Log.log("Use existing newsfetcher for url: " + url);
fetcher = this.fetchers[url];
fetcher.setReloadInterval(reloadInterval);
fetcher.broadcastItems();
@@ -73,8 +76,8 @@ module.exports = NodeHelper.create({
* and broadcasts these using sendSocketNotification.
*/
broadcastFeeds: function () {
var feeds = {};
for (var f in this.fetchers) {
const feeds = {};
for (let f in this.fetchers) {
feeds[f] = this.fetchers[f].items();
}
this.sendSocketNotification("NEWS_ITEMS", feeds);

View File

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

View File

@@ -0,0 +1,177 @@
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const fs = require("fs");
const path = require("path");
const Log = require("logger");
class gitHelper {
constructor() {
this.gitRepos = [];
this.baseDir = path.normalize(__dirname + "/../../../");
}
getRefRegex(branch) {
return new RegExp("s*([a-z,0-9]+[.][.][a-z,0-9]+) " + branch, "g");
}
async execShell(command) {
let res = { stdout: "", stderr: "" };
const { stdout, stderr } = await exec(command);
res.stdout = stdout;
res.stderr = stderr;
return res;
}
async isGitRepo(moduleFolder) {
let res = await this.execShell("cd " + moduleFolder + " && git remote -v");
if (res.stderr) {
Log.error("Failed to fetch git data for " + moduleFolder + ": " + res.stderr);
return false;
} else {
return true;
}
}
add(moduleName) {
let moduleFolder = this.baseDir;
if (moduleName !== "default") {
moduleFolder = moduleFolder + "modules/" + moduleName;
}
try {
Log.info("Checking git for module: " + moduleName);
// Throws error if file doesn't exist
fs.statSync(path.join(moduleFolder, ".git"));
// Fetch the git or throw error if no remotes
if (this.isGitRepo(moduleFolder)) {
// Folder has .git and has at least one git remote, watch this folder
this.gitRepos.unshift({ module: moduleName, folder: moduleFolder });
}
} catch (err) {
// Error when directory .git doesn't exist or doesn't have any remotes
// This module is not managed with git, skip
}
}
async getStatusInfo(repo) {
let gitInfo = {
module: repo.module,
// commits behind:
behind: 0,
// branch name:
current: "",
// current hash:
hash: "",
// remote branch:
tracking: "",
isBehindInStatus: false
};
let res;
if (repo.module === "default") {
// the hash is only needed for the mm repo
res = await this.execShell("cd " + repo.folder + " && git rev-parse HEAD");
if (res.stderr) {
Log.error("Failed to get current commit hash for " + repo.module + ": " + res.stderr);
}
gitInfo.hash = res.stdout;
}
if (repo.res) {
// mocking
res = repo.res;
} else {
res = await this.execShell("cd " + repo.folder + " && git status -sb");
}
if (res.stderr) {
Log.error("Failed to get git status for " + repo.module + ": " + res.stderr);
// exit without git status info
return;
}
// only the first line of stdout is evaluated
let status = res.stdout.split("\n")[0];
// examples for status:
// ## develop...origin/develop
// ## master...origin/master [behind 8]
status = status.match(/(?![.#])([^.]*)/g);
// examples for status:
// [ ' develop', 'origin/develop', '' ]
// [ ' master', 'origin/master [behind 8]', '' ]
gitInfo.current = status[0].trim();
status = status[1].split(" ");
// examples for status:
// [ 'origin/develop' ]
// [ 'origin/master', '[behind', '8]' ]
gitInfo.tracking = status[0].trim();
if (status[2]) {
// git fetch was already called before so `git status -sb` delivers already the behind number
gitInfo.behind = parseInt(status[2].substring(0, status[2].length - 1));
gitInfo.isBehindInStatus = true;
}
return gitInfo;
}
async getRepoInfo(repo) {
let gitInfo;
if (repo.gitInfo) {
// mocking
gitInfo = repo.gitInfo;
} else {
gitInfo = await this.getStatusInfo(repo);
}
if (!gitInfo) {
return;
}
if (gitInfo.isBehindInStatus) {
return gitInfo;
}
let res;
if (repo.res) {
// mocking
res = repo.res;
} else {
res = await this.execShell("cd " + repo.folder + " && git fetch --dry-run");
}
// example output:
// From https://github.com/MichMich/MagicMirror
// e40ddd4..06389e3 develop -> origin/develop
// here the result is in stderr (this is a git default, don't ask why ...)
const matches = res.stderr.match(this.getRefRegex(gitInfo.current));
if (!matches || !matches[0]) {
// no refs found, nothing to do
return;
}
// get behind with refs
try {
res = await this.execShell("cd " + repo.folder + " && git rev-list --ancestry-path --count " + matches[0]);
gitInfo.behind = parseInt(res.stdout);
return gitInfo;
} catch (err) {
Log.error("Failed to get git revisions for " + repo.module + ": " + err);
}
}
async getStatus() {
const gitResultList = [];
for (let repo of this.gitRepos) {
const gitInfo = await this.getStatusInfo(repo);
if (gitInfo) {
gitResultList.unshift(gitInfo);
}
}
return gitResultList;
}
async getRepos() {
const gitResultList = [];
for (let repo of this.gitRepos) {
const gitInfo = await this.getRepoInfo(repo);
if (gitInfo) {
gitResultList.unshift(gitInfo);
}
}
return gitResultList;
}
}
module.exports.gitHelper = gitHelper;

View File

@@ -1,9 +1,5 @@
const SimpleGit = require("simple-git");
const simpleGits = [];
const fs = require("fs");
const path = require("path");
const GitHelper = require(__dirname + "/git_helper.js");
const defaultModules = require(__dirname + "/../defaultmodules.js");
const Log = require(__dirname + "/../../../js/logger.js");
const NodeHelper = require("node_helper");
module.exports = NodeHelper.create({
@@ -12,34 +8,21 @@ module.exports = NodeHelper.create({
updateTimer: null,
updateProcessStarted: false,
gitHelper: new GitHelper.gitHelper(),
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
// others will be added in front
// 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 + "/../../../")) });
this.gitHelper.add("default");
var promises = [];
for (var moduleName in modules) {
for (let moduleName in modules) {
if (!this.ignoreUpdateChecking(moduleName)) {
// Default modules are included in the main MagicMirror repo
var moduleFolder = path.normalize(__dirname + "/../../" + moduleName);
try {
Log.info("Checking git for module: " + moduleName);
let stat = fs.statSync(path.join(moduleFolder, ".git"));
promises.push(this.resolveRemote(moduleName, moduleFolder));
} catch (err) {
// Error when directory .git doesn't exist
// This module is not managed with git, skip
continue;
}
this.gitHelper.add(moduleName);
}
}
return Promise.all(promises);
},
socketNotificationReceived: function (notification, payload) {
@@ -54,36 +37,10 @@ module.exports = NodeHelper.create({
}
},
resolveRemote: function (moduleName, moduleFolder) {
return new Promise((resolve, reject) => {
var git = SimpleGit(moduleFolder);
git.getRemotes(true, (err, remotes) => {
if (remotes.length < 1 || remotes[0].name.length < 1) {
// No valid remote for folder, skip
return resolve();
}
// Folder has .git and has at least one git remote, watch this folder
simpleGits.unshift({ module: moduleName, git: git });
resolve();
});
});
},
performFetch: function () {
var self = this;
simpleGits.forEach((sg) => {
sg.git.fetch(["--dry-run"]).status((err, data) => {
data.module = sg.module;
if (!err) {
sg.git.log({ "-1": null }, (err, data2) => {
if (!err && data2.latest && "hash" in data2.latest) {
data.hash = data2.latest.hash;
self.sendSocketNotification("STATUS", data);
}
});
}
});
});
performFetch: async function () {
for (let gitInfo of await this.gitHelper.getRepos()) {
this.sendSocketNotification("STATUS", gitInfo);
}
this.scheduleNextFetch(this.config.updateInterval);
},
@@ -93,7 +50,7 @@ module.exports = NodeHelper.create({
delay = 60 * 1000;
}
var self = this;
let self = this;
clearTimeout(this.updateTimer);
this.updateTimer = setTimeout(function () {
self.performFetch();

View File

@@ -5,32 +5,35 @@
* MIT Licensed.
*/
Module.register("updatenotification", {
// Define module defaults
defaults: {
updateInterval: 10 * 60 * 1000, // every 10 minutes
refreshInterval: 24 * 60 * 60 * 1000, // one day
ignoreModules: []
ignoreModules: [],
timeout: 5000
},
suspended: false,
moduleList: {},
// Override start method.
start: function () {
var self = this;
Log.log("Start updatenotification");
Log.info("Starting module: " + this.name);
setInterval(() => {
self.moduleList = {};
self.updateDom(2);
}, self.config.refreshInterval);
this.moduleList = {};
this.updateDom(2);
}, this.config.refreshInterval);
},
notificationReceived: function (notification, payload, sender) {
if (notification === "DOM_OBJECTS_CREATED") {
this.sendSocketNotification("CONFIG", this.config);
this.sendSocketNotification("MODULES", Module.definitions);
//this.hide(0, { lockString: self.identifier });
//this.hide(0, { lockString: this.identifier });
}
},
// Override socket notification handler.
socketNotificationReceived: function (notification, payload) {
if (notification === "STATUS") {
this.updateUI(payload);
@@ -38,13 +41,12 @@ Module.register("updatenotification", {
},
updateUI: function (payload) {
var self = this;
if (payload && payload.behind > 0) {
// if we haven't seen info for this module
if (this.moduleList[payload.module] === undefined) {
// save it
this.moduleList[payload.module] = payload;
self.updateDom(2);
this.updateDom(2);
}
//self.show(1000, { lockString: self.identifier });
} else if (payload && payload.behind === 0) {
@@ -52,41 +54,41 @@ Module.register("updatenotification", {
if (this.moduleList[payload.module] !== undefined) {
// remove it
delete this.moduleList[payload.module];
self.updateDom(2);
this.updateDom(2);
}
}
},
diffLink: function (module, text) {
var localRef = module.hash;
var remoteRef = module.tracking.replace(/.*\//, "");
const localRef = module.hash;
const remoteRef = module.tracking.replace(/.*\//, "");
return '<a href="https://github.com/MichMich/MagicMirror/compare/' + localRef + "..." + remoteRef + '" ' + 'class="xsmall dimmed" ' + 'style="text-decoration: none;" ' + 'target="_blank" >' + text + "</a>";
},
// Override dom generator.
getDom: function () {
var wrapper = document.createElement("div");
const wrapper = document.createElement("div");
if (this.suspended === false) {
// process the hash of module info found
for (var key of Object.keys(this.moduleList)) {
for (const key of Object.keys(this.moduleList)) {
let m = this.moduleList[key];
var message = document.createElement("div");
const message = document.createElement("div");
message.className = "small bright";
var icon = document.createElement("i");
const icon = document.createElement("i");
icon.className = "fa fa-exclamation-circle";
icon.innerHTML = "&nbsp;";
message.appendChild(icon);
var updateInfoKeyName = m.behind === 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
const updateInfoKeyName = m.behind === 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
var subtextHtml = this.translate(updateInfoKeyName, {
let subtextHtml = this.translate(updateInfoKeyName, {
COMMIT_COUNT: m.behind,
BRANCH_NAME: m.current
});
var text = document.createElement("span");
const text = document.createElement("span");
if (m.module === "default") {
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
subtextHtml = this.diffLink(m, subtextHtml);
@@ -99,7 +101,7 @@ Module.register("updatenotification", {
wrapper.appendChild(message);
var subtext = document.createElement("div");
const subtext = document.createElement("div");
subtext.innerHTML = subtextHtml;
subtext.className = "xsmall dimmed";
wrapper.appendChild(subtext);

View File

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

View File

@@ -1,33 +1,37 @@
{% if forecast or weatherData %}
{% if weatherData %}
{% set forecast = weatherData.days %}
{% set numSteps = forecast | calcNumEntries %}
{% else %}
{% set numSteps = forecast | calcNumSteps %}
{% endif %}
{% if forecast %}
{% set numSteps = forecast | calcNumSteps %}
{% set currentStep = 0 %}
<table class="{{ config.tableClass }}">
{% if config.ignoreToday %}
{% set forecast = forecast.splice(1) %}
{% endif %}
{% set forecast = forecast.slice(0, numSteps) %}
{% for f in forecast %}
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
{% if (currentStep == 0) %}
{% if (currentStep == 0) and config.ignoreToday == false %}
<td class="day">{{ "TODAY" | translate }}</td>
{% elif (currentStep == 1) %}
{% elif (currentStep == 1) and config.ignoreToday == false %}
<td class="day">{{ "TOMORROW" | translate }}</td>
{% else %}
<td class="day">{{ f.date.format('ddd') }}</td>
{% endif %}
<td class="bright weather-icon"><span class="wi weathericon wi-{{ f.weatherType }}"></span></td>
<td class="align-right bright max-temp">
{{ f.maxTemperature | roundValue | unit("temperature") }}
{{ f.maxTemperature | roundValue | unit("temperature") | decimalSymbol }}
</td>
<td class="align-right min-temp">
{{ f.minTemperature | roundValue | unit("temperature") }}
{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}
</td>
{% if config.showPrecipitationAmount %}
<td class="align-right bright precipitation">
{{ f.precipitation | unit("precip") }}
</td>
{% if f.precipitationUnits %}
<td class="align-right bright precipitation">
{{ f.precipitation }}{{ f.precipitationUnits }}
</td>
{% else %}
<td class="align-right bright precipitation">
{{ f.precipitation | unit("precip") }}
</td>
{% endif %}
{% endif %}
</tr>
{% set currentStep = currentStep + 1 %}
@@ -35,7 +39,7 @@
</table>
{% else %}
<div class="dimmed light small">
{{ "LOADING" | translate | safe }}
{{ "LOADING" | translate }}
</div>
{% endif %}

View File

@@ -1,7 +1,4 @@
{% if hourly or weatherData %}
{% if weatherData %}
{% set hourly = weatherData.hours %}
{% endif %}
{% if hourly %}
{% set numSteps = hourly | calcNumEntries %}
{% set currentStep = 0 %}
<table class="{{ config.tableClass }}">
@@ -14,9 +11,15 @@
{{ hour.temperature | roundValue | unit("temperature") }}
</td>
{% if config.showPrecipitationAmount %}
<td class="align-right bright precipitation">
{{ hour.precipitation | unit("precip") }}
</td>
{% if hour.precipitationUnits %}
<td class="align-right bright precipitation">
{{ hour.precipitation }}{{ hour.precipitationUnits }}
</td>
{% else %}
<td class="align-right bright precipitation">
{{ hour.precipitation | unit("precip") }}
</td>
{% endif %}
{% endif %}
</tr>
{% set currentStep = currentStep + 1 %}
@@ -24,9 +27,9 @@
</table>
{% else %}
<div class="dimmed light small">
{{ "LOADING" | translate | safe }}
{{ "LOADING" | translate }}
</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `hourly` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{weatherData | dump}}</div> -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{hourly | dump}}</div> -->

View File

@@ -1,133 +1,3 @@
# MagicMirror² Weather Module Weather Provider Development Documentation
# Weather Module Weather Provider Development Documentation
This document describes the way to develop your own MagicMirror² weather module weather provider.
Table of Contents:
- The weather provider file: yourprovider.js
- [Weather provider methods to implement](#weather-provider-methods-to-implement)
- [Weather Provider instance methods](#weather-provider-instance-methods)
- [WeatherObject](#weatherobject)
---
## The weather provider file: yourprovider.js
This is the script in which the weather provider will be defined. In its most simple form, the weather provider must implement the following:
```javascript
WeatherProvider.register("yourprovider", {
providerName: "YourProvider",
fetchCurrentWeather() {},
fetchWeatherForecast() {}
});
```
### Weather provider methods to implement
#### `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.
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);`.
It will then automatically refresh the module DOM with the new data.
#### `fetchWeatherForecast()`
This method is called when the weather module tries to fetch the weather of your provider. The implementation of this method is required.
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);`.
It will then automatically refresh the module DOM with the new data.
### Weather Provider instance methods
#### `init()`
Called when a weather provider is initialized.
#### `setConfig(config)`
Called to set the config, this config is the same as the weather module's config.
#### `start()`
Called when the weather provider is about to start.
#### `currentWeather()`
This returns a WeatherDay object for the current weather.
#### `weatherForecast()`
This returns an array of WeatherDay objects for the weather forecast.
#### `fetchedLocation()`
This returns the name of the fetched location or an empty string.
#### `setCurrentWeather(currentWeatherObject)`
Set the currentWeather and notify the delegate that new information is available.
#### `setWeatherForecast(weatherForecastArray)`
Set the weatherForecastArray and notify the delegate that new information is available.
#### `setFetchedLocation(name)`
Set the fetched location name.
#### `updateAvailable()`
Notify the delegate that new weather is available.
#### `fetchData(url, method, data)`
A convenience function to make requests. It returns a promise.
### WeatherObject
| Property | Type | Value/Unit |
| -------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
| units | `string` | Gets initialized with the constructor. <br> Possible values: `metric`, `imperial` |
| tempUnits | `string` | Gets initialized with the constructor. <br> Possible values: `metric`, `imperial` |
| windUnits | `string` | Gets initialized with the constructor. <br> Possible values: `metric`, `imperial` |
| date | `object` | [Moment.js](https://momentjs.com/) object of the time/date. |
| windSpeed | `number` | Metric: `meter/second` <br> Imperial: `miles/hour` |
| windDirection | `number` | Direction of the wind in degrees. |
| sunrise | `object` | [Moment.js](https://momentjs.com/) object of sunrise. |
| sunset | `object` | [Moment.js](https://momentjs.com/) object of sunset. |
| temperature | `number` | Current temperature |
| minTemperature | `number` | Lowest temperature of the day. |
| maxTemperature | `number` | Highest temperature of the day. |
| weatherType | `string` | Icon name of the weather type. <br> Possible values: [WeatherIcons](https://www.npmjs.com/package/weathericons) |
| humidity | `number` | Percentage of humidity |
| rain | `number` | Metric: `millimeters` <br> Imperial: `inches` |
| snow | `number` | Metric: `millimeters` <br> Imperial: `inches` |
| precipitation | `number` | Metric: `millimeters` <br> Imperial: `inches` <br> UK Met Office provider: `percent` |
#### Current weather
For the current weather object the following properties are required:
- humidity
- sunrise
- sunset
- temperature
- units
- weatherType
- windDirection
- windSpeed
#### Weather forecast
For the forecast weather object the following properties are required:
- date
- maxTemperature
- minTemperature
- rain
- units
- weatherType
For how to develop your own weather provider, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/development/weather-provider.html).

View File

@@ -8,13 +8,23 @@
* MIT Licensed
*
* This class is a provider for Dark Sky.
* Note that the Dark Sky API does not provide rainfall. Instead it provides snowfall and precipitation probability
* Note that the Dark Sky API does not provide rainfall. Instead it provides
* snowfall and precipitation probability
*/
WeatherProvider.register("darksky", {
// Set the name of the provider.
// Not strictly required, but helps for debugging.
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: {
imperial: "us",
metric: "si"
@@ -89,7 +99,8 @@ WeatherProvider.register("darksky", {
weather.snow = 0;
// The API will return centimeters if units is 'si' and will return inches for 'us'
// Note that the Dark Sky API does not provide rainfall. Instead it provides snowfall and precipitation probability
// Note that the Dark Sky API does not provide rainfall.
// Instead it provides snowfall and precipitation probability
if (forecast.hasOwnProperty("precipAccumulation")) {
if (this.config.units === "imperial" && !isNaN(forecast.precipAccumulation)) {
weather.snow = forecast.precipAccumulation;

View File

@@ -0,0 +1,644 @@
/* global WeatherProvider, WeatherObject */
/* Magic Mirror
* Module: Weather
* Provider: Environment Canada (EC)
*
* This class is a provider for Environment Canada MSC Datamart
* Note that this is only for Canadian locations and does not require an API key (access is anonymous)
*
* EC Documentation at following links:
* https://dd.weather.gc.ca/citypage_weather/schema/
* https://eccc-msc.github.io/open-data/msc-datamart/readme_en/
*
* This module supports Canadian locations only and requires 2 additional config parms:
*
* siteCode - the city/town unique identifier for which weather is to be displayed. Format is 's0000000'.
*
* provCode - the 2-character province code for the selected city/town.
*
* Example: for Toronto, Ontario, the following parms would be used
*
* siteCode: 's0000458',
* provCode: 'ON'
*
* To determine the siteCode and provCode values for a Canadian city/town, look at the Environment Canada document
* at https://dd.weather.gc.ca/citypage_weather/docs/site_list_en.csv (or site_list_fr.csv). There you will find a table
* with locations you can search under column B (English Names), with the corresponding siteCode under
* column A (Codes) and provCode under column C (Province).
*
* Original by Kevin Godin
*
* License to use Environment Canada (EC) data is detailed here:
* https://eccc-msc.github.io/open-data/licence/readme_en/
*
*/
WeatherProvider.register("envcanada", {
// Set the name of the provider for debugging and alerting purposes (eg. provide eye-catcher)
providerName: "Environment Canada",
// Set the default config properties that is specific to this provider
defaults: {
siteCode: "s1234567",
provCode: "ON"
},
//
// Set config values (equates to weather module config values). Also set values pertaining to caching of
// Today's temperature forecast (for use in the Forecast functions below)
//
setConfig: function (config) {
this.config = config;
this.todayTempCacheMin = 0;
this.todayTempCacheMax = 0;
this.todayCached = false;
this.cacheCurrentTemp = 999;
},
//
// Called when the weather provider is started
//
start: function () {
Log.info(`Weather provider: ${this.providerName} started.`);
this.setFetchedLocation(this.config.location);
// Ensure kmH are ignored since these are custom-handled by this Provider
this.config.useKmh = false;
},
//
// Override the fetchCurrentWeather method to query EC and construct a Current weather object
//
fetchCurrentWeather() {
this.fetchData(this.getUrl(), "GET")
.then((data) => {
if (!data) {
// Did not receive usable new data.
return;
}
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
this.setCurrentWeather(currentWeather);
})
.catch(function (request) {
Log.error("Could not load EnvCanada site data ... ", request);
})
.finally(() => this.updateAvailable());
},
//
// Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
//
fetchWeatherForecast() {
this.fetchData(this.getUrl(), "GET")
.then((data) => {
if (!data) {
// Did not receive usable new data.
return;
}
const forecastWeather = this.generateWeatherObjectsFromForecast(data);
this.setWeatherForecast(forecastWeather);
})
.catch(function (request) {
Log.error("Could not load EnvCanada forecast data ... ", request);
})
.finally(() => this.updateAvailable());
},
//
// Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
//
fetchWeatherHourly() {
this.fetchData(this.getUrl(), "GET")
.then((data) => {
if (!data) {
// Did not receive usable new data.
return;
}
const hourlyWeather = this.generateWeatherObjectsFromHourly(data);
this.setWeatherHourly(hourlyWeather);
})
.catch(function (request) {
Log.error("Could not load EnvCanada hourly data ... ", request);
})
.finally(() => this.updateAvailable());
},
//
// Override fetchData function to handle XML document (base function assumes JSON)
//
fetchData: function (url, method = "GET", data = null) {
return new Promise(function (resolve, reject) {
const request = new XMLHttpRequest();
request.open(method, url, true);
request.onreadystatechange = function () {
if (this.readyState === 4) {
if (this.status === 200) {
resolve(this.responseXML);
} else {
reject(request);
}
}
};
request.send();
});
},
//////////////////////////////////////////////////////////////////////////////////
//
// Environment Canada methods - not part of the standard Provider methods
//
//////////////////////////////////////////////////////////////////////////////////
//
// Build the EC URL based on the Site Code and Province Code specified in the config parms. Note that the
// URL defaults to the Englsih version simply because there is no language dependancy in the data
// being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
//
// Also note that access is supported through a proxy service (thingproxy.freeboard.io) to mitigate
// CORS errors when accessing EC
//
getUrl() {
return "https://thingproxy.freeboard.io/fetch/https://dd.weather.gc.ca/citypage_weather/xml/" + this.config.provCode + "/" + this.config.siteCode + "_e.xml";
},
//
// Generate a WeatherObject based on current EC weather conditions
//
generateWeatherObjectFromCurrentWeather(ECdoc) {
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
// There are instances where EC will update weather data and current temperature will not be
// provided. While this is a defect in the EC systems, we need to accommodate to avoid a current temp
// of NaN being displayed. Therefore... whenever we get a valid current temp from EC, we will cache
// the value. Whenever EC data is missing current temp, we will provide the cached value
// instead. This is reasonable since the cached value will typically be accurate within the previous
// hour. The only time this does not work as expected is when MM is restarted and the first query to
// EC finds no current temp. In this scenario, MM will end up displaying a current temp of null;
if (ECdoc.querySelector("siteData currentConditions temperature").textContent) {
currentWeather.temperature = this.convertTemp(ECdoc.querySelector("siteData currentConditions temperature").textContent);
this.cacheCurrentTemp = currentWeather.temperature;
} else {
currentWeather.temperature = this.cacheCurrentTemp;
}
currentWeather.windSpeed = this.convertWind(ECdoc.querySelector("siteData currentConditions wind speed").textContent);
currentWeather.windDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent;
// Ensure showPrecipitationAmount is forced to false. EC does not really provide POP for current day
// and this feature for the weather module (current only) is sort of broken in that it wants
// to say POP but will display precip as an accumulated amount vs. a percentage.
this.config.showPrecipitationAmount = false;
//
// If the module config wants to showFeelsLike... default to the current temperature.
// Check for EC wind chill and humidex values and overwrite the feelsLikeTemp value.
// This assumes that the EC current conditions will never contain both a wind chill
// and humidex temperature.
//
if (this.config.showFeelsLike) {
currentWeather.feelsLikeTemp = currentWeather.temperature;
if (ECdoc.querySelector("siteData currentConditions windChill")) {
currentWeather.feelsLikeTemp = this.convertTemp(ECdoc.querySelector("siteData currentConditions windChill").textContent);
}
if (ECdoc.querySelector("siteData currentConditions humidex")) {
currentWeather.feelsLikeTemp = this.convertTemp(ECdoc.querySelector("siteData currentConditions humidex").textContent);
}
}
//
// Need to map EC weather icon to MM weatherType values
//
currentWeather.weatherType = this.convertWeatherType(ECdoc.querySelector("siteData currentConditions iconCode").textContent);
//
// Capture the sunrise and sunset values from EC data
//
const sunList = ECdoc.querySelectorAll("siteData riseSet dateTime");
currentWeather.sunrise = moment(sunList[1].querySelector("timeStamp").textContent, "YYYYMMDDhhmmss");
currentWeather.sunset = moment(sunList[3].querySelector("timeStamp").textContent, "YYYYMMDDhhmmss");
return currentWeather;
},
//
// Generate an array of WeatherObjects based on EC weather forecast
//
generateWeatherObjectsFromForecast(ECdoc) {
// Declare an array to hold each day's forecast object
const days = [];
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
const foreBaseDates = ECdoc.querySelectorAll("siteData forecastGroup dateTime");
const baseDate = foreBaseDates[1].querySelector("timeStamp").textContent;
weather.date = moment(baseDate, "YYYYMMDDhhmmss");
const foreGroup = ECdoc.querySelectorAll("siteData forecastGroup forecast");
// For simplicity, we will only accumulate precipitation and will not try to break out
// rain vs snow accumulations
weather.rain = null;
weather.snow = null;
weather.precipitation = null;
//
// The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
// 2 elements. the first element for a day details the Today (daytime) forecast while the second
// element details the Tonight (nightime) forecast. Element 0 is always for the current day.
//
// However... the forecast is somewhat 'rolling'.
//
// If the EC forecast is queried in the morning, then Element 0 will contain Current
// Today and Element 1 will contain Current Tonight. From there, the next 5 days of forecast will be
// contained in Elements 2/3, 4/5, 6/7, 8/9, and 10/11. This module will create a 6-day forecast using
// all of these Elements.
//
// But, if the EC forecast is queried in late afternoon, the Current Today forecast will be rolled
// off and Element 0 will contain Current Tonight. From there, the next 5 days will be contained in
// Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Elelement 11 will contain a forecast for a 6th day,
// but only for the Today portion (not Tonight). This module will create a 6-day forecast using
// Elements 0 to 11, and will ignore the additional Todat forecast in Element 11.
//
// We need to determine if Element 0 is showing the forecast for Current Today or Current Tonight.
// This is required to understand how Min and Max temperature will be determined, and to understand
// where the next day's (aka Tomorrow's) forecast is located in the forecast array.
//
let nextDay = 0;
let lastDay = 0;
const currentTemp = ECdoc.querySelector("siteData currentConditions temperature").textContent;
//
// If the first Element is Current Today, look at Current Today and Current Tonight for the current day.
//
if (foreGroup[0].querySelector("period[textForecastName='Today']")) {
this.todaytempCacheMin = 0;
this.todaytempCacheMax = 0;
this.todayCached = true;
this.setMinMaxTemps(weather, foreGroup, 0, true, currentTemp);
this.setPrecipitation(weather, foreGroup, 0);
//
// Set the Element number that will reflect where the next day's forecast is located. Also set
// the Element number where the end of the forecast will be. This is important because of the
// rolling nature of the EC forecast. In the current scenario (Today and Tonight are present
// in elements 0 and 11, we know that we will have 6 full days of forecasts and we will use
// them. We will set lastDay such that we iterate through all 12 elements of the forecast.
//
nextDay = 2;
lastDay = 12;
}
//
// If the first Element is Current Tonight, look at Tonight only for the current day.
//
if (foreGroup[0].querySelector("period[textForecastName='Tonight']")) {
this.setMinMaxTemps(weather, foreGroup, 0, false, currentTemp);
this.setPrecipitation(weather, foreGroup, 0);
//
// Set the Element number that will reflect where the next day's forecast is located. Also set
// the Element number where the end of the forecast will be. This is important because of the
// rolling nature of the EC forecast. In the current scenario (only Current Tonight is present
// in Element 0, we know that we will have 6 full days of forecasts PLUS a half-day and
// forecast in the final element. Because we will only use full day forecasts, we set the
// lastDay number to ensure we ignore that final half-day (in forecast Element 11).
//
nextDay = 1;
lastDay = 11;
}
//
// Need to map EC weather icon to MM weatherType values. Always pick the first Element's icon to
// reflect either Today or Tonight depending on what the forecast is showing in Element 0.
//
weather.weatherType = this.convertWeatherType(foreGroup[0].querySelector("abbreviatedForecast iconCode").textContent);
// Push the weather object into the forecast array.
days.push(weather);
//
// Now do the the rest of the forecast starting at nextDay. We will process each day using 2 EC
// forecast Elements. This will address the fact that the EC forecast always includes Today and
// Tonight for each day. This is why we iterate through the forecast by a a count of 2, with each
// iteration looking at the current Element and the next Element.
//
let lastDate = moment(baseDate, "YYYYMMDDhhmmss");
for (let stepDay = nextDay; stepDay < lastDay; stepDay += 2) {
let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
// Add 1 to the date to reflect the current forecast day we are building
lastDate = lastDate.add(1, "day");
weather.date = moment(lastDate, "X");
// Capture the temperatures for the current Element and the next Element in order to set
// the Min and Max temperatures for the forecast
this.setMinMaxTemps(weather, foreGroup, stepDay, true, currentTemp);
weather.rain = null;
weather.snow = null;
weather.precipitation = null;
this.setPrecipitation(weather, foreGroup, stepDay);
//
// Need to map EC weather icon to MM weatherType values. Always pick the first Element icon.
//
weather.weatherType = this.convertWeatherType(foreGroup[stepDay].querySelector("abbreviatedForecast iconCode").textContent);
// Push the weather object into the forecast array.
days.push(weather);
}
return days;
},
//
// Generate an array of WeatherObjects based on EC hourly weather forecast
//
generateWeatherObjectsFromHourly(ECdoc) {
// Declare an array to hold each hour's forecast object
const hours = [];
// Get local timezone UTC offset so that each hourly time can be calculated properly
const baseHours = ECdoc.querySelectorAll("siteData hourlyForecastGroup dateTime");
const hourOffset = baseHours[1].getAttribute("UTCOffset");
//
// The EC hourly forecast is held in a 24-element array - Elements 0 to 23 - with Element 0 holding
// the forecast for the next 'on the hour' timeslot. This means the array is a rolling 24 hours.
//
const hourGroup = ECdoc.querySelectorAll("siteData hourlyForecastGroup hourlyForecast");
for (let stepHour = 0; stepHour < 24; stepHour += 1) {
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
// Determine local time by applying UTC offset to the forecast timestamp
const foreTime = moment(hourGroup[stepHour].getAttribute("dateTimeUTC"), "YYYYMMDDhhmmss");
const currTime = foreTime.add(hourOffset, "hours");
weather.date = moment(currTime, "X");
// Capture the temperature
weather.temperature = this.convertTemp(hourGroup[stepHour].querySelector("temperature").textContent);
// Capture Likelihood of Precipitation (LOP) and unit-of-measure values
const precipLOP = hourGroup[stepHour].querySelector("lop").textContent * 1.0;
if (precipLOP > 0) {
weather.precipitation = precipLOP;
weather.precipitationUnits = hourGroup[stepHour].querySelector("lop").getAttribute("units");
}
//
// Need to map EC weather icon to MM weatherType values. Always pick the first Element icon.
//
weather.weatherType = this.convertWeatherType(hourGroup[stepHour].querySelector("iconCode").textContent);
// Push the weather object into the forecast array.
hours.push(weather);
}
return hours;
},
//
// Determine Min and Max temp based on a supplied Forecast Element index and a boolen that denotes if
// the next Forecast element should be considered - i.e. look at Today *and* Tonight vs.Tonight-only
//
setMinMaxTemps(weather, foreGroup, today, fullDay, currentTemp) {
const todayTemp = foreGroup[today].querySelector("temperatures temperature").textContent;
const todayClass = foreGroup[today].querySelector("temperatures temperature").getAttribute("class");
//
// The following logic is largely aimed at accommodating the Current day's forecast whereby we
// can have either Current Today+Current Tonight or only Current Tonight.
//
// If fullDay is false, then we only have Tonight for the current day's forecast - meaning we have
// lost a min or max temp value for the day. Therefore, we will see if we were able to cache the the
// Today forecast for the current day. If we have, we will use them. If we do not have the cached values,
// it means that MM or the Computer has been restarted since the time EC rolled off Today from the
// forecast. In this scenario, we will simply default to the Current Conditions temperature and then
// check the Tonight temperature.
//
if (fullDay === false) {
if (this.todayCached === true) {
weather.minTemperature = this.todayTempCacheMin;
weather.maxTemperature = this.todayTempCacheMax;
} else {
weather.minTemperature = this.convertTemp(currentTemp);
weather.maxTemperature = weather.minTemperature;
}
}
//
// We will check to see if the current Element's temperature is Low or High and set weather values
// accordingly. We will also check the condition where fullDay is true *and* we are looking at forecast
// element 0. This is a special case where we will cache temperature values so that we have them later
// in the current day when the Current Today element rolls off and we have Current Tonight only.
//
if (todayClass === "low") {
weather.minTemperature = this.convertTemp(todayTemp);
if (today === 0 && fullDay === true) {
this.todayTempCacheMin = weather.minTemperature;
}
}
if (todayClass === "high") {
weather.maxTemperature = this.convertTemp(todayTemp);
if (today === 0 && fullDay === true) {
this.todayTempCacheMax = weather.maxTemperature;
}
}
const nextTemp = foreGroup[today + 1].querySelector("temperatures temperature").textContent;
const nextClass = foreGroup[today + 1].querySelector("temperatures temperature").getAttribute("class");
if (fullDay === true) {
if (nextClass === "low") {
weather.minTemperature = this.convertTemp(nextTemp);
}
if (nextClass === "high") {
weather.maxTemperature = this.convertTemp(nextTemp);
}
}
},
//
// Check for a Precipitation forecast. EC can provide a forecast in 2 ways: either an accumulation figure
// or a POP percentage. If there is a POP, then that is what the module will show. If there is an accumulation,
// then it will be displayed ONLY if no POP is present.
//
// POP Logic: By default, we want to show the POP for 'daytime' since we are presuming that is what
// people are more interested in seeing. While EC provides a separate POP for daytime and nightime portions
// of each day, the weather module does not really allow for that view of a daily forecast. There we will
// ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
// the nightime forecast after a certain point in the afternoon. As such, we will be showing the nightime POP
// (if one exists) in that specific scenario.
//
// Accumulation Logic: Similar to POP, we want to show accumulation for 'daytime' since we presume that is what
// people are interested in seeing. While EC provides a separate accumulation for daytime and nightime portions
// of each day, the weather module does not really allow for that view of a daily forecast. There we will
// ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
// the nightime forecast after a certain point in that specific scenario.
//
setPrecipitation(weather, foreGroup, today) {
if (foreGroup[today].querySelector("precipitation accumulation")) {
weather.precipitation = foreGroup[today].querySelector("precipitation accumulation amount").textContent * 1.0;
weather.precipitationUnits = " " + foreGroup[today].querySelector("precipitation accumulation amount").getAttribute("units");
if (this.config.units === "imperial") {
if (weather.precipitationUnits === " cm") {
weather.precipitation = (weather.precipitation * 0.394).toFixed(2);
weather.precipitationUnits = " in";
}
if (weather.precipitationUnits === " mm") {
weather.precipitation = (weather.precipitation * 0.0394).toFixed(2);
weather.precipitationUnits = " in";
}
}
}
// Check Today element for POP
if (foreGroup[today].querySelector("abbreviatedForecast pop").textContent > 0) {
weather.precipitation = foreGroup[today].querySelector("abbreviatedForecast pop").textContent;
weather.precipitationUnits = foreGroup[today].querySelector("abbreviatedForecast pop").getAttribute("units");
}
},
//
// Unit conversions
//
//
// Convert C to F temps
//
convertTemp(temp) {
if (this.config.tempUnits === "imperial") {
return 1.8 * temp + 32;
} else {
return temp;
}
},
//
// Convert km/h to mph
//
convertWind(kilo) {
if (this.config.windUnits === "imperial") {
return kilo / 1.609344;
} else {
return kilo;
}
},
//
// Convert the icons to a more usable name.
//
convertWeatherType(weatherType) {
const weatherTypes = {
"00": "day-sunny",
"01": "day-sunny",
"02": "day-sunny-overcast",
"03": "day-cloudy",
"04": "day-cloudy",
"05": "day-cloudy",
"06": "day-sprinkle",
"07": "day-showers",
"08": "day-snow",
"09": "day-thunderstorm",
10: "cloud",
11: "showers",
12: "rain",
13: "rain",
14: "sleet",
15: "sleet",
16: "snow",
17: "snow",
18: "snow",
19: "thunderstorm",
20: "cloudy",
21: "cloudy",
22: "day-cloudy",
23: "day-haze",
24: "fog",
25: "snow-wind",
26: "sleet",
27: "sleet",
28: "rain",
29: "na",
30: "night-clear",
31: "night-clear",
32: "night-partly-cloudy",
33: "night-alt-cloudy",
34: "night-alt-cloudy",
35: "night-partly-cloudy",
36: "night-alt-showers",
37: "night-rain-mix",
38: "night-alt-snow",
39: "night-thunderstorm",
40: "snow-wind",
41: "tornado",
42: "tornado",
43: "windy",
44: "smoke",
45: "sandstorm",
46: "thunderstorm",
47: "thunderstorm",
48: "tornado"
};
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
}
});

View File

@@ -14,20 +14,30 @@ WeatherProvider.register("openweathermap", {
// But for debugging (and future alerts) it would be nice to have the real name.
providerName: "OpenWeatherMap",
// Set the default config properties that is specific to this provider
defaults: {
apiVersion: "2.5",
apiBase: "https://api.openweathermap.org/data/",
weatherEndpoint: "", // can be "onecall", "forecast" or "weather" (for current)
locationID: false,
location: false,
lat: 0, // the onecall endpoint needs lat / lon values, it doesn'T support the locationId
lon: 0,
apiKey: ""
},
// Overwrite the fetchCurrentWeather method.
fetchCurrentWeather() {
this.fetchData(this.getUrl())
.then((data) => {
if (!data || !data.main || typeof data.main.temp === "undefined") {
// Did not receive usable new data.
// Maybe this needs a better check?
return;
if (this.config.weatherEndpoint === "/onecall") {
const weatherData = this.generateWeatherObjectsFromOnecall(data);
this.setCurrentWeather(weatherData.current);
this.setFetchedLocation(`${data.timezone}`);
} else {
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
this.setCurrentWeather(currentWeather);
}
this.setFetchedLocation(`${data.name}, ${data.sys.country}`);
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
this.setCurrentWeather(currentWeather);
})
.catch(function (request) {
Log.error("Could not load data ... ", request);
@@ -39,16 +49,15 @@ WeatherProvider.register("openweathermap", {
fetchWeatherForecast() {
this.fetchData(this.getUrl())
.then((data) => {
if (!data || !data.list || !data.list.length) {
// Did not receive usable new data.
// Maybe this needs a better check?
return;
if (this.config.weatherEndpoint === "/onecall") {
const weatherData = this.generateWeatherObjectsFromOnecall(data);
this.setWeatherForecast(weatherData.days);
this.setFetchedLocation(`${data.timezone}`);
} else {
const forecast = this.generateWeatherObjectsFromForecast(data.list);
this.setWeatherForecast(forecast);
this.setFetchedLocation(`${data.city.name}, ${data.city.country}`);
}
this.setFetchedLocation(`${data.city.name}, ${data.city.country}`);
const forecast = this.generateWeatherObjectsFromForecast(data.list);
this.setWeatherForecast(forecast);
})
.catch(function (request) {
Log.error("Could not load data ... ", request);
@@ -56,8 +65,8 @@ WeatherProvider.register("openweathermap", {
.finally(() => this.updateAvailable());
},
// Overwrite the fetchWeatherData method.
fetchWeatherData() {
// Overwrite the fetchWeatherHourly method.
fetchWeatherHourly() {
this.fetchData(this.getUrl())
.then((data) => {
if (!data) {
@@ -69,7 +78,7 @@ WeatherProvider.register("openweathermap", {
this.setFetchedLocation(`(${data.lat},${data.lon})`);
const weatherData = this.generateWeatherObjectsFromOnecall(data);
this.setWeatherData(weatherData);
this.setWeatherHourly(weatherData.hours);
})
.catch(function (request) {
Log.error("Could not load data ... ", request);
@@ -77,6 +86,31 @@ WeatherProvider.register("openweathermap", {
.finally(() => this.updateAvailable());
},
/**
* Overrides method for setting config to check if endpoint is correct for hourly
*
* @param {object} config The configuration object
*/
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 */
/*
* Gets the complete url for the request
@@ -428,6 +462,8 @@ WeatherProvider.register("openweathermap", {
} else {
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) {
params += "id=" + this.config.locationID;
} else if (this.config.location) {

View File

@@ -1,4 +1,4 @@
/* global WeatherProvider, WeatherObject, SunCalc */
/* global WeatherProvider, WeatherObject */
/* Magic Mirror
* Module: Weather
@@ -7,13 +7,19 @@
* By BuXXi https://github.com/buxxi
* MIT Licensed
*
* This class is a provider for SMHI (Sweden only).
* Note that SMHI doesn't provide sunrise and sundown, use SunCalc to calculate it.
* Metric system is the only supported unit.
* This class is a provider for SMHI (Sweden only). Metric system is the only
* supported unit.
*/
WeatherProvider.register("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
*/
@@ -49,25 +55,26 @@ WeatherProvider.register("smhi", {
/**
* Overrides method for setting config with checks for the precipitationValue being unset or invalid
*
* @param config
* @param {object} config The configuration object
*/
setConfig(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);
config.precipitationValue = "pmedian";
config.precipitationValue = this.defaults.precipitationValue;
}
},
/**
* Of all the times returned find out which one is closest to the current time, should be the first if the data isn't old.
*
* @param times
* @param {object[]} times Array of time objects
* @returns {object} The weatherdata closest to the current time
*/
getClosestToCurrentTime(times) {
let now = moment();
let minDiff = undefined;
for (time of times) {
for (const time of times) {
let diff = Math.abs(moment(time.validTime).diff(now));
if (!minDiff || diff < Math.abs(moment(minDiff.validTime).diff(now))) {
minDiff = time;
@@ -78,6 +85,8 @@ WeatherProvider.register("smhi", {
/**
* Get the forecast url for the configured coordinates
*
* @returns {string} the url for the specified coordinates
*/
getURL() {
let lon = this.config.lon;
@@ -90,25 +99,25 @@ WeatherProvider.register("smhi", {
* The returned units is always in metric system.
* Requires coordinates to determine if its daytime or nighttime to know which icon to use and also to set sunrise and sunset.
*
* @param weatherData
* @param coordinates
* @param weatherData
* @param coordinates
* @param {object} weatherData Weatherdata to convert
* @param {object} coordinates Coordinates of the locations of the weather
* @returns {WeatherObject} The converted weatherdata at the specified location
*/
convertWeatherDataToObject(weatherData, coordinates) {
let currentWeather = new WeatherObject("metric", "metric", "metric"); //Weather data is only for Sweden and nobody in Sweden would use imperial
// Weather data is only for Sweden and nobody in Sweden would use imperial
let currentWeather = new WeatherObject("metric", "metric", "metric");
currentWeather.date = moment(weatherData.validTime);
let times = SunCalc.getTimes(currentWeather.date.toDate(), coordinates.lat, coordinates.lon);
currentWeather.sunrise = moment(times.sunrise, "X");
currentWeather.sunset = moment(times.sunset, "X");
currentWeather.updateSunTime(coordinates.lat, coordinates.lon);
currentWeather.humidity = this.paramValue(weatherData, "r");
currentWeather.temperature = this.paramValue(weatherData, "t");
currentWeather.windSpeed = this.paramValue(weatherData, "ws");
currentWeather.windDirection = this.paramValue(weatherData, "wd");
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), this.isDayTime(currentWeather));
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), currentWeather.isDayTime());
//Determine the precipitation amount and category and update the weatherObject with it, the valuetype to use can be configured or uses median as default.
// Determine the precipitation amount and category and update the
// weatherObject with it, the valuetype to use can be configured or uses
// median as default.
let precipitationValue = this.paramValue(weatherData, this.config.precipitationValue);
switch (this.paramValue(weatherData, "pcat")) {
// 0 = No precipitation
@@ -136,19 +145,18 @@ WeatherProvider.register("smhi", {
/**
* Takes all of the data points and converts it to one WeatherObject per day.
*
* @param allWeatherData
* @param coordinates
* @param allWeatherData
* @param coordinates
* @param {object[]} allWeatherData Array of weatherdata
* @param {object} coordinates Coordinates of the locations of the weather
* @returns {WeatherObject[]} Array of weatherobjects
*/
convertWeatherDataGroupedByDay(allWeatherData, coordinates) {
var currentWeather;
let currentWeather;
let result = [];
let allWeatherObjects = this.fillInGaps(allWeatherData).map((weatherData) => this.convertWeatherDataToObject(weatherData, coordinates));
var dayWeatherTypes = [];
let dayWeatherTypes = [];
for (weatherObject of allWeatherObjects) {
for (const weatherObject of allWeatherObjects) {
//If its the first object or if a day change we need to reset the summary object
if (!currentWeather || !currentWeather.date.isSame(weatherObject.date, "day")) {
currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
@@ -163,7 +171,7 @@ WeatherProvider.register("smhi", {
}
//Keep track of what icons has been used for each hour of daytime and use the middle one for the forecast
if (this.isDayTime(weatherObject)) {
if (weatherObject.isDayTime()) {
dayWeatherTypes.push(weatherObject.weatherType);
}
if (dayWeatherTypes.length > 0) {
@@ -184,37 +192,31 @@ WeatherProvider.register("smhi", {
},
/**
* Resolve coordinates from the response data (probably preferably to use this if it's not matching the config values exactly)
* Resolve coordinates from the response data (probably preferably to use
* this if it's not matching the config values exactly)
*
* @param data
* @param {object} data Response data from the weather service
* @returns {{lon, lat}} the lat/long coordinates of the data
*/
resolveCoordinates(data) {
return { lat: data.geometry.coordinates[0][1], lon: data.geometry.coordinates[0][0] };
},
/**
* Checks if the weatherObject is at dayTime.
*
* @param weatherObject
*/
isDayTime(weatherObject) {
return weatherObject.date.isBetween(weatherObject.sunrise, weatherObject.sunset, undefined, "[]");
},
/**
* The distance between the data points is increasing in the data the more distant the prediction is.
* Find these gaps and fill them with the previous hours data to make the data returned a complete set.
*
* @param data
* @param {object[]} data Response data from the weather service
* @returns {object[]} Given data with filled gaps
*/
fillInGaps(data) {
let result = [];
for (var i = 1; i < data.length; i++) {
for (let i = 1; i < data.length; i++) {
let to = moment(data[i].validTime);
let from = moment(data[i - 1].validTime);
let hours = moment.duration(to.diff(from)).asHours();
// For each hour add a datapoint but change the validTime
for (var j = 0; j < hours; j++) {
for (let j = 0; j < hours; j++) {
let current = Object.assign({}, data[i]);
current.validTime = from.clone().add(j, "hours").toISOString();
result.push(current);
@@ -224,84 +226,81 @@ WeatherProvider.register("smhi", {
},
/**
* Helper method to fetch a property from the returned data set.
* The returned values is an array with always one value in it.
* Helper method to get a property from the returned data set.
*
* @param currentWeatherData
* @param name
* @param currentWeatherData
* @param name
* @param {object} currentWeatherData Weatherdata to get from
* @param {string} name The name of the property
* @returns {*} The value of the property in the weatherdata
*/
paramValue(currentWeatherData, name) {
return currentWeatherData.parameters.filter((p) => p.name == name).flatMap((p) => p.values)[0];
return currentWeatherData.parameters.filter((p) => p.name === name).flatMap((p) => p.values)[0];
},
/**
* Map the icon value from SHMI to an icon that MagicMirror understands.
* Map the icon value from SMHI to an icon that MagicMirror understands.
* Uses different icons depending if its daytime or nighttime.
* SHMI's description of what the numeric value means is the comment after the case.
* SMHI's description of what the numeric value means is the comment after the case.
*
* @param input
* @param isDayTime
* @param input
* @param isDayTime
* @param {number} input The SMHI icon value
* @param {boolean} isDayTime True if the icon should be for daytime, false for nighttime
* @returns {string} The icon name for the MagicMirror
*/
convertWeatherType(input, isDayTime) {
switch (input) {
case 1:
return isDayTime ? "day-sunny" : "night-clear"; // Clear sky
case 2:
return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; //Nearly clear sky
return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; // Nearly clear sky
case 3:
return isDayTime ? "day-cloudy" : "night-cloudy"; //Variable cloudiness
return isDayTime ? "day-cloudy" : "night-cloudy"; // Variable cloudiness
case 4:
return isDayTime ? "day-cloudy" : "night-cloudy"; //Halfclear sky
return isDayTime ? "day-cloudy" : "night-cloudy"; // Halfclear sky
case 5:
return "cloudy"; //Cloudy sky
return "cloudy"; // Cloudy sky
case 6:
return "cloudy"; //Overcast
return "cloudy"; // Overcast
case 7:
return "fog"; //Fog
return "fog"; // Fog
case 8:
return "showers"; //Light rain showers
return "showers"; // Light rain showers
case 9:
return "showers"; //Moderate rain showers
return "showers"; // Moderate rain showers
case 10:
return "showers"; //Heavy rain showers
return "showers"; // Heavy rain showers
case 11:
return "thunderstorm"; //Thunderstorm
return "thunderstorm"; // Thunderstorm
case 12:
return "sleet"; //Light sleet showers
return "sleet"; // Light sleet showers
case 13:
return "sleet"; //Moderate sleet showers
return "sleet"; // Moderate sleet showers
case 14:
return "sleet"; //Heavy sleet showers
return "sleet"; // Heavy sleet showers
case 15:
return "snow"; //Light snow showers
return "snow"; // Light snow showers
case 16:
return "snow"; //Moderate snow showers
return "snow"; // Moderate snow showers
case 17:
return "snow"; //Heavy snow showers
return "snow"; // Heavy snow showers
case 18:
return "rain"; //Light rain
return "rain"; // Light rain
case 19:
return "rain"; //Moderate rain
return "rain"; // Moderate rain
case 20:
return "rain"; //Heavy rain
return "rain"; // Heavy rain
case 21:
return "thunderstorm"; //Thunder
return "thunderstorm"; // Thunder
case 22:
return "sleet"; // Light sleet
case 23:
return "sleet"; //Moderate sleet
return "sleet"; // Moderate sleet
case 24:
return "sleet"; // Heavy sleet
case 25:
return "snow"; // Light snowfall
case 26:
return "snow"; //Moderate snowfall
return "snow"; // Moderate snowfall
case 27:
return "snow"; //Heavy snowfall
return "snow"; // Heavy snowfall
default:
return "";
}

View File

@@ -1,4 +1,4 @@
/* global WeatherProvider, WeatherObject, SunCalc */
/* global WeatherProvider, WeatherObject */
/* Magic Mirror
* Module: Weather
@@ -14,6 +14,13 @@ WeatherProvider.register("ukmetoffice", {
// But for debugging (and future alerts) it would be nice to have the real name.
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: {
imperial: "us",
metric: "si"
@@ -74,6 +81,7 @@ WeatherProvider.register("ukmetoffice", {
*/
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
const location = currentWeatherData.SiteRep.DV.Location;
// data times are always UTC
let nowUtc = moment.utc();
@@ -81,8 +89,8 @@ WeatherProvider.register("ukmetoffice", {
let timeInMins = nowUtc.diff(midnightUtc, "minutes");
// loop round each of the (5) periods, look for today (the first period may be yesterday)
for (var i in currentWeatherData.SiteRep.DV.Location.Period) {
let periodDate = moment.utc(currentWeatherData.SiteRep.DV.Location.Period[i].value.substr(0, 10), "YYYY-MM-DD");
for (const period of location.Period) {
const periodDate = moment.utc(period.value.substr(0, 10), "YYYY-MM-DD");
// ignore if period is before today
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
@@ -90,17 +98,17 @@ WeatherProvider.register("ukmetoffice", {
if (moment().diff(periodDate, "minutes") > 0) {
// loop round the reports looking for the one we are in
// $ value specifies the time in minutes-of-the-day: 0, 180, 360,...1260
for (var j in currentWeatherData.SiteRep.DV.Location.Period[i].Rep) {
let p = currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].$;
for (const rep of period.Rep) {
const p = rep.$;
if (timeInMins >= p && timeInMins - 180 < p) {
// finally got the one we want, so populate weather object
currentWeather.humidity = currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].H;
currentWeather.temperature = this.convertTemp(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].T);
currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].F);
currentWeather.precipitation = parseInt(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].Pp);
currentWeather.windSpeed = this.convertWindSpeed(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].S);
currentWeather.windDirection = this.convertWindDirection(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].D);
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].W);
currentWeather.humidity = rep.H;
currentWeather.temperature = this.convertTemp(rep.T);
currentWeather.feelsLikeTemp = this.convertTemp(rep.F);
currentWeather.precipitation = parseInt(rep.Pp);
currentWeather.windSpeed = this.convertWindSpeed(rep.S);
currentWeather.windDirection = this.convertWindDirection(rep.D);
currentWeather.weatherType = this.convertWeatherType(rep.W);
}
}
}
@@ -108,9 +116,7 @@ WeatherProvider.register("ukmetoffice", {
}
// determine the sunrise/sunset times - not supplied in UK Met Office data
let times = this.calcAstroData(currentWeatherData.SiteRep.DV.Location);
currentWeather.sunrise = times[0];
currentWeather.sunset = times[1];
currentWeather.updateSunTime(location.lat, location.lon);
return currentWeather;
},
@@ -123,21 +129,21 @@ WeatherProvider.register("ukmetoffice", {
// loop round the (5) periods getting the data
// for each period array, Day is [0], Night is [1]
for (var j in forecasts.SiteRep.DV.Location.Period) {
for (const period of forecasts.SiteRep.DV.Location.Period) {
const weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
// data times are always UTC
const dateStr = forecasts.SiteRep.DV.Location.Period[j].value;
const dateStr = period.value;
let periodDate = moment.utc(dateStr.substr(0, 10), "YYYY-MM-DD");
// ignore if period is before today
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
// populate the weather object
weather.date = moment.utc(dateStr.substr(0, 10), "YYYY-MM-DD");
weather.minTemperature = this.convertTemp(forecasts.SiteRep.DV.Location.Period[j].Rep[1].Nm);
weather.maxTemperature = this.convertTemp(forecasts.SiteRep.DV.Location.Period[j].Rep[0].Dm);
weather.weatherType = this.convertWeatherType(forecasts.SiteRep.DV.Location.Period[j].Rep[0].W);
weather.precipitation = parseInt(forecasts.SiteRep.DV.Location.Period[j].Rep[0].PPd);
weather.minTemperature = this.convertTemp(period.Rep[1].Nm);
weather.maxTemperature = this.convertTemp(period.Rep[0].Dm);
weather.weatherType = this.convertWeatherType(period.Rep[0].W);
weather.precipitation = parseInt(period.Rep[0].PPd);
days.push(weather);
}
@@ -146,20 +152,6 @@ WeatherProvider.register("ukmetoffice", {
return days;
},
/*
* calculate the astronomical data
*/
calcAstroData(location) {
const sunTimes = [];
// determine the sunrise/sunset times
let times = SunCalc.getTimes(new Date(), location.lat, location.lon);
sunTimes.push(moment(times.sunrise, "X"));
sunTimes.push(moment(times.sunset, "X"));
return sunTimes;
},
/*
* Convert the Met Office icons to a more usable name.
*/
@@ -240,16 +232,16 @@ WeatherProvider.register("ukmetoffice", {
return windCardinals.hasOwnProperty(windDirection) ? windCardinals[windDirection] : null;
},
/*
/**
* Generates an url with api parameters based on the config.
*
* return String - URL params.
* @param {string} forecastType daily or 3hourly forecast
* @returns {string} url
*/
getParams(forecastType) {
let params = "?";
params += "res=" + forecastType;
params += "&key=" + this.config.apiKey;
return params;
}
});

View File

@@ -1,3 +1,5 @@
/* global WeatherProvider, WeatherObject */
/* Magic Mirror
* Module: Weather
*
@@ -11,9 +13,8 @@
* Hourly data for next 2 days ("hourly") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-hourly.pdf
* 3-hourly data for the next 7 days ("3hourly") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-3-hourly.pdf
* Daily data for the next 7 days ("daily") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-daily.pdf
*/
/* NOTES
*
* NOTES
* This provider requires longitude/latitude coordinates, rather than a location ID (as with the previous Met Office provider)
* Provide the following in your config.js file:
* weatherProvider: "ukmetofficedatahub",
@@ -44,14 +45,22 @@ WeatherProvider.register("ukmetofficedatahub", {
// Set the name of the provider.
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)
getUrl(forecastType) {
let queryStrings = "?";
queryStrings += "latitude=" + this.config.lat;
queryStrings += "&longitude=" + this.config.lon;
if (this.config.appendLocationNameToHeader) {
queryStrings += "&includeLocationName=" + true;
}
queryStrings += "&includeLocationName=" + true;
// Return URL, making sure there is a trailing "/" in the base URL.
return this.config.apiBase + (this.config.apiBase.endsWith("/") ? "" : "/") + forecastType + queryStrings;
@@ -61,13 +70,11 @@ WeatherProvider.register("ukmetofficedatahub", {
// For DataHub requests, the API key/secret are sent in the headers rather than as query strings.
// Headers defined according to Data Hub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
getHeaders() {
let headers = {
return {
accept: "application/json",
"x-ibm-client-id": this.config.apiKey,
"x-ibm-client-secret": this.config.apiSecret
};
return headers;
},
// Fetch data using supplied URL and request headers
@@ -83,7 +90,7 @@ WeatherProvider.register("ukmetofficedatahub", {
this.fetchWeather(this.getUrl("hourly"), this.getHeaders())
.then((data) => {
// Check data is useable
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length == 0) {
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
// Did not receive usable new data.
// Maybe this needs a better check?
Log.error("Possibly bad current/hourly data?");
@@ -117,7 +124,7 @@ WeatherProvider.register("ukmetofficedatahub", {
let nowUtc = moment.utc();
// Find hour that contains the current time
for (hour in forecastDataHours) {
for (let hour in forecastDataHours) {
let forecastTime = moment.utc(forecastDataHours[hour].time);
if (nowUtc.isSameOrAfter(forecastTime) && nowUtc.isBefore(moment(forecastTime.add(1, "h")))) {
currentWeather.date = forecastTime;
@@ -140,11 +147,9 @@ WeatherProvider.register("ukmetofficedatahub", {
}
// Determine the sunrise/sunset times - (still) not supplied in UK Met Office data
// Passes {longitude, latitude, height} to calcAstroData
// Could just pass lat/long from this.config, but returned data from MO also contains elevation
let times = this.calcAstroData(currentWeatherData.features[0].geometry.coordinates);
currentWeather.sunrise = times[0];
currentWeather.sunset = times[1];
// Passes {longitude, latitude} to SunCalc, could pass height to, but
// SunCalc.getTimes doesnt take that into account
currentWeather.updateSunTime(this.config.lat, this.config.lon);
return currentWeather;
},
@@ -154,7 +159,7 @@ WeatherProvider.register("ukmetofficedatahub", {
this.fetchWeather(this.getUrl("daily"), this.getHeaders())
.then((data) => {
// Check data is useable
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length == 0) {
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
// Did not receive usable new data.
// Maybe this needs a better check?
Log.error("Possibly bad forecast data?");
@@ -188,7 +193,7 @@ WeatherProvider.register("ukmetofficedatahub", {
let today = moment.utc().startOf("date");
// Go through each day in the forecasts
for (day in forecastDataDays) {
for (let day in forecastDataDays) {
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
// Get date of forecast
@@ -213,7 +218,6 @@ WeatherProvider.register("ukmetofficedatahub", {
// Pass on full details so they can be used in custom templates
// Note the units of the supplied data when using this (see top of file)
forecastWeather.rawData = forecastDataDays[day];
dailyForecasts.push(forecastWeather);
@@ -228,18 +232,6 @@ WeatherProvider.register("ukmetofficedatahub", {
this.fetchedLocationName = name;
},
// Calculate sunrise/sunset times
calcAstroData(location) {
const sunTimes = [];
// Careful to pass values to SunCalc in correct order (latitude, longitude, elevation)
let times = SunCalc.getTimes(new Date(), location[1], location[0], location[2]);
sunTimes.push(moment(times.sunrise, "X"));
sunTimes.push(moment(times.sunset, "X"));
return sunTimes;
},
// Convert temperatures to Fahrenheit (from degrees C), if required
convertTemp(tempInC) {
return this.config.tempUnits === "imperial" ? (tempInC * 9) / 5 + 32 : tempInC;
@@ -250,11 +242,11 @@ WeatherProvider.register("ukmetofficedatahub", {
// To use kilometres per hour, use "kph"
// Else assumed imperial and the value is returned in miles per hour (a Met Office user is likely to be UK-based)
convertWindSpeed(windInMpS) {
if (this.config.windUnits == "mps") {
if (this.config.windUnits === "mps") {
return windInMpS;
}
if (this.config.windUnits == "kph" || this.config.windUnits == "metric" || this.config.useKmh) {
if (this.config.windUnits === "kph" || this.config.windUnits === "metric" || this.config.useKmh) {
return windInMpS * 3.6;
}

View File

@@ -7,13 +7,23 @@
* By Andrew Pometti
* MIT Licensed
*
* This class is a provider for Weatherbit, based on Nicholas Hubbard's class for Dark Sky & Vince Peri's class for Weather.gov.
* This class is a provider for Weatherbit, based on Nicholas Hubbard's class
* for Dark Sky & Vince Peri's class for Weather.gov.
*/
WeatherProvider.register("weatherbit", {
// Set the name of the provider.
// Not strictly required, but helps for debugging.
providerName: "Weatherbit",
// 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: {
imperial: "I",
metric: "M"
@@ -80,7 +90,6 @@ WeatherProvider.register("weatherbit", {
currentWeather.windSpeed = parseFloat(currentWeatherData.data[0].wind_spd);
currentWeather.windDirection = currentWeatherData.data[0].wind_dir;
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.data[0].weather.icon);
Log.log("Wx Icon: " + currentWeatherData.data[0].weather.icon);
currentWeather.sunrise = moment(currentWeatherData.data[0].sunrise, "HH:mm").add(tzOffset, "m");
currentWeather.sunset = moment(currentWeatherData.data[0].sunset, "HH:mm").add(tzOffset, "m");

View File

@@ -1,4 +1,4 @@
/* global WeatherProvider, WeatherObject, SunCalc */
/* global WeatherProvider, WeatherObject */
/* Magic Mirror
* Module: Weather
@@ -19,6 +19,14 @@ WeatherProvider.register("weathergov", {
// But for debugging (and future alerts) it would be nice to have the real name.
providerName: "Weather.gov",
// Set the default config properties that is specific to this provider
defaults: {
apiBase: "https://api.weather.gov/points/",
weatherEndpoint: "/forecast",
lat: 0,
lon: 0
},
// Flag all needed URLs availability
configURLs: false,
@@ -121,7 +129,12 @@ WeatherProvider.register("weathergov", {
.finally(() => {
// excellent, let's fetch some actual wx data
this.configURLs = true;
this.fetchCurrentWeather();
// handle 'forecast' config, fall back to 'current'
if (config.type === "forecast") {
this.fetchWeatherForecast();
} else {
this.fetchCurrentWeather();
}
});
},
@@ -145,18 +158,11 @@ WeatherProvider.register("weathergov", {
currentWeather.precipitation = this.convertLength(currentWeatherData.precipitationLastHour.value);
currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.heatIndex.value);
let isDaytime = true;
if (currentWeatherData.icon.includes("day")) {
isDaytime = true;
} else {
isDaytime = false;
}
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.textDescription, isDaytime);
// determine the sunrise/sunset times - not supplied in weather.gov data
let times = this.calcAstroData(this.config.lat, this.config.lon);
currentWeather.sunrise = times[0];
currentWeather.sunset = times[1];
currentWeather.updateSunTime(this.config.lat, this.config.lon);
// update weatherType
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.textDescription, currentWeather.isDayTime());
return currentWeather;
},
@@ -259,20 +265,6 @@ WeatherProvider.register("weathergov", {
}
},
/*
* Calculate the astronomical data
*/
calcAstroData(lat, lon) {
const sunTimes = [];
// determine the sunrise/sunset times
let times = SunCalc.getTimes(new Date(), lat, lon);
sunTimes.push(moment(times.sunrise, "X"));
sunTimes.push(moment(times.sunset, "X"));
return sunTimes;
},
/*
* Convert the icons to a more usable name.
*/

View File

@@ -12,10 +12,6 @@ Module.register("weather", {
weatherProvider: "openweathermap",
roundTemp: false,
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,
useKmh: false,
tempUnits: config.units,
@@ -37,28 +33,25 @@ Module.register("weather", {
showIndoorHumidity: false,
maxNumberOfDays: 5,
maxEntries: 5,
ignoreToday: false,
fade: true,
fadePoint: 0.25, // Start on 1/4th of the list.
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,
calendarClass: "calendar",
tableClass: "small",
onlyTemp: false,
showPrecipitationAmount: false,
colored: false,
showFeelsLike: true,
feelsLikeWithDegree: false
showFeelsLike: true
},
// Module properties.
weatherProvider: null,
// Can be used by the provider to display location of event if nothing else is specified
firstEvent: null,
// Define required scripts.
getStyles: function () {
return ["font-awesome.css", "weather-icons.css", "weather.css"];
@@ -89,8 +82,6 @@ Module.register("weather", {
// Let the weather provider know we are starting.
this.weatherProvider.start();
this.config.feelsLikeWithDegree = this.translate("FEELS").indexOf("{DEGREE}") > -1;
// Add custom filters
this.addFilters();
@@ -101,15 +92,13 @@ Module.register("weather", {
// Override notification handler.
notificationReceived: function (notification, payload, sender) {
if (notification === "CALENDAR_EVENTS") {
var senderClasses = sender.data.classes.toLowerCase().split(" ");
const senderClasses = sender.data.classes.toLowerCase().split(" ");
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
this.firstEvent = false;
for (var e in payload) {
var event = payload[e];
this.firstEvent = null;
for (let event of payload) {
if (event.location || event.geo) {
this.firstEvent = event;
//Log.log("First upcoming event with location: ", event);
Log.debug("First upcoming event with location: ", event);
break;
}
}
@@ -127,24 +116,27 @@ Module.register("weather", {
getTemplate: function () {
switch (this.config.type.toLowerCase()) {
case "current":
return `current.njk`;
return "current.njk";
case "hourly":
return `hourly.njk`;
return "hourly.njk";
case "daily":
case "forecast":
return `forecast.njk`;
return "forecast.njk";
//Make the invalid values use the "Loading..." from forecast
default:
return `${this.config.type.toLowerCase()}.njk`;
return "forecast.njk";
}
},
// Add all the data to the template.
getTemplateData: function () {
const forecast = this.weatherProvider.weatherForecast();
return {
config: this.config,
current: this.weatherProvider.currentWeather(),
forecast: this.weatherProvider.weatherForecast(),
weatherData: this.weatherProvider.weatherData(),
forecast: forecast,
hourly: this.weatherProvider.weatherHourly(),
indoor: {
humidity: this.indoorHumidity,
temperature: this.indoorTemperature
@@ -157,28 +149,40 @@ Module.register("weather", {
Log.log("New weather information available.");
this.updateDom(0);
this.scheduleUpdate();
if (this.weatherProvider.currentWeather()) {
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.weatherProvider.currentWeather().weatherType.replace("-", "_") });
}
},
scheduleUpdate: function (delay = null) {
var nextLoad = this.config.updateInterval;
let nextLoad = this.config.updateInterval;
if (delay !== null && delay >= 0) {
nextLoad = delay;
}
setTimeout(() => {
if (this.config.weatherEndpoint === "/onecall") {
this.weatherProvider.fetchWeatherData();
} else if (this.config.type === "forecast") {
this.weatherProvider.fetchWeatherForecast();
} else {
this.weatherProvider.fetchCurrentWeather();
switch (this.config.type.toLowerCase()) {
case "current":
this.weatherProvider.fetchCurrentWeather();
break;
case "hourly":
this.weatherProvider.fetchWeatherHourly();
break;
case "daily":
case "forecast":
this.weatherProvider.fetchWeatherForecast();
break;
default:
Log.error(`Invalid type ${this.config.type} configured (must be one of 'current', 'hourly', 'daily' or 'forecast')`);
}
}, nextLoad);
},
roundValue: function (temperature) {
var decimals = this.config.roundTemp ? 0 : 1;
return parseFloat(temperature).toFixed(decimals);
const decimals = this.config.roundTemp ? 0 : 1;
const roundValue = parseFloat(temperature).toFixed(decimals);
return roundValue === "-0" ? 0 : roundValue;
},
addFilters() {
@@ -272,8 +276,8 @@ Module.register("weather", {
if (this.config.fadePoint < 0) {
this.config.fadePoint = 0;
}
var startingPoint = numSteps * this.config.fadePoint;
var numFadesteps = numSteps - startingPoint;
const startingPoint = numSteps * this.config.fadePoint;
const numFadesteps = numSteps - startingPoint;
if (currentStep >= startingPoint) {
return 1 - (currentStep - startingPoint) / numFadesteps;
} else {

View File

@@ -1,3 +1,5 @@
/* global SunCalc */
/* Magic Mirror
* Module: Weather
*
@@ -10,6 +12,14 @@
* As soon as we start implementing the forecast, mode properties will be added.
*/
class WeatherObject {
/**
* Constructor for a WeatherObject
*
* @param {string} units what units to use, "imperial" or "metric"
* @param {string} tempUnits what tempunits to use
* @param {string} windUnits what windunits to use
* @param {boolean} useKmh use kmh if true, mps if false
*/
constructor(units, tempUnits, windUnits, useKmh) {
this.units = units;
this.tempUnits = tempUnits;
@@ -28,6 +38,7 @@ class WeatherObject {
this.rain = null;
this.snow = null;
this.precipitation = null;
this.precipitationUnits = null;
this.feelsLikeTemp = null;
}
@@ -79,8 +90,7 @@ class WeatherObject {
}
kmhWindSpeed() {
const windInKmh = this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
return windInKmh;
return this.windUnits === "imperial" ? this.windSpeed * 1.609344 : (this.windSpeed * 60 * 60) / 1000;
}
nextSunAction() {
@@ -112,4 +122,33 @@ class WeatherObject {
return this.tempUnits === "imperial" ? feelsLike : ((feelsLike - 32) * 5) / 9;
}
/**
* Checks if the weatherObject is at dayTime.
*
* @returns {boolean} true if it is at dayTime
*/
isDayTime() {
return this.date.isBetween(this.sunrise, this.sunset, undefined, "[]");
}
/**
* Update the sunrise / sunset time depending on the location. This can be
* used if your provider doesnt provide that data by itself. Then SunCalc
* is used here to calculate them according to the location.
*
* @param {number} lat latitude
* @param {number} lon longitude
*/
updateSunTime(lat, lon) {
let now = !this.date ? new Date() : this.date.toDate();
let times = SunCalc.getTimes(now, lat, lon);
this.sunrise = moment(times.sunrise, "X");
this.sunset = moment(times.sunset, "X");
}
}
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = WeatherObject;
}

View File

@@ -8,15 +8,16 @@
*
* This class is the blueprint for a weather provider.
*/
var WeatherProvider = Class.extend({
const WeatherProvider = Class.extend({
// Weather Provider Properties
providerName: null,
defaults: {},
// The following properties have accessor methods.
// Try to not access them directly.
currentWeatherObject: null,
weatherForecastArray: null,
weatherDataObject: null,
weatherHourlyArray: null,
fetchedLocationName: null,
// 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.`);
},
// 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.
fetchWeatherData: function () {
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherData method.`);
fetchWeatherHourly: function () {
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherHourly method.`);
},
// 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.
weatherData: function () {
return this.weatherDataObject;
weatherHourly: function () {
return this.weatherHourlyArray;
},
// This returns the name of the fetched location or an empty string.
@@ -95,9 +96,9 @@ var WeatherProvider = Class.extend({
this.weatherForecastArray = weatherForecastArray;
},
// Set the weatherDataObject and notify the delegate that new information is available.
setWeatherData: function (weatherDataObject) {
this.weatherDataObject = weatherDataObject;
// Set the weatherHourlyArray and notify the delegate that new information is available.
setWeatherHourly: function (weatherHourlyArray) {
this.weatherHourlyArray = weatherHourlyArray;
},
// Set the fetched location name.
@@ -113,7 +114,7 @@ var WeatherProvider = Class.extend({
// A convenience function to make requests. It returns a promise.
fetchData: function (url, method = "GET", data = null) {
return new Promise(function (resolve, reject) {
var request = new XMLHttpRequest();
const request = new XMLHttpRequest();
request.open(method, url, true);
request.onreadystatechange = function () {
if (this.readyState === 4) {
@@ -154,10 +155,11 @@ WeatherProvider.register = function (providerIdentifier, providerDetails) {
WeatherProvider.initialize = function (providerIdentifier, delegate) {
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.setConfig(delegate.config);
provider.setConfig(config);
provider.providerIdentifier = providerIdentifier;
if (!provider.providerName) {

View File

@@ -1,5 +1,7 @@
# 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.
This module displays the weather forecast for the coming week, including an an icon to display the current conditions, the minimum temperature and the maximum temperature.

View File

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

View File

@@ -1,8 +1,12 @@
/* eslint-disable */
/* Magic Mirror
* Module: WeatherForecast
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*
* This module is deprecated. Any additional feature will no longer be merged.
*/
Module.register("weatherforecast", {
// Default module config.
@@ -337,7 +341,9 @@ Module.register("weatherforecast", {
*
* argument data object - Weather information received form openweather.org.
*/
processWeather: function (data) {
processWeather: function (data, momenttz) {
let mom = momenttz ? momenttz : moment; // Exception last.
// Forcast16 (paid) API endpoint provides this data. Onecall endpoint
// does not.
if (data.city) {
@@ -351,6 +357,13 @@ Module.register("weatherforecast", {
this.forecast = [];
var lastDay = null;
var forecastData = {};
var dayStarts = 8;
var dayEnds = 17;
if (data.city && data.city.sunrise && data.city.sunset) {
dayStarts = new Date(mom.unix(data.city.sunrise).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
dayEnds = new Date(mom.unix(data.city.sunset).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
}
// Handle different structs between forecast16 and onecall endpoints
var forecastList = null;
@@ -370,11 +383,11 @@ Module.register("weatherforecast", {
var day;
var hour;
if (forecast.dt_txt) {
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();
day = mom(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
hour = new Date(mom(forecast.dt_txt).locale("en").format("YYYY-MM-DD HH:mm:ss")).getHours();
} else {
day = moment(forecast.dt, "X").format("ddd");
hour = moment(forecast.dt, "X").toDate().getHours();
day = mom(forecast.dt, "X").format("ddd");
hour = new Date(mom(forecast.dt, "X")).getHours();
}
if (day !== lastDay) {
@@ -383,7 +396,7 @@ Module.register("weatherforecast", {
icon: this.config.iconTable[forecast.weather[0].icon],
maxTemp: this.roundValue(forecast.temp.max),
minTemp: this.roundValue(forecast.temp.min),
rain: this.processRain(forecast, forecastList)
rain: this.processRain(forecast, forecastList, mom)
};
this.forecast.push(forecastData);
lastDay = day;
@@ -400,7 +413,7 @@ Module.register("weatherforecast", {
// 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.
if (hour >= 8 && hour <= 17) {
if (hour > dayStarts && hour < dayEnds) {
forecastData.icon = this.config.iconTable[forecast.weather[0].icon];
}
}
@@ -462,7 +475,8 @@ Module.register("weatherforecast", {
*/
roundValue: function (temperature) {
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)
@@ -472,16 +486,18 @@ Module.register("weatherforecast", {
* That object has a property "3h" which contains the amount of rain since the previous forecast in the list.
* This code finds all forecasts that is for the same day and sums the amount of rain and returns that.
*/
processRain: function (forecast, allForecasts) {
processRain: function (forecast, allForecasts, momenttz) {
let mom = momenttz ? momenttz : moment; // Exception last.
//If the amount of rain actually is a number, return it
if (!isNaN(forecast.rain)) {
return forecast.rain;
}
//Find all forecasts that is for the same day
var checkDateTime = forecast.dt_txt ? moment(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss") : moment(forecast.dt, "X");
var checkDateTime = forecast.dt_txt ? mom(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss") : mom(forecast.dt, "X");
var daysForecasts = allForecasts.filter(function (item) {
var itemDateTime = item.dt_txt ? moment(item.dt_txt, "YYYY-MM-DD hh:mm:ss") : moment(item.dt, "X");
var itemDateTime = item.dt_txt ? mom(item.dt_txt, "YYYY-MM-DD hh:mm:ss") : mom(item.dt, "X");
return itemDateTime.isSame(checkDateTime, "day") && item.rain instanceof Object;
});

19728
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,30 @@
{
"name": "magicmirror",
"version": "2.14.0",
"version": "2.17.1",
"description": "The open source modular smart mirror platform.",
"main": "js/electron.js",
"scripts": {
"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",
"install": "echo \"Installing vendor files ...\n\" && cd vendor && 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\"",
"test": "NODE_ENV=test mocha tests --recursive",
"test:coverage": "NODE_ENV=test nyc mocha tests --recursive --timeout=3000",
"test:e2e": "NODE_ENV=test mocha tests/e2e --recursive",
"test:unit": "NODE_ENV=test mocha tests/unit --recursive",
"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:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json",
"test": "NODE_ENV=test jest -i --forceExit",
"test:coverage": "NODE_ENV=test nyc --reporter=lcov --reporter=text jest -i --forceExit",
"test:electron": "NODE_ENV=test jest --selectProjects electron -i --forceExit",
"test:e2e": "NODE_ENV=test jest --selectProjects e2e -i --forceExit",
"test:unit": "NODE_ENV=test jest --selectProjects unit -i --forceExit",
"test:prettier": "prettier . --check",
"test:js": "eslint 'js/**/*.js' 'modules/default/**/*.js' 'clientonly/*.js' 'serveronly/*.js' 'translations/*.js' 'vendor/*.js' 'tests/**/*.js' 'config/*' --config .eslintrc.json",
"test:css": "stylelint 'css/main.css' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json",
"test:calendar": "node ./modules/default/calendar/debug.js",
"config:check": "node js/check_config.js",
"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:css": "stylelint css/main.css modules/default/**/*.css --config .stylelintrc.json --fix"
"lint:prettier": "prettier . --write",
"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' 'fonts/*.css' 'modules/default/**/*.css' 'vendor/*.css' --config .stylelintrc.json --fix",
"lint:staged": "pretty-quick --staged",
"prepare": "[ -f node_modules/.bin/husky ] && husky install || echo no husky installed."
},
"repository": {
"type": "git",
@@ -42,57 +46,94 @@
},
"homepage": "https://magicmirror.builders",
"devDependencies": {
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"danger": "^10.5.4",
"eslint-config-prettier": "^7.0.0",
"eslint-plugin-jsdoc": "^30.7.8",
"eslint-plugin-prettier": "^3.2.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-jest": "^24.4.2",
"eslint-plugin-jsdoc": "^36.1.0",
"eslint-plugin-prettier": "^4.0.0",
"express-basic-auth": "^1.2.0",
"husky": "^4.3.5",
"jsdom": "^16.4.0",
"lodash": "^4.17.20",
"mocha": "^8.2.1",
"mocha-each": "^2.0.1",
"mocha-logger": "^1.0.7",
"husky": "^7.0.2",
"jest": "^27.2.2",
"jsdom": "^17.0.0",
"lodash": "^4.17.21",
"nyc": "^15.1.0",
"prettier": "^2.2.1",
"pretty-quick": "^3.1.0",
"spectron": "^10.0.1",
"stylelint": "^13.8.0",
"prettier": "^2.4.1",
"pretty-quick": "^3.1.1",
"sinon": "^11.1.2",
"spectron": "^15.0.0",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^20.0.0",
"stylelint-prettier": "^1.1.2"
"stylelint-config-standard": "^22.0.0",
"stylelint-prettier": "^1.2.0",
"suncalc": "^1.8.0"
},
"optionalDependencies": {
"electron": "^8.5.3"
"electron": "^13.5.1"
},
"dependencies": {
"colors": "^1.4.0",
"console-stamp": "^3.0.0-rc4.2",
"eslint": "^7.15.0",
"console-stamp": "^3.0.3",
"digest-fetch": "^1.2.1",
"eslint": "^7.32.0",
"express": "^4.17.1",
"express-ipfilter": "^1.1.2",
"express-ipfilter": "^1.2.0",
"feedme": "^2.0.2",
"helmet": "^4.2.0",
"ical": "^0.8.0",
"iconv-lite": "^0.6.2",
"helmet": "^4.6.0",
"iconv-lite": "^0.6.3",
"module-alias": "^2.2.2",
"moment": "^2.29.1",
"node-ical": "^0.12.7",
"request": "^2.88.2",
"rrule": "^2.6.6",
"rrule-alt": "^2.2.8",
"simple-git": "^2.31.0",
"socket.io": "^3.0.4",
"valid-url": "^1.0.9"
"node-fetch": "^2.6.5",
"node-ical": "^0.13.0",
"socket.io": "^4.2.0"
},
"_moduleAliases": {
"node_helper": "js/node_helper.js"
"node_helper": "js/node_helper.js",
"logger": "js/logger.js"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
"engines": {
"node": ">=12"
},
"jest": {
"verbose": true,
"projects": [
{
"displayName": "unit",
"moduleNameMapper": {
"logger": "<rootDir>/js/logger.js"
},
"testMatch": [
"**/tests/unit/**/*.[jt]s?(x)"
],
"testPathIgnorePatterns": [
"<rootDir>/tests/unit/mocks"
]
},
{
"displayName": "electron",
"testMatch": [
"**/tests/electron/**/*.[jt]s?(x)"
],
"testPathIgnorePatterns": [
"<rootDir>/tests/electron/modules/mocks",
"<rootDir>/tests/electron/global-setup.js",
"<rootDir>/tests/electron/modules/basic-auth.js"
]
},
{
"displayName": "e2e",
"setupFilesAfterEnv": [
"<rootDir>/tests/e2e/mock-console.js"
],
"testMatch": [
"**/tests/e2e/**/*.[jt]s?(x)"
],
"modulePaths": [
"<rootDir>/js/"
],
"testPathIgnorePatterns": [
"<rootDir>/tests/e2e/global-setup.js",
"<rootDir>/tests/e2e/mock-console.js"
]
}
]
}
}

View File

@@ -1,8 +1,8 @@
const app = require("../js/app.js");
const Log = require("../js/logger.js");
const Log = require("logger");
app.start(function (config) {
var bindAddress = config.address ? config.address : "localhost";
var httpType = config.useHttps ? "https" : "http";
app.start((config) => {
const bindAddress = config.address ? config.address : "localhost";
const httpType = config.useHttps ? "https" : "http";
Log.log("\nReady to go! Please point your browser to: " + httpType + "://" + bindAddress + ":" + config.port);
});

View File

@@ -0,0 +1,37 @@
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:xxx@gmail.com
X-WR-TIMEZONE:Europe/Zurich
BEGIN:VTIMEZONE
TZID:Etc/UTC
X-LIC-LOCATION:Etc/UTC
BEGIN:STANDARD
TZOFFSETFROM:+0000
TZOFFSETTO:+0000
TZNAME:GMT
DTSTART:19700101T00000--äüüßßß-0
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;VALUE=DATE:20210325
DTEND;VALUE=DATE:20210326
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1
DTSTAMP:20210421T154106Z
UID:zzz@google.com
REATED:20200831T200244Z
DESCRIPTION:
LAST-MODIFIED:20200831T200244Z
LOCATION:
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Birthday
TRANSP:OPAQUE
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:This is an event reminder
TRIGGER:-P0DT7H0M0S
END:VALARM
END:VEVENT

View File

@@ -1,44 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>
<channel>
<title>Rodrigo Ramírez Norambuena</title>
<atom:link href="https://rodrigoramirez.com/feed/" rel="self" type="application/rss+xml"/>
<link>https://rodrigoramirez.com</link>
<description>Temas sobre Linux, VoIP, Open Source, tecnología y lo relacionado.</description>
<lastBuildDate>Fri, 21 Oct 2016 21:30:22 +0000</lastBuildDate>
<language>es-ES</language>
<sy:updatePeriod>hourly</sy:updatePeriod>
<sy:updateFrequency>1</sy:updateFrequency>
<generator>https://wordpress.org/?v=4.7.3</generator>
<item>
<title>QPanel 0.13.0</title>
<link>https://rodrigoramirez.com/qpanel-0-13-0/</link>
<comments>https://rodrigoramirez.com/qpanel-0-13-0/#comments</comments>
<pubDate>Tue, 20 Sep 2016 11:16:08 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<category><![CDATA[spy]]></category>
<category><![CDATA[supervision]]></category>
<category><![CDATA[templates]]></category>
<category><![CDATA[whisper]]></category>
<channel>
<title>Rodrigo Ramírez Norambuena</title>
<atom:link href="https://rodrigoramirez.com/feed/" rel="self" type="application/rss+xml" />
<link>https://rodrigoramirez.com</link>
<description>Temas sobre Linux, VoIP, Open Source, tecnología y lo relacionado.</description>
<lastBuildDate>Fri, 21 Oct 2016 21:30:22 +0000</lastBuildDate>
<language>es-ES</language>
<sy:updatePeriod>hourly</sy:updatePeriod>
<sy:updateFrequency>1</sy:updateFrequency>
<generator>https://wordpress.org/?v=4.7.3</generator>
<item>
<title>QPanel 0.13.0</title>
<link>https://rodrigoramirez.com/qpanel-0-13-0/</link>
<comments>https://rodrigoramirez.com/qpanel-0-13-0/#comments</comments>
<pubDate>Tue, 20 Sep 2016 11:16:08 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<category><![CDATA[spy]]></category>
<category><![CDATA[supervision]]></category>
<category><![CDATA[templates]]></category>
<category><![CDATA[whisper]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1299</guid>
<description><![CDATA[<p>Ya está disponible la versión 0.13.0 de QPanel Para instalar esta nueva versión, la debes descargar de https://github.com/roramirez/qpanel/tree/0.13.0 En al README.md puedes encontrar las instrucciones para hacer que funcione en tu sistema. En esta nueva versión cuenta con los siguientes cambios: Se establece un limite para el reciclado del tiempo de conexión a la base [&#8230;]</p>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1299</guid>
<description><![CDATA[<p>Ya está disponible la versión 0.13.0 de QPanel Para instalar esta nueva versión, la debes descargar de https://github.com/roramirez/qpanel/tree/0.13.0 En al README.md puedes encontrar las instrucciones para hacer que funcione en tu sistema. En esta nueva versión cuenta con los siguientes cambios: Se establece un limite para el reciclado del tiempo de conexión a la base [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-13-0/">QPanel 0.13.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible la versión 0.13.0 de QPanel</p>
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible la versión 0.13.0 de QPanel</p>
<p>Para instalar esta nueva versión, la debes descargar de</p>
<ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.13.0">https://github.com/roramirez/qpanel/tree/0.13.0</a></li>
@@ -57,25 +57,25 @@
<p>&nbsp;</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-13-0/">QPanel 0.13.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-13-0/feed/</wfw:commentRss>
<slash:comments>3</slash:comments>
</item>
<item>
<title>Problema VirtualBox &#8220;starting virtual machine&#8221; &#8230;</title>
<link>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/</link>
<comments>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/#respond</comments>
<pubDate>Sat, 10 Sep 2016 22:50:13 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<category><![CDATA[no arranca]]></category>
<category><![CDATA[Problema]]></category>
<category><![CDATA[VirtualBox]]></category>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-13-0/feed/</wfw:commentRss>
<slash:comments>3</slash:comments>
</item>
<item>
<title>Problema VirtualBox &#8220;starting virtual machine&#8221; &#8230;</title>
<link>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/</link>
<comments>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/#respond</comments>
<pubDate>Sat, 10 Sep 2016 22:50:13 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<category><![CDATA[no arranca]]></category>
<category><![CDATA[Problema]]></category>
<category><![CDATA[VirtualBox]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1284</guid>
<description><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox.  La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje &#8220;starting virtual machine&#8221;, como el de la imagen de a continuación. [&#8230;]</p>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1284</guid>
<description><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox.  La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje &#8220;starting virtual machine&#8221;, como el de la imagen de a continuación. [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/">Problema VirtualBox &#8220;starting virtual machine&#8221; &#8230;</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox.  La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje &#8220;starting virtual machine&#8221;, como el de la imagen de a continuación.</p>
<content:encoded><![CDATA[<p>Después de una actualización de Debian, de la rama stretch/sid, tuve un problema con VirtualBox.  La versión que se actualizó fue a la virtualbox 5.1.4-dfsg-1+b1. El gran problema era que ninguna maquina virtual quería arrancar, se quedaba en un largo limbo con el mensaje &#8220;starting virtual machine&#8221;, como el de la imagen de a continuación.</p>
<p><a href="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png"><img class="aligncenter wp-image-1290 size-full" src="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png" alt="Starting virtual machine ... VirtualBox" width="648" height="554" srcset="https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09.png 648w, https://rodrigoramirez.com/wp-content/uploads/Screenshot-at-2016-09-10-19-25-09-300x256.png 300w" sizes="(max-width: 648px) 100vw, 648px" /></a></p>
<p>Ninguna, pero ninguna maquina arrancó, se quedaban en ese mensaje. Fue de esos instantes en que sudas helado &#8230; <img src="https://s.w.org/images/core/emoji/2.2.1/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>Con un poco de investigación fue a parar al archivo<em> ~/.VirtualBox/VBoxSVC.log </em>que indicaba</p>
@@ -85,7 +85,7 @@
<p>&nbsp;</p>
<p>Fui&#8230; algo de donde agarrarse. Mirando un poco mas se trataba de problemas con los permisos al vboxdrvu, mirando indicaba que tenía 0600.</p>
<p>&nbsp;</p>
<pre>$ ls -lh /dev/vboxdrvu
<pre>$ ls -lh /dev/vboxdrvu
crw------- 1 root root 10, 56 Sep 10 12:47 /dev/vboxdrvu</pre>
<p>&nbsp;</p>
<p>El tema es que deben estar en 0666,  le cambias los permisos y eso soluciona el problema <img src="https://s.w.org/images/core/emoji/2.2.1/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
@@ -95,24 +95,24 @@ $ ls -lh /dev/vboxdrvu
crw-rw-rw- 1 root root 10, 56 Sep 10 12:47 /dev/vboxdrvu</pre>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/">Problema VirtualBox &#8220;starting virtual machine&#8221; &#8230;</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>Mejorando la consola interactiva de Python</title>
<link>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/</link>
<comments>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/#comments</comments>
<pubDate>Tue, 06 Sep 2016 04:24:43 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[desarrollo]]></category>
<category><![CDATA[Desarrollo]]></category>
<category><![CDATA[Python]]></category>
<wfw:commentRss>https://rodrigoramirez.com/problema-virtualbox-starting-virtual-machine/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>Mejorando la consola interactiva de Python</title>
<link>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/</link>
<comments>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/#comments</comments>
<pubDate>Tue, 06 Sep 2016 04:24:43 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[desarrollo]]></category>
<category><![CDATA[Desarrollo]]></category>
<category><![CDATA[Python]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1247</guid>
<description><![CDATA[<p>Cuando estás desarrollando en Python es muy cool estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente. La consola de Python funciona y cumple su cometido. Solo al tipear  python  te permite entrar en modo interactivo e ir probando cosas. El punto es que a veces [&#8230;]</p>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1247</guid>
<description><![CDATA[<p>Cuando estás desarrollando en Python es muy cool estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente. La consola de Python funciona y cumple su cometido. Solo al tipear  python  te permite entrar en modo interactivo e ir probando cosas. El punto es que a veces [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/">Mejorando la consola interactiva de Python</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p>Cuando estás desarrollando en Python es muy <em>cool</em> estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente.</p>
<content:encoded><![CDATA[<p>Cuando estás desarrollando en Python es muy <em>cool</em> estar utilizando la consola interactiva para ir probando cosas antes de ponerlas dentro del archivo de código fuente.</p>
<p>La consola de Python funciona y cumple su cometido. Solo al tipear  <em>python  </em>te permite entrar en modo interactivo e ir probando cosas.</p>
<p>El punto es que a veces uno necesita ir un poco más allá. Como autocomentado de código o resaltado de sintaxis, para eso tengo dos truco que utilizo generalmente.</p>
<h2>Truco a)</h2>
@@ -139,31 +139,31 @@ $ ls -lh /dev/vboxdrvu
<p>O lo agregas a un bashrc, zshrc o la shell que ocupes.</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/">Mejorando la consola interactiva de Python</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/feed/</wfw:commentRss>
<slash:comments>4</slash:comments>
</item>
<item>
<title>QPanel 0.12.0 con estadísticas</title>
<link>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/</link>
<comments>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/#respond</comments>
<pubDate>Mon, 22 Aug 2016 04:19:03 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<category><![CDATA[spy]]></category>
<category><![CDATA[supervision]]></category>
<category><![CDATA[templates]]></category>
<category><![CDATA[whisper]]></category>
<wfw:commentRss>https://rodrigoramirez.com/mejorando-la-consola-interactiva-python/feed/</wfw:commentRss>
<slash:comments>4</slash:comments>
</item>
<item>
<title>QPanel 0.12.0 con estadísticas</title>
<link>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/</link>
<comments>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/#respond</comments>
<pubDate>Mon, 22 Aug 2016 04:19:03 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<category><![CDATA[spy]]></category>
<category><![CDATA[supervision]]></category>
<category><![CDATA[templates]]></category>
<category><![CDATA[whisper]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1268</guid>
<description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.12.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.12.0 En esta nueva versión las funcionalidades agregadas son: Permite remover los agentes de las cola Posibilidad de cancelar llamadas que están en espera de atención Estadísticas por rango de fecha obtenidas desde [&#8230;]</p>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1268</guid>
<description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.12.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.12.0 En esta nueva versión las funcionalidades agregadas son: Permite remover los agentes de las cola Posibilidad de cancelar llamadas que están en espera de atención Estadísticas por rango de fecha obtenidas desde [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/">QPanel 0.12.0 con estadísticas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.12.0</p>
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.12.0</p>
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
<ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.12.0">https://github.com/roramirez/qpanel/tree/0.12.0</a></li>
@@ -178,31 +178,31 @@ $ ls -lh /dev/vboxdrvu
<p>Si deseas colaborar con el proyecto puedes agregar nuevas sugerencias mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a> ó colaborar mediante <a href="https://github.com/roramirez/qpanel/blob/dd42cf0f534408505f57b0d387dffee2f3688711/README.md#how-to-contribute">mediante un Pull Request</a></p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/">QPanel 0.12.0 con estadísticas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>QPanel 0.11.0 con Spy, Whisper y mas</title>
<link>https://rodrigoramirez.com/qpanel-spy-supervisor/</link>
<comments>https://rodrigoramirez.com/qpanel-spy-supervisor/#comments</comments>
<pubDate>Thu, 21 Jul 2016 01:53:21 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<category><![CDATA[spy]]></category>
<category><![CDATA[supervision]]></category>
<category><![CDATA[templates]]></category>
<category><![CDATA[whisper]]></category>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-12-0-estadisticas/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>QPanel 0.11.0 con Spy, Whisper y mas</title>
<link>https://rodrigoramirez.com/qpanel-spy-supervisor/</link>
<comments>https://rodrigoramirez.com/qpanel-spy-supervisor/#comments</comments>
<pubDate>Thu, 21 Jul 2016 01:53:21 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<category><![CDATA[spy]]></category>
<category><![CDATA[supervision]]></category>
<category><![CDATA[templates]]></category>
<category><![CDATA[whisper]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1245</guid>
<description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.11.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.11.0 Esta versión hemos agregado  algunas funcionalidades que los usuarios  han ido solicitando. Para esta versión es posible realizar Spy, Whisper o Barge a un canal para la supervisión de los miembros que [&#8230;]</p>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1245</guid>
<description><![CDATA[<p>Ya está disponible una nueva versión de QPanel, esta es la 0.11.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.11.0 Esta versión hemos agregado  algunas funcionalidades que los usuarios  han ido solicitando. Para esta versión es posible realizar Spy, Whisper o Barge a un canal para la supervisión de los miembros que [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-spy-supervisor/">QPanel 0.11.0 con Spy, Whisper y mas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.11.0</p>
<content:encoded><![CDATA[<p><img class="aligncenter" src="https://raw.githubusercontent.com/roramirez/qpanel/e55aa16bbd85b579ee82e56469526270c5afa462/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="685" height="385" />Ya está disponible una nueva versión de QPanel, esta es la 0.11.0</p>
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
<ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.11.0">https://github.com/roramirez/qpanel/tree/0.11.0</a></li>
@@ -216,22 +216,22 @@ $ ls -lh /dev/vboxdrvu
<p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-spy-supervisor/">QPanel 0.11.0 con Spy, Whisper y mas</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-spy-supervisor/feed/</wfw:commentRss>
<slash:comments>4</slash:comments>
</item>
<item>
<title>Añadir Swap a un sistema</title>
<link>https://rodrigoramirez.com/crear-swap/</link>
<comments>https://rodrigoramirez.com/crear-swap/#respond</comments>
<pubDate>Fri, 15 Jul 2016 05:07:43 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-spy-supervisor/feed/</wfw:commentRss>
<slash:comments>4</slash:comments>
</item>
<item>
<title>Añadir Swap a un sistema</title>
<link>https://rodrigoramirez.com/crear-swap/</link>
<comments>https://rodrigoramirez.com/crear-swap/#respond</comments>
<pubDate>Fri, 15 Jul 2016 05:07:43 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1234</guid>
<description><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap. La  memoria swap es un espacio de intercambio en disco para cuando el sistema ya no puede utilizar más memoria RAM. El problema para mi es que algunos sistemas de maquinas virtuales no asignan por defecto [&#8230;]</p>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1234</guid>
<description><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap. La  memoria swap es un espacio de intercambio en disco para cuando el sistema ya no puede utilizar más memoria RAM. El problema para mi es que algunos sistemas de maquinas virtuales no asignan por defecto [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/crear-swap/">Añadir Swap a un sistema</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap.</p>
<content:encoded><![CDATA[<p>Algo que me toma generalmente hacer es cuando trabajo con maquina virtuales es asignar una cantidad determinada de Swap.</p>
<p>La  memoria swap es un espacio de intercambio en disco para cuando el sistema ya no puede utilizar más memoria RAM.</p>
<p>El problema para mi es que algunos sistemas de maquinas virtuales no asignan por defecto un espacio para la Swap, lo que te lleva a que el sistema pueda tener crash durante la ejecución.</p>
<p>Para comprobar la asignación de memoria, al ejecutar el comando <em>free</em> nos debería mostrar como algo similar a lo siguiente</p>
@@ -271,27 +271,27 @@ Swap:         3071          0       3071</pre>
<p>&nbsp;</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/crear-swap/">Añadir Swap a un sistema</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/crear-swap/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>QPanel 0.10.0 con vista consolidada</title>
<link>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/</link>
<comments>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/#respond</comments>
<pubDate>Mon, 20 Jun 2016 19:32:55 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<wfw:commentRss>https://rodrigoramirez.com/crear-swap/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>QPanel 0.10.0 con vista consolidada</title>
<link>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/</link>
<comments>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/#respond</comments>
<pubDate>Mon, 20 Jun 2016 19:32:55 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<category><![CDATA[app_queue]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[FreeSWITCH]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queue]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1227</guid>
<description><![CDATA[<p>Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible. Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.10.0 Esta versión versión nos preocupamos de realizar mejoras, refactorizaciones y agregamos una nueva funcionalidad. La nueva funcionalidad incluida es  que ahora es posible contar con una vista consolidada para [&#8230;]</p>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1227</guid>
<description><![CDATA[<p>Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible. Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.10.0 Esta versión versión nos preocupamos de realizar mejoras, refactorizaciones y agregamos una nueva funcionalidad. La nueva funcionalidad incluida es  que ahora es posible contar con una vista consolidada para [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/">QPanel 0.10.0 con vista consolidada</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.10.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible.</p>
<content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.10.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />Ya con la release numero 28 la nueva versión 0.10.0 de QPanel ya está disponible.</p>
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
<ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.10.0">https://github.com/roramirez/qpanel/tree/0.10.0</a></li>
@@ -301,29 +301,29 @@ Swap:         3071          0       3071</pre>
<p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/">QPanel 0.10.0 con vista consolidada</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>Nerdearla 2016, WebRTC Glue</title>
<link>https://rodrigoramirez.com/nerdearla-2016/</link>
<comments>https://rodrigoramirez.com/nerdearla-2016/#respond</comments>
<pubDate>Wed, 15 Jun 2016 17:55:41 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<category><![CDATA[baires]]></category>
<category><![CDATA[charla]]></category>
<category><![CDATA[Computación]]></category>
<category><![CDATA[informatica]]></category>
<category><![CDATA[tech]]></category>
<category><![CDATA[ti]]></category>
<category><![CDATA[webrtc]]></category>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-10-0-vista-consolidada/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>Nerdearla 2016, WebRTC Glue</title>
<link>https://rodrigoramirez.com/nerdearla-2016/</link>
<comments>https://rodrigoramirez.com/nerdearla-2016/#respond</comments>
<pubDate>Wed, 15 Jun 2016 17:55:41 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<category><![CDATA[baires]]></category>
<category><![CDATA[charla]]></category>
<category><![CDATA[Computación]]></category>
<category><![CDATA[informatica]]></category>
<category><![CDATA[tech]]></category>
<category><![CDATA[ti]]></category>
<category><![CDATA[webrtc]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1218</guid>
<description><![CDATA[<p>Días atrás estuve participando en el evento llamado Nerdearla en Buenos Aires.  El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes. Habían muchas cosas interesantes tanto en las presentaciones, co-working y workshop que se hubieron. Si te [&#8230;]</p>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1218</guid>
<description><![CDATA[<p>Días atrás estuve participando en el evento llamado Nerdearla en Buenos Aires.  El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes. Habían muchas cosas interesantes tanto en las presentaciones, co-working y workshop que se hubieron. Si te [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/nerdearla-2016/">Nerdearla 2016, WebRTC Glue</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p>Días atrás estuve participando en el evento llamado <a href="https://nerdear.la/">Nerdearla</a> en Buenos Aires.  El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes.</p>
<content:encoded><![CDATA[<p>Días atrás estuve participando en el evento llamado <a href="https://nerdear.la/">Nerdearla</a> en Buenos Aires.  El ambiente era genial si eres de esas personas que desde niño sintio curiosidad por ver como funcionan las cosas, donde desarmabas para volver armar lo juguetes.</p>
<p>Habían muchas cosas interesantes tanto en las presentaciones, co-working y workshop que se hubieron. Si te lo perdiste te recomiendo que estés pendiente para el proximo año.</p>
<p>&nbsp;</p>
<p>Te podias encontrar con una nuestra como esta<a href="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS.jpg"><img class="aligncenter size-medium wp-image-1221" src="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-300x169.jpg" alt="Kaypro II" width="300" height="169" srcset="https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-300x169.jpg 300w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-768x432.jpg 768w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS-1024x576.jpg 1024w, https://rodrigoramirez.com/wp-content/uploads/CkhnO83XAAAfaxS.jpg 1200w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>
@@ -338,30 +338,30 @@ Swap:         3071          0       3071</pre>
&nbsp;</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/nerdearla-2016/">Nerdearla 2016, WebRTC Glue</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/nerdearla-2016/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>QPanel 0.9.0</title>
<link>https://rodrigoramirez.com/qpanel-0-9-0/</link>
<comments>https://rodrigoramirez.com/qpanel-0-9-0/#respond</comments>
<pubDate>Mon, 09 May 2016 18:40:23 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[callcenter]]></category>
<category><![CDATA[colas]]></category>
<category><![CDATA[monitor]]></category>
<category><![CDATA[monitoreo]]></category>
<category><![CDATA[panel]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queues]]></category>
<wfw:commentRss>https://rodrigoramirez.com/nerdearla-2016/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>QPanel 0.9.0</title>
<link>https://rodrigoramirez.com/qpanel-0-9-0/</link>
<comments>https://rodrigoramirez.com/qpanel-0-9-0/#respond</comments>
<pubDate>Mon, 09 May 2016 18:40:23 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Software]]></category>
<category><![CDATA[asterisk]]></category>
<category><![CDATA[callcenter]]></category>
<category><![CDATA[colas]]></category>
<category><![CDATA[monitor]]></category>
<category><![CDATA[monitoreo]]></category>
<category><![CDATA[panel]]></category>
<category><![CDATA[qpanel]]></category>
<category><![CDATA[queues]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1206</guid>
<description><![CDATA[<p>El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.9.0 Esta versión versión nos preocupamos de realizar mejoras y refactorizaciones en el codigo para dar un mejor rendimiento, como también de la compatibilidad con la versión 11 de [&#8230;]</p>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1206</guid>
<description><![CDATA[<p>El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0 Para instalar esta nueva versión, debes visitar la siguiente URL https://github.com/roramirez/qpanel/tree/0.9.0 Esta versión versión nos preocupamos de realizar mejoras y refactorizaciones en el codigo para dar un mejor rendimiento, como también de la compatibilidad con la versión 11 de [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-9-0/">QPanel 0.9.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.9.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0</p>
<content:encoded><![CDATA[<p><img class="alignleft" src="https://raw.githubusercontent.com/roramirez/qpanel/0.9.0/samples/animation.gif" alt="Panel monitor callcenter | Qpanel Monitor Colas" width="403" height="227" />El Panel monitor callcenter para colas de Asterisk ya cuenta con una nueva versión, la 0.9.0</p>
<p>Para instalar esta nueva versión, debes visitar la siguiente URL</p>
<ul>
<li><a href="https://github.com/roramirez/qpanel/tree/0.9.0">https://github.com/roramirez/qpanel/tree/0.9.0</a></li>
@@ -376,35 +376,35 @@ Swap:         3071          0       3071</pre>
<p>El proyecto siempre está abierto a nuevas sugerencias las cuales puedes agregar mediante un <a href="https://github.com/roramirez/qpanel/issues/new?title=[Feature]">issue</a>.</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/qpanel-0-9-0/">QPanel 0.9.0</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-9-0/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>Mandar un email desde la shell</title>
<link>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/</link>
<comments>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/#comments</comments>
<pubDate>Wed, 13 Apr 2016 13:05:13 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<category><![CDATA[mini-tips]]></category>
<category><![CDATA[bash]]></category>
<category><![CDATA[cli]]></category>
<category><![CDATA[Email]]></category>
<category><![CDATA[mail]]></category>
<category><![CDATA[sh]]></category>
<category><![CDATA[shell]]></category>
<wfw:commentRss>https://rodrigoramirez.com/qpanel-0-9-0/feed/</wfw:commentRss>
<slash:comments>0</slash:comments>
</item>
<item>
<title>Mandar un email desde la shell</title>
<link>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/</link>
<comments>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/#comments</comments>
<pubDate>Wed, 13 Apr 2016 13:05:13 +0000</pubDate>
<dc:creator><![CDATA[decipher]]></dc:creator>
<category><![CDATA[Linux]]></category>
<category><![CDATA[mini-tips]]></category>
<category><![CDATA[bash]]></category>
<category><![CDATA[cli]]></category>
<category><![CDATA[Email]]></category>
<category><![CDATA[mail]]></category>
<category><![CDATA[sh]]></category>
<category><![CDATA[shell]]></category>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1172</guid>
<description><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando mail en un servidor con Linux. Si usas mail a secas te va pidiendo los datos para crear el correo, principalmente el body del correo. Para automatizar esto a través de un [&#8230;]</p>
<guid isPermaLink="false">https://rodrigoramirez.com/?p=1172</guid>
<description><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando mail en un servidor con Linux. Si usas mail a secas te va pidiendo los datos para crear el correo, principalmente el body del correo. Para automatizar esto a través de un [&#8230;]</p>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mandar-un-email-desde-la-shell/">Mandar un email desde la shell</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></description>
<content:encoded><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando <em>mail</em> en un servidor con Linux.</p>
<content:encoded><![CDATA[<p>Dejo esto por acá ya que es algo que siempre me olvido como es. El tema es enviar un email mediante el comando <em>mail</em> en un servidor con Linux.</p>
<p>Si usas mail a secas te va pidiendo los datos para crear el correo, principalmente el body del correo. Para automatizar esto a través de un <em>echo</em> le pasas por pipe a <em>mail</em></p>
<pre>echo "Cuerpo del mensaje" | mail -s Asunto a@rodrigoramirez.com</pre>
<p>La entrada <a rel="nofollow" href="https://rodrigoramirez.com/mandar-un-email-desde-la-shell/">Mandar un email desde la shell</a> aparece primero en <a rel="nofollow" href="https://rodrigoramirez.com">Rodrigo Ramírez Norambuena</a>.</p>
]]></content:encoded>
<wfw:commentRss>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/feed/</wfw:commentRss>
<slash:comments>4</slash:comments>
</item>
</channel>
<wfw:commentRss>https://rodrigoramirez.com/mandar-un-email-desde-la-shell/feed/</wfw:commentRss>
<slash:comments>4</slash:comments>
</item>
</channel>
</rss>

File diff suppressed because it is too large Load Diff

21
tests/configs/default.js Normal file
View File

@@ -0,0 +1,21 @@
/* Magic Mirror Test default config for modules
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
exports.configFactory = function (options) {
return Object.assign(
{
electronOptions: {
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
contextIsolation: false
}
},
modules: []
},
options
);
};

View File

@@ -3,22 +3,9 @@
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
var config = {
port: 8080,
ipWhitelist: [],
language: "en",
timeFormat: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true
}
},
modules: []
};
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
ipWhitelist: []
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,22 +3,7 @@
* 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: 24,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true
}
},
modules: []
};
let config = require(process.cwd() + "/tests/configs/default.js").configFactory();
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -0,0 +1,21 @@
/* Magic Mirror Test config sample module alert
*
* By rejas
* MIT Licensed.
*/
let config = {
modules: [
{
module: "alert",
config: {
display_time: 1000000,
welcome_message: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -3,19 +3,8 @@
* 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",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true
}
},
modules: [
{
@@ -25,7 +14,7 @@ var config = {
calendars: [
{
maximumNumberOfDays: 10000,
url: "http://localhost:8011/tests/configs/data/calendar_test.ics",
url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
auth: {
user: "MagicMirror",
pass: "CallMeADog"
@@ -35,7 +24,7 @@ var config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* 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",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true
}
},
modules: [
{
@@ -25,7 +14,7 @@ var config = {
calendars: [
{
maximumNumberOfDays: 10000,
url: "http://localhost:8010/tests/configs/data/calendar_test.ics",
url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
auth: {
user: "MagicMirror",
pass: "CallMeADog",
@@ -36,7 +25,7 @@ var config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -0,0 +1,32 @@
/* Magic Mirror Test config default calendar with auth by default
*
* By Rodrigo Ramírez Norambuena https://rodrigoramirez.com
* MIT Licensed.
*/
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
modules: [
{
module: "calendar",
position: "bottom_bar",
config: {
calendars: [
{
maximumNumberOfDays: 10000,
url: "http://localhost:8010/tests/configs/data/calendar_test.ics",
auth: {
user: "MagicMirror",
pass: "CallMeADog"
}
}
]
}
}
]
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = config;
}

View File

@@ -1,19 +1,10 @@
/* Magic Mirror Test config custom calendar
*
* By Rejas
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true
}
},
modules: [
{
@@ -33,7 +24,7 @@ let config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* 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",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true
}
},
modules: [
{
@@ -31,7 +20,7 @@ var config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -5,19 +5,8 @@
* 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",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true
}
},
modules: [
{
@@ -38,7 +27,7 @@ var config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

View File

@@ -3,19 +3,8 @@
* 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",
let config = require(process.cwd() + "/tests/configs/default.js").configFactory({
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true
}
},
modules: [
{
@@ -25,7 +14,7 @@ var config = {
calendars: [
{
maximumNumberOfDays: 10000,
url: "http://localhost:8012/tests/configs/data/calendar_test.ics",
url: "http://localhost:8080/tests/configs/data/calendar_test.ics",
user: "MagicMirror",
pass: "CallMeADog"
}
@@ -33,7 +22,7 @@ var config = {
}
}
]
};
});
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {

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