Merge pull request #1776 from MichMich/develop

v2.9.0
This commit is contained in:
Michael Teeuw 2019-10-01 19:45:34 +02:00 committed by GitHub
commit 500147e130
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 3112 additions and 2004 deletions

View File

@ -17,6 +17,7 @@
},
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2017,
"ecmaFeatures": {
"globalReturn": true
}

6
.gitignore vendored
View File

@ -11,7 +11,9 @@ coverage
.grunt
.lock-wscript
build/Release
node_modules
/node_modules/**/*
fonts/node_modules/**/*
vendor/node_modules/**/*
jspm_modules
.npm
.node_repl_history
@ -81,3 +83,5 @@ Temporary Items
*.orig
*.rej
*.bak
!/tests/node_modules/**/*

View File

@ -1,3 +1,4 @@
dist: trusty
language: node_js
node_js:
- "8"

View File

@ -7,6 +7,29 @@ This project adheres to [Semantic Versioning](http://semver.org/).
❤️ **Donate:** Enjoying MagicMirror²? [Please consider a donation!](https://magicmirror.builders/donate) With your help we can continue to improve the MagicMirror² core.
## [2.9.0] - 2019-10-01
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md).
### Added
- Spanish translation for "PRECIP".
- Adding a Malay (Malaysian) translation for MagicMirror².
- Add test check URLs of vendors 200 and 404 HTTP CODE.
- Add tests for new weather module and helper to stub ajax requests.
### Updated
- Updatenotification module: Display update notification for a limited (configurable) time.
- Enabled e2e/vendor_spec.js tests.
- The css/custom.css will be rename after the next release. We've add into `run-start.sh` a instruction by GIT to ignore with `--skip-worktree` and `rm --cached`. [#1540](https://github.com/MichMich/MagicMirror/issues/1540)
- Disable sending of notification CLOCK_SECOND when displaySeconds is false.
### Fixed
- Updatenotification module: Properly handle race conditions, prevent crash.
- Send `NEWS_FEED` notification also for the first news messages which are shown.
- Fixed issue where weather module would not refresh data after a network or API outage. [#1722](https://github.com/MichMich/MagicMirror/issues/1722)
- Fixed weatherforecast module not displaying rain amount on fallback endpoint.
- Notifications CLOCK_SECOND & CLOCK_MINUTE being from startup instead of matched against the clock and avoid drifting.
## [2.8.0] - 2019-07-01
**Note:** This update uses new dependencies. Please update using the following command: `git pull && npm install`. If you are having issues running Electron, make sure your [Raspbian is up to date](https://www.raspberrypi.org/documentation/raspbian/updating.md).
@ -28,18 +51,18 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Automatically try to fix eslint errors by passing `--fix` option to it
- Added sunrise and sunset times to weathergov weather provider [#1705](https://github.com/MichMich/MagicMirror/issues/1705)
- Added "useLocationAsHeader" to display "location" in `config.js` as header when location name is not returned
- Added to `newsfeed.js`: in order to design the news article better with css, three more class-names were introduced: newsfeed-desc, newsfeed-desc, newsfeed-desc
- Added to `newsfeed.js`: in order to design the news article better with css, three more class-names were introduced: newsfeed-desc, newsfeed-desc, newsfeed-desc
### Updated
- English translation for "Feels" to "Feels like"
- Fixed the example calender url in `config.js.sample`
- Update `ical.js` to solve various calendar issues.
- Update weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
- Update weather city list url [#1676](https://github.com/MichMich/MagicMirror/issues/1676)
- Only update clock once per minute when seconds aren't shown
### Fixed
- Fixed uncaught exception, race condition on module update
- Fixed issue [#1696](https://github.com/MichMich/MagicMirror/issues/1696), some ical files start date to not parse to date type
- Fixed uncaught exception, race condition on module update
- Fixed issue [#1696](https://github.com/MichMich/MagicMirror/issues/1696), some ical files start date to not parse to date type
- Allowance HTML5 autoplay-policy (policy is changed from Chrome 66 updates)
- Handle SIGTERM messages
- Fixes sliceMultiDayEvents so it respects maximumNumberOfDays

View File

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

View File

@ -130,13 +130,6 @@ if $NPM_INSTALL; then
echo -e "\e[96mInstalling npm ...\e[90m"
# Fetch the latest version of npm from the selected branch
# The NODE_STABLE_BRANCH variable will need to be manually adjusted when a new branch is released. (e.g. 7.x)
# Only tested (stable) versions are recommended as newer versions could break MagicMirror.
#NODE_STABLE_BRANCH="9.x"
#curl -sL https://deb.nodesource.com/setup_$NODE_STABLE_BRANCH | sudo -E bash -
#
sudo apt-get install -y npm
echo -e "\e[92mnpm installation Done!\e[0m"
fi

View File

@ -203,7 +203,7 @@ var MM = (function() {
*/
var updateModuleContent = function(module, newHeader, newContent) {
var moduleWrapper = document.getElementById(module.identifier);
if (moduleWrapper === null) return;
if (moduleWrapper === null) {return;}
var headerWrapper = moduleWrapper.getElementsByClassName("module-header");
var contentWrapper = moduleWrapper.getElementsByClassName("module-content");

View File

@ -33,7 +33,7 @@ ical.objectHandlers['RRULE'] = function(val, params, curr, stack, line){
var originalEnd = ical.objectHandlers['END'];
ical.objectHandlers['END'] = function (val, params, curr, stack) {
// Recurrence rules are only valid for VEVENT, VTODO, and VJOURNAL.
// More specifically, we need to filter the VCALENDAR type because we might end up with a defined rrule
// More specifically, we need to filter the VCALENDAR type because we might end up with a defined rrule
// due to the subtypes.
if ((val === "VEVENT") || (val === "VTODO") || (val === "VJOURNAL")) {
if (curr.rrule) {

View File

@ -41,26 +41,40 @@ Module.register("clock",{
// Schedule update interval.
var self = this;
self.second = 0;
self.minute = 0;
self.lastDisplayedMinute = null;
setInterval(function() {
if (self.config.displaySeconds || self.lastDisplayedMinute !== moment().minute()) {
self.updateDom();
}
if (self.second === 59) {
self.second = 0;
if (self.minute === 59){
self.minute = 0;
} else {
self.minute++;
}
self.sendNotification("CLOCK_MINUTE", self.minute);
self.second = moment().second();
self.minute = moment().minute();
//Calculate how many ms should pass until next update depending on if seconds is displayed or not
var delayCalculator = function(reducedSeconds) {
if (self.config.displaySeconds) {
return 1000 - moment().milliseconds();
} else {
self.second++;
self.sendNotification("CLOCK_SECOND", self.second);
return ((60 - reducedSeconds) * 1000) - moment().milliseconds();
}
}, 1000);
};
//A recursive timeout function instead of interval to avoid drifting
var notificationTimer = function() {
self.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 = (self.second + 1) % 60;
if (self.second !== 0) {
self.sendNotification("CLOCK_SECOND", self.second);
setTimeout(notificationTimer, delayCalculator(0));
return;
}
}
//If minute changed or seconds isn't displayed send CLOCK_MINUTE-notification
self.minute = (self.minute + 1) % 60;
self.sendNotification("CLOCK_MINUTE", self.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 locale.
moment.locale(config.language);

View File

@ -273,7 +273,7 @@ Module.register("currentweather",{
if (this.config.useLocationAsHeader && this.config.location !== false) {
return this.config.location;
}
return this.data.header;
},

View File

@ -327,6 +327,11 @@ Module.register("newsfeed",{
self.updateDom(self.config.animationSpeed);
// Broadcast NewsFeed if needed
if (self.config.broadcastNewsFeeds) {
self.sendNotification("NEWS_FEED", {items: self.newsItems});
}
timer = setInterval(function() {
self.activeItem++;
self.updateDom(self.config.animationSpeed);

View File

@ -10,11 +10,17 @@ module.exports = NodeHelper.create({
config: {},
updateTimer: null,
updateProcessStarted: false,
start: function () {
},
configureModules: function(modules) {
// Push MagicMirror itself , biggest chance it'll show up last in UI and isn't overwritten
// others will be added in front, asynchronously
simpleGits.push({"module": "default", "git": SimpleGit(path.normalize(__dirname + "/../../../"))});
for (moduleName in modules) {
if (defaultModules.indexOf(moduleName) < 0) {
// Default modules are included in the main MagicMirror repo
@ -22,6 +28,7 @@ module.exports = NodeHelper.create({
var stat;
try {
//console.log("checking git for module="+moduleName)
stat = fs.statSync(path.join(moduleFolder, ".git"));
} catch(err) {
// Error when directory .git doesn't exist
@ -36,30 +43,29 @@ module.exports = NodeHelper.create({
// No valid remote for folder, skip
return;
}
// Folder has .git and has at least one git remote, watch this folder
simpleGits.push({"module": mn, "git": git});
simpleGits.unshift({"module": mn, "git": git});
});
}(moduleName, moduleFolder);
}
}
// Push MagicMirror itself last, biggest chance it'll show up last in UI and isn't overwritten
simpleGits.push({"module": "default", "git": SimpleGit(path.normalize(__dirname + "/../../../"))});
},
socketNotificationReceived: function (notification, payload) {
if (notification === "CONFIG") {
this.config = payload;
} else if(notification === "MODULES") {
this.configureModules(payload);
this.preformFetch();
// if this is the 1st time thru the update check process
if(this.updateProcessStarted==false ){
this.updateProcessStarted=true;
this.configureModules(payload);
this.preformFetch();
}
}
},
preformFetch() {
var self = this;
simpleGits.forEach(function(sg) {
sg.git.fetch().status(function(err, data) {
data.module = sg.module;

View File

@ -2,40 +2,56 @@ Module.register("updatenotification", {
defaults: {
updateInterval: 10 * 60 * 1000, // every 10 minutes
refreshInterval: 24 * 60 * 60 * 1000, // one day
},
status: false,
suspended: false,
moduleList: {},
start: function () {
var self = this;
Log.log("Start updatenotification");
setInterval( () => { self.moduleList = {};self.updateDom(2); } , self.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: self.identifier });
}
},
socketNotificationReceived: function (notification, payload) {
if (notification === "STATUS") {
this.status = payload;
this.updateUI();
this.updateUI(payload);
}
},
updateUI: function () {
updateUI: function (payload) {
var self = this;
if (this.status && this.status.behind > 0) {
self.updateDom(0);
self.show(1000, { lockString: self.identifier });
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);
}
//self.show(1000, { lockString: self.identifier });
} else if (payload && payload.behind == 0){
// if the module WAS in the list, but shouldn't be
if(this.moduleList[payload.module] != undefined){
// remove it
delete this.moduleList[payload.module];
self.updateDom(2);
}
}
},
diffLink: function(text) {
var localRef = this.status.hash;
var remoteRef = this.status.tracking.replace(/.*\//, "");
diffLink: function(module, text) {
var localRef = module.hash;
var remoteRef = module.tracking.replace(/.*\//, "");
return "<a href=\"https://github.com/MichMich/MagicMirror/compare/"+localRef+"..."+remoteRef+"\" "+
"class=\"xsmall dimmed\" "+
"style=\"text-decoration: none;\" "+
@ -47,41 +63,53 @@ Module.register("updatenotification", {
// Override dom generator.
getDom: function () {
var wrapper = document.createElement("div");
if(this.suspended==false){
// process the hash of module info found
for(key of Object.keys(this.moduleList)){
let m= this.moduleList[key];
if (this.status && this.status.behind > 0) {
var message = document.createElement("div");
message.className = "small bright";
var message = document.createElement("div");
message.className = "small bright";
var icon = document.createElement("i");
icon.className = "fa fa-exclamation-circle";
icon.innerHTML = "&nbsp;";
message.appendChild(icon);
var icon = document.createElement("i");
icon.className = "fa fa-exclamation-circle";
icon.innerHTML = "&nbsp;";
message.appendChild(icon);
var updateInfoKeyName = this.status.behind === 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
var subtextHtml = this.translate(updateInfoKeyName, {
COMMIT_COUNT: this.status.behind,
BRANCH_NAME: this.status.current
});
var updateInfoKeyName = m.behind == 1 ? "UPDATE_INFO_SINGLE" : "UPDATE_INFO_MULTIPLE";
var text = document.createElement("span");
if (this.status.module === "default") {
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
subtextHtml = this.diffLink(subtextHtml);
} else {
text.innerHTML = this.translate("UPDATE_NOTIFICATION_MODULE", {
MODULE_NAME: this.status.module
var subtextHtml = this.translate(updateInfoKeyName, {
COMMIT_COUNT: m.behind,
BRANCH_NAME: m.current
});
var text = document.createElement("span");
if (m.module == "default") {
text.innerHTML = this.translate("UPDATE_NOTIFICATION");
subtextHtml = this.diffLink(m,subtextHtml);
} else {
text.innerHTML = this.translate("UPDATE_NOTIFICATION_MODULE", {
MODULE_NAME: m.module
});
}
message.appendChild(text);
wrapper.appendChild(message);
var subtext = document.createElement("div");
subtext.innerHTML = subtextHtml;
subtext.className = "xsmall dimmed";
wrapper.appendChild(subtext);
}
message.appendChild(text);
wrapper.appendChild(message);
var subtext = document.createElement("div");
subtext.innerHTML = subtextHtml;
subtext.className = "xsmall dimmed";
wrapper.appendChild(subtext);
}
return wrapper;
},
suspend: function() {
this.suspended=true;
},
resume: function() {
this.suspended=false;
this.updateDom(2);
}
});

View File

@ -32,7 +32,8 @@ WeatherProvider.register("darksky", {
this.setCurrentWeather(currentWeather);
}).catch(function(request) {
Log.error("Could not load data ... ", request);
});
})
.finally(() => this.updateAvailable())
},
fetchWeatherForecast() {
@ -47,7 +48,8 @@ WeatherProvider.register("darksky", {
this.setWeatherForecast(forecast);
}).catch(function(request) {
Log.error("Could not load data ... ", request);
});
})
.finally(() => this.updateAvailable())
},
// Create a URL from the config and base URL.

View File

@ -34,6 +34,7 @@ WeatherProvider.register("openweathermap", {
.catch(function(request) {
Log.error("Could not load data ... ", request);
})
.finally(() => this.updateAvailable())
},
// Overwrite the fetchCurrentWeather method.
@ -54,6 +55,7 @@ WeatherProvider.register("openweathermap", {
.catch(function(request) {
Log.error("Could not load data ... ", request);
})
.finally(() => this.updateAvailable())
},
/** OpenWeatherMap Specific Methods - These are not part of the default provider methods */

View File

@ -41,6 +41,7 @@ WeatherProvider.register("ukmetoffice", {
.catch(function(request) {
Log.error("Could not load data ... ", request);
})
.finally(() => this.updateAvailable())
},
// Overwrite the fetchCurrentWeather method.
@ -62,6 +63,7 @@ WeatherProvider.register("ukmetoffice", {
.catch(function(request) {
Log.error("Could not load data ... ", request);
})
.finally(() => this.updateAvailable())
},

View File

@ -35,6 +35,7 @@ WeatherProvider.register("weathergov", {
.catch(function(request) {
Log.error("Could not load data ... ", request);
})
.finally(() => this.updateAvailable())
},
// Overwrite the fetchCurrentWeather method.
@ -53,6 +54,7 @@ WeatherProvider.register("weathergov", {
.catch(function(request) {
Log.error("Could not load data ... ", request);
})
.finally(() => this.updateAvailable())
},
/** Weather.gov Specific Methods - These are not part of the default provider methods */

0
modules/default/weather/weather.js Executable file → Normal file
View File

View File

@ -79,16 +79,12 @@ var WeatherProvider = Class.extend({
setCurrentWeather: function(currentWeatherObject) {
// We should check here if we are passing a WeatherDay
this.currentWeatherObject = currentWeatherObject;
this.updateAvailable();
},
// Set the weatherForecastArray and notify the delegate that new information is available.
setWeatherForecast: function(weatherForecastArray) {
// We should check here if we are passing a WeatherDay
this.weatherForecastArray = weatherForecastArray;
this.updateAvailable();
},
// Set the fetched location name.

View File

@ -353,7 +353,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: forecast.rain
rain: this.processRain(forecast, data.list)
};
this.forecast.push(forecastData);
@ -434,5 +434,38 @@ Module.register("weatherforecast",{
roundValue: function(temperature) {
var decimals = this.config.roundTemp ? 0 : 1;
return parseFloat(temperature).toFixed(decimals);
},
/* processRain(forecast, allForecasts)
* Calculates the amount of rain for a whole day even if long term forecasts isn't available for the appid.
*
* When using the the fallback endpoint forecasts are provided in 3h intervals and the rain-property is an object instead of number.
* 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) {
//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 daysForecasts = allForecasts.filter(function(item) {
var itemDateTime = (!!item.dt_txt) ? moment(item.dt_txt, "YYYY-MM-DD hh:mm:ss") : moment(item.dt, "X");
return itemDateTime.isSame(checkDateTime, "day") && item.rain instanceof Object;
});
//If no rain this day return undefined so it wont be displayed for this day
if (daysForecasts.length == 0) {
return undefined;
}
//Summarize all the rain from the matching days
return daysForecasts.map(function(item) {
return Object.values(item.rain)[0];
}).reduce(function(a, b) {
return a + b;
}, 0);
}
});

4106
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,13 @@
{
"name": "magicmirror",
"version": "2.8.0",
"version": "2.9.0",
"description": "The open source modular smart mirror platform.",
"main": "js/electron.js",
"scripts": {
"start": "sh run-start.sh",
"install": "cd vendor && npm install",
"install-fonts": "cd fonts && npm install",
"postinstall": "sh installers/postinstall/postinstall.sh && npm run install-fonts",
"postinstall": "sh untrack-css.sh && sh installers/postinstall/postinstall.sh && npm run install-fonts",
"test": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests --recursive",
"test:unit": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests/unit --recursive",
"test:e2e": "NODE_ENV=test ./node_modules/mocha/bin/mocha tests/e2e --recursive",
@ -63,6 +63,7 @@
"feedme": "latest",
"helmet": "^3.9.0",
"iconv-lite": "latest",
"lodash": "^4.17.11",
"moment": "latest",
"request": "^2.88.0",
"rrule": "^2.6.2",

View File

@ -1,3 +1,6 @@
./untrack-css.sh
if [ -z "$DISPLAY" ]; then #If not set DISPLAY is SSH remote or tty
export DISPLAY=:0 # Set by default display
fi

View File

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

View File

@ -0,0 +1,40 @@
/* Magic Mirror Test config default weather
*
* By fewieden https://github.com/fewieden
*
* MIT Licensed.
*/
let 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: [
{
module: "weather",
position: "bottom_bar",
config: {
location: "Munich",
apiKey: "fake key",
initialLoadDelay: 3000,
useBeaufort: false,
showWindDirectionAsArrow: true,
showHumidity: true,
roundTemp: true,
degreeLabel: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = config;}

View File

@ -0,0 +1,37 @@
/* Magic Mirror Test config default weather
*
* By fewieden https://github.com/fewieden
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 24,
units: "imperial",
electronOptions: {
webPreferences: {
nodeIntegration: true,
},
},
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
location: "Munich",
apiKey: "fake key",
initialLoadDelay: 3000,
decimalSymbol: ",",
showHumidity: true
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = config;}

View File

@ -0,0 +1,37 @@
/* Magic Mirror Test config default weather
*
* By fewieden https://github.com/fewieden
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
},
},
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "forecast",
location: "Munich",
apiKey: "fake key",
weatherEndpoint: "/forecast/daily",
initialLoadDelay: 3000
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = config;}

View File

@ -0,0 +1,40 @@
/* Magic Mirror Test config default weather
*
* By fewieden https://github.com/fewieden
*
* MIT Licensed.
*/
let config = {
port: 8080,
ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
language: "en",
timeFormat: 12,
units: "metric",
electronOptions: {
webPreferences: {
nodeIntegration: true,
},
},
modules: [
{
module: "weather",
position: "bottom_bar",
config: {
type: "forecast",
location: "Munich",
apiKey: "fake key",
weatherEndpoint: "/forecast/daily",
initialLoadDelay: 3000,
showPrecipitationAmount: true,
colored: true,
tableClass: "myTableClass"
}
}
]
};
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {module.exports = config;}

View File

@ -0,0 +1,4 @@
const generateWeather = require("./weather_current");
const generateWeatherForecast = require("./weather_forecast");
module.exports = {generateWeather, generateWeatherForecast};

View File

@ -0,0 +1,54 @@
const _ = require('lodash');
function generateWeather(extendedData = {}) {
return JSON.stringify(_.merge({}, {
coord:{
lon: 11.58,
lat: 48.14
},
weather:[
{
id: 615,
main: "Snow",
description: "light rain and snow",
icon: "13d"
},
{
id: 500,
main: "Rain",
description: "light rain",
icon: "10d"
}
],
base: "stations",
main:{
temp: 1.49,
pressure: 1005,
humidity: 93.7,
temp_min: 1,
temp_max: 2
},
visibility: 7000,
wind:{
speed: 11.8,
deg: 250
},
clouds:{
all: 75
},
dt: 1547387400,
sys:{
type: 1,
id: 1267,
message: 0.0031,
country: "DE",
sunrise: 1547362817,
sunset: 1547394301
},
id: 2867714,
name: "Munich",
cod: 200
}, extendedData));
}
module.exports = generateWeather;

View File

@ -0,0 +1,97 @@
const _ = require('lodash');
function generateWeatherForecast(extendedData = {}) {
return JSON.stringify(_.merge({}, {
"city": {
"id": 2867714,
"name": "Munich",
"coord": {"lon": 11.5754, "lat": 48.1371},
"country": "DE",
"population": 1260391,
"timezone": 7200
},
"cod": "200",
"message": 0.9653487,
"cnt": 7,
"list": [{
"dt": 1568372400,
"sunrise": 1568350044,
"sunset": 1568395948,
"temp": {"day": 24.44, "min": 15.35, "max": 24.44, "night": 15.35, "eve": 18, "morn": 23.03},
"pressure": 1031.65,
"humidity": 70,
"weather": [{"id": 801, "main": "Clouds", "description": "few clouds", "icon": "02d"}],
"speed": 3.35,
"deg": 314,
"clouds": 21
}, {
"dt": 1568458800,
"sunrise": 1568436525,
"sunset": 1568482223,
"temp": {"day": 20.81, "min": 13.56, "max": 21.02, "night": 13.56, "eve": 16.6, "morn": 15.88},
"pressure": 1028.81,
"humidity": 72,
"weather": [{"id": 500, "main": "Rain", "description": "light rain", "icon": "10d"}],
"speed": 2.21,
"deg": 81,
"clouds": 100
}, {
"dt": 1568545200,
"sunrise": 1568523007,
"sunset": 1568568497,
"temp": {"day": 22.65, "min": 13.76, "max": 22.88, "night": 15.27, "eve": 17.45, "morn": 13.76},
"pressure": 1023.75,
"humidity": 64,
"weather": [{"id": 800, "main": "Clear", "description": "sky is clear", "icon": "01d"}],
"speed": 1.15,
"deg": 7,
"clouds": 0
}, {
"dt": 1568631600,
"sunrise": 1568609489,
"sunset": 1568654771,
"temp": {"day": 23.45, "min": 13.95, "max": 23.45, "night": 13.95, "eve": 17.75, "morn": 15.21},
"pressure": 1020.41,
"humidity": 64,
"weather": [{"id": 800, "main": "Clear", "description": "sky is clear", "icon": "01d"}],
"speed": 3.07,
"deg": 298,
"clouds": 7
}, {
"dt": 1568718000,
"sunrise": 1568695970,
"sunset": 1568741045,
"temp": {"day": 20.55, "min": 10.95, "max": 20.55, "night": 10.95, "eve": 14.82, "morn": 13.24},
"pressure": 1019.4,
"humidity": 66,
"weather": [{"id": 800, "main": "Clear", "description": "sky is clear", "icon": "01d"}],
"speed": 2.8,
"deg": 333,
"clouds": 2
}, {
"dt": 1568804400,
"sunrise": 1568782452,
"sunset": 1568827319,
"temp": {"day": 18.15, "min": 7.75, "max": 18.15, "night": 7.75, "eve": 12.45, "morn": 9.41},
"pressure": 1017.56,
"humidity": 52,
"weather": [{"id": 800, "main": "Clear", "description": "sky is clear", "icon": "01d"}],
"speed": 2.92,
"deg": 34,
"clouds": 0
}, {
"dt": 1568890800,
"sunrise": 1568868934,
"sunset": 1568913593,
"temp": {"day": 14.85, "min": 5.56, "max": 15.05, "night": 5.56, "eve": 9.56, "morn": 6.25},
"pressure": 1022.7,
"humidity": 59,
"weather": [{"id": 800, "main": "Clear", "description": "sky is clear", "icon": "01d"}],
"speed": 2.89,
"deg": 51,
"clouds": 1
}]
}, extendedData));
}
module.exports = generateWeatherForecast;

View File

@ -0,0 +1,270 @@
const expect = require("chai").expect;
const fs = require("fs");
const moment = require("moment");
const path = require("path");
const wdajaxstub = require("webdriverajaxstub");
const helpers = require("../global-setup");
const {generateWeather, generateWeatherForecast} = require("./mocks");
const wait = () => new Promise(res => setTimeout(res, 3000));
describe("Weather module", function() {
let app;
helpers.setupTimeout(this);
async function setup(responses) {
app = await helpers.startApplication({
args: ["js/electron.js"]
});
wdajaxstub.init(app.client, responses);
app.client.setupStub();
}
afterEach(function() {
return helpers.stopApplication(app);
});
describe("Current weather", function() {
let template;
before(function() {
template = fs.readFileSync(path.join(__dirname, "..", "..", "..", "modules", "default", "weather", "current.njk"), "utf8");
});
describe("Default configuration", function() {
before(function() {
process.env.MM_CONFIG_FILE = "tests/configs/modules/weather/currentweather_default.js";
});
it("should render wind speed and wind direction", async function() {
const weather = generateWeather();
await setup([weather, template]);
return app.client.waitUntilTextExists(".weather .normal.medium span:nth-child(2)", "6 WSW", 10000);
});
it("should render sunrise", async function() {
const sunrise = moment().startOf("day").unix();
const sunset = moment().startOf("day").unix();
const weather = generateWeather({sys: {sunrise, sunset}});
await setup([weather, template]);
await app.client.waitForExist(".weather .normal.medium span.wi.dimmed.wi-sunrise", 10000);
return app.client.waitUntilTextExists(".weather .normal.medium span:nth-child(4)", "12:00 am", 10000);
});
it("should render sunset", async function() {
const sunrise = moment().startOf("day").unix();
const sunset = moment().endOf("day").unix();
const weather = generateWeather({sys: {sunrise, sunset}});
await setup([weather, template]);
await app.client.waitForExist(".weather .normal.medium span.wi.dimmed.wi-sunset", 10000);
return app.client.waitUntilTextExists(".weather .normal.medium span:nth-child(4)", "11:59 pm", 10000);
});
it("should render temperature with icon", async function() {
const weather = generateWeather();
await setup([weather, template]);
await app.client.waitForExist(".weather .large.light span.wi.weathericon.wi-snow", 10000);
return app.client.waitUntilTextExists(".weather .large.light span.bright", "1.5°", 10000);
});
it("should render feels like temperature", async function() {
const weather = generateWeather();
await setup([weather, template]);
return app.client.waitUntilTextExists(".weather .normal.medium span.dimmed", "Feels like -5.6°", 10000);
});
});
describe("Configuration Options", function() {
before(function() {
process.env.MM_CONFIG_FILE = "tests/configs/modules/weather/currentweather_options.js";
});
it("should render useBeaufort = false", async function() {
const weather = generateWeather();
await setup([weather, template]);
return app.client.waitUntilTextExists(".weather .normal.medium span:nth-child(2)", "12", 10000);
});
it("should render showWindDirectionAsArrow = true", async function() {
const weather = generateWeather();
await setup([weather, template]);
await app.client.waitForExist(".weather .normal.medium sup i.fa-long-arrow-up", 10000);
const element = await app.client.getHTML(".weather .normal.medium sup i.fa-long-arrow-up");
expect(element).to.include("transform:rotate(250deg);");
});
it("should render showHumidity = true", async function() {
const weather = generateWeather();
await setup([weather, template]);
await app.client.waitUntilTextExists(".weather .normal.medium span:nth-child(3)", "93", 10000);
return app.client.waitForExist(".weather .normal.medium sup i.wi-humidity", 10000);
});
it("should render degreeLabel = true", async function() {
const weather = generateWeather();
await setup([weather, template]);
await app.client.waitUntilTextExists(".weather .large.light span.bright", "1°C", 10000);
return app.client.waitUntilTextExists(".weather .normal.medium span.dimmed", "Feels like -6°C", 10000);
});
});
describe("Current weather units", function() {
before(function() {
process.env.MM_CONFIG_FILE = "tests/configs/modules/weather/currentweather_units.js";
});
it("should render imperial units", async function() {
const weather = generateWeather({
main:{
temp: 1.49 * 9 / 5 + 32,
temp_min: 1 * 9 / 5 + 32,
temp_max: 2 * 9 / 5 + 32
},
wind:{
speed: 11.8 * 2.23694
},
});
await setup([weather, template]);
await app.client.waitUntilTextExists(".weather .normal.medium span:nth-child(2)", "6 WSW", 10000);
await app.client.waitUntilTextExists(".weather .large.light span.bright", "34,7°", 10000);
return app.client.waitUntilTextExists(".weather .normal.medium span.dimmed", "22,0°", 10000);
});
it("should render decimalSymbol = ','", async function() {
const weather = generateWeather({
main:{
temp: 1.49 * 9 / 5 + 32,
temp_min: 1 * 9 / 5 + 32,
temp_max: 2 * 9 / 5 + 32
},
wind:{
speed: 11.8 * 2.23694
},
});
await setup([weather, template]);
await app.client.waitUntilTextExists(".weather .normal.medium span:nth-child(3)", "93,7", 10000);
await app.client.waitUntilTextExists(".weather .large.light span.bright", "34,7°", 10000);
return app.client.waitUntilTextExists(".weather .normal.medium span.dimmed", "22,0°", 10000);
});
});
});
describe("Weather Forecast", function() {
let template;
before(function() {
template = fs.readFileSync(path.join(__dirname, "..", "..", "..", "modules", "default", "weather", "forecast.njk"), "utf8");
});
describe("Default configuration", function() {
before(function() {
process.env.MM_CONFIG_FILE = "tests/configs/modules/weather/forecastweather_default.js";
});
it("should render days", async function() {
const weather = generateWeatherForecast();
await setup([weather, template]);
const days = ["Fri", "Sat", "Sun", "Mon", "Tue"];
for (const [index, day] of days.entries()) {
await app.client.waitUntilTextExists(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(1)`, day, 10000);
}
});
it("should render icons", async function() {
const weather = generateWeatherForecast();
await setup([weather, template]);
const icons = ["day-cloudy", "rain", "day-sunny", "day-sunny", "day-sunny"];
for (const [index, icon] of icons.entries()) {
await app.client.waitForExist(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(2) span.wi-${icon}`, 10000);
}
});
it("should render max temperatures", async function() {
const weather = generateWeatherForecast();
await setup([weather, template]);
const temperatures = ["24.4°", "21.0°", "22.9°", "23.4°", "20.6°"];
for (const [index, temp] of temperatures.entries()) {
await app.client.waitUntilTextExists(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(3)`, temp, 10000);
}
});
it("should render min temperatures", async function() {
const weather = generateWeatherForecast();
await setup([weather, template]);
const temperatures = ["15.3°", "13.6°", "13.8°", "13.9°", "10.9°"];
for (const [index, temp] of temperatures.entries()) {
await app.client.waitUntilTextExists(`.weather table.small tr:nth-child(${index + 1}) td:nth-child(4)`, temp, 10000);
}
});
it("should render fading of rows", async function() {
const weather = generateWeatherForecast();
await setup([weather, template]);
const opacities = [1, 1, 0.8, 0.5333333333333333, 0.2666666666666667];
await app.client.waitForExist('.weather table.small', 10000);
for (const [index, opacity] of opacities.entries()) {
const html = await app.client.getHTML(`.weather table.small tr:nth-child(${index + 1})`);
expect(html).to.includes(`<tr style="opacity: ${opacity};">`);
}
});
});
describe("Configuration Options", function() {
before(function() {
process.env.MM_CONFIG_FILE = "tests/configs/modules/weather/forecastweather_options.js";
});
it("should render custom table class", async function() {
const weather = generateWeatherForecast();
await setup([weather, template]);
await app.client.waitForExist(`.weather table.myTableClass`, 10000);
});
it("should render colored rows", async function() {
const weather = generateWeatherForecast();
await setup([weather, template]);
await app.client.waitForExist(`.weather table.myTableClass`, 10000);
const rows = await app.client.$$('.weather table.myTableClass tr.colored');
expect(rows.length).to.be.equal(5);
});
});
});
});

View File

@ -6,11 +6,10 @@ const describe = global.describe;
const it = global.it;
const before = global.before;
const after = global.after;
const mlog = require("mocha-logger");
describe("Vendors", function () {
return; // Test still getting failed in Travis
helpers.setupTimeout(this);
var app = null;
@ -40,5 +39,17 @@ describe("Vendors", function () {
});
});
});
Object.keys(vendors).forEach(vendor => {
it(`should return 404 HTTP code for vendor https://localhost/"${vendor}"`, function() {
urlVendor = "http://localhost:8080/" + vendors[vendor];
request.get(urlVendor, function (err, res, body) {
if (!err)
expect(res.statusCode).to.equal(404);
else
mlog.pending(`There error vendor 404 test ${err}`);
});
});
});
});
});

33
tests/node_modules/webdriverajaxstub/index.js generated vendored Normal file
View File

@ -0,0 +1,33 @@
function plugin (wdInstance, requests) {
if (typeof wdInstance.addCommand !== "function") {
throw new Error("You can't use WebdriverAjaxStub with this version of WebdriverIO");
}
function stub(requests, done) {
window.XMLHttpRequest = function () {
this.open = function (method, url) {
this.method = method;
this.url = url;
};
this.send = function () {
this.status = 200;
this.readyState = 4;
const response = requests.shift() || [];
this.response = response;
this.responseText = response;
this.onreadystatechange();
};
return this;
};
done();
}
wdInstance.addCommand("setupStub", function() {
return wdInstance.executeAsync(stub, requests);
});
}
module.exports.init = plugin;

0
translations/en.json Executable file → Normal file
View File

View File

@ -31,5 +31,6 @@
"UPDATE_INFO_SINGLE": "Tu actual instalación está {COMMIT_COUNT} commit cambios detrás de la rama {BRANCH_NAME}.",
"UPDATE_INFO_MULTIPLE": "Tu actual instalación está {COMMIT_COUNT} commits cambios detrás de la rama {BRANCH_NAME}.",
"FEELS": "Sensación térmica de"
"FEELS": "Sensación térmica de",
"PRECIP": "Precipitación"
}

33
translations/ms-my.json Normal file
View File

@ -0,0 +1,33 @@
{
"LOADING": "Tunggu Sebentar &hellip;",
"TODAY": "Hari ini",
"TOMORROW": "Esok",
"DAYAFTERTOMORROW": "Lusa",
"RUNNING": "Berakhir dalam",
"EMPTY": "Tidak ada agenda",
"WEEK": "Minggu ke-{weekNumber}",
"N": "U",
"NNE": "UUT",
"NE": "TL",
"ENE": "TTL",
"E": "T",
"ESE": "TT",
"SE": "T",
"SSE": "ST",
"S": "S",
"SSW": "SBD",
"SW": "BD",
"WSW": "BBD",
"W": "B",
"WNW": "BBL",
"NW": "BL",
"NNW": "UBL",
"UPDATE_NOTIFICATION": "MagicMirror² mempunyai update terkini.",
"UPDATE_NOTIFICATION_MODULE": "Modul {MODULE_NAME} mempunyai update terkini.",
"UPDATE_INFO_SINGLE": "Pemasangan MagicMirror² ini mempunyai {COMMIT_COUNT} commit terkebelakang dari branch {BRANCH_NAME}.",
"UPDATE_INFO_MULTIPLE": "Pemasangan MagicMirror² ini mempunyai {COMMIT_COUNT} commit terkebelakang dari branch {BRANCH_NAME}."
}

View File

@ -40,7 +40,8 @@ var translations = {
"cs" : "translations/cs.json", // Czech
"hr" : "translations/hr.json", // Croatian
"sk" : "translations/sk.json", // Slovak
"tlh" : "translations/tlh.json" // Klingon
"tlh" : "translations/tlh.json", // Klingon
"ms-my" : "translations/ms-my.json" // Malay
};
if (typeof module !== "undefined") {module.exports = translations;}

11
untrack-css.sh Executable file
View File

@ -0,0 +1,11 @@
# Long history here
# https://github.com/MichMich/MagicMirror/pull/1540
STATUS_CUSTOM_CSS=$(git ls-files -v css/custom.css|cut -f 1 --delimiter=" ")
if [ "$STATUS_CUSTOM_CSS" = "H" ]; then
echo "We'll remove from the repository the css/custom.css"
echo "This script apply git update-index --skip-worktree css/custom.css"
git update-index --skip-worktree css/custom.css
git rm --cached css/custom.css
fi

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

@ -5,7 +5,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": {
"version": "5.6.3",
"resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.6.3.tgz",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.6.3.tgz",
"integrity": "sha512-s5PLdI9NYgjBvfrv6rhirPHlAHWx+Sfo/IjsAeiXYfmemC/GSjwsyz1wLnGPazbLPXWfk62ks980o9AmsxYUEQ=="
},
"a-sync-waterfall": {