Michael Teeuw abe5c08a52
Release 2.23.0 (#3078)
## [2.23.0] - 2023-04-04

Thanks to: @angeldeejay, @buxxi, @CarJem, @dariom, @DaveChild, @dWoolridge, @grenagit, @Hirschberger, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @nfogal, @psieg, @rajniszp, @retroflex, @SkySails and @tomzt.

Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome!

### Added

- Added increments for hourly forecasts in weather module (#2996)
- Added tests for hourly weather forecast
- Added possibility to ignore MagicMirror repo in updatenotification module
- Added Pirate Weather as new weather provider (#3005)
- Added possibility to use your own templates in Alert module
- Added error message if `<modulename>.js` file is missing in module folder to get a hint in the logs (#2403)
- Added possibility to use environment variables in `config.js` (#1756)
- Added option `pastDaysCount` to default calendar module to control of how many days past events should be displayed
- Added thai language to alert module
- Added option `sendNotifications` in clock module (#3056)

### Removed

- Removed darksky weather provider
- Removed unneeded (and unwanted) '.' after the year in calendar repeatingCountTitle (#2896)

### Updated

- Use develop as target branch for dependabot
- Update issue template, contributing doc and sample config
- The weather modules clearly separates precipitation amount and probability (risk of rain/snow)
  - This requires all providers that only supports probability to change the config from `showPrecipitationAmount` to `showPrecipitationProbability`.
- Update tests for weather and calendar module
- Changed updatenotification module for MagicMirror repo only: Send only notifications for `master` if there is a tag on a newer commit
- Update dates in Calendar widgets every minute
- Cleanup jest coverage for patches
- Update `stylelint` dependencies, switch to `stylelint-config-standard` and handle `stylelint` issues, update `main.css` matching new rules
- Update Eslint config, add new rule and handle issue
- Convert lots of callbacks to async/await
- Revise require imports (#3071 and #3072)

### Fixed

- Fix wrong day labels in envcanada forecast (#2987)
- Fix for missing default class name prefix for customEvents in calendar
- Fix electron flashing white screen on startup (#1919)
- Fix weathergov provider hourly forecast (#3008)
- Fix message display with HTML code into alert module (#2828)
- Fix typo in french translation
- Yr wind direction is no longer inverted
- Fix async node_helper stopping electron start (#2487)
- The wind direction arrow now points in the direction the wind is flowing, not into the wind (#3019)
- Fix precipitation css styles and rounding value
- Fix wrong vertical alignment of calendar title column when wrapEvents is true (#3053)
- Fix empty news feed stopping the reload forever
- Fix e2e tests (failed after async changes) by running calendar and newsfeed tests last
- Lint: Use template literals instead of string concatenation
- Fix default alert module to render HTML for title and message
- Fix Open-Meteo wind speed units
2023-04-04 20:44:32 +02:00

317 lines
11 KiB
JavaScript

/* global SunCalc */
/* MagicMirror²
* Module: Clock
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
Module.register("clock", {
// Module config defaults.
defaults: {
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",
sendNotifications: false,
/* specific to the analog clock */
analogSize: "200px",
analogFace: "simple", // options: 'none', 'simple', 'face-###' (where ### is 001 to 012 inclusive)
analogPlacement: "bottom", // options: 'top', 'bottom', 'left', 'right'
analogShowDate: "top", // OBSOLETE, can be replaced with analogPlacement and showTime, options: false, 'top', or 'bottom'
secondsColor: "#888888",
showSunTimes: false,
showMoonTimes: false,
lat: 47.630539,
lon: -122.344147
},
// Define required scripts.
getScripts: function () {
return ["moment.js", "moment-timezone.js", "suncalc.js"];
},
// Define styles.
getStyles: function () {
return ["clock_styles.css"];
},
// Define start sequence.
start: function () {
Log.info(`Starting module: ${this.name}`);
// Schedule update interval.
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
const delayCalculator = (reducedSeconds) => {
const EXTRA_DELAY = 50; // Deliberate imperceptible delay to prevent off-by-one timekeeping errors
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
const notificationTimer = () => {
this.updateDom();
if (this.config.sendNotifications) {
// 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
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(this.second));
// Set locale.
moment.locale(config.language);
},
// Override dom generator.
getDom: function () {
const wrapper = document.createElement("div");
wrapper.classList.add("clock-grid");
/************************************
* Create wrappers for analog and digital clock
*/
const analogWrapper = document.createElement("div");
analogWrapper.className = "clock-circle";
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");
// Style Wrappers
dateWrapper.className = "date normal medium";
timeWrapper.className = "time bright large light";
secondsWrapper.className = "seconds dimmed";
sunWrapper.className = "sun dimmed small";
moonWrapper.className = "moon dimmed small";
weekWrapper.className = "week dimmed medium";
// Set content of wrappers.
// 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
let timeString;
const now = moment();
if (this.config.timezone) {
now.tz(this.config.timezone);
}
let hourSymbol = "HH";
if (this.config.timeFormat !== 24) {
hourSymbol = "h";
}
if (this.config.clockBold) {
timeString = now.format(`${hourSymbol}[<span class="bold">]mm[</span>]`);
} else {
timeString = now.format(`${hourSymbol}:mm`);
}
if (this.config.showDate) {
dateWrapper.innerHTML = now.format(this.config.dateFormat);
digitalWrapper.appendChild(dateWrapper);
}
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);
}
/**
* Format the time according to the config
*
* @param {object} config The config of the module
* @param {object} time time to format
* @returns {string} The formatted time string
*/
function formatTime(config, time) {
let formatString = `${hourSymbol}:mm`;
if (config.showPeriod && config.timeFormat !== 24) {
formatString += config.showPeriodUpper ? "A" : "a";
}
return moment(time).format(formatString);
}
/****************************************************************
* Create wrappers for Sun Times, only if specified in config
*/
if (this.config.showSunTimes) {
const sunTimes = SunCalc.getTimes(now, this.config.lat, this.config.lon);
const isVisible = now.isBetween(sunTimes.sunrise, sunTimes.sunset);
let nextEvent;
if (now.isBefore(sunTimes.sunrise)) {
nextEvent = sunTimes.sunrise;
} else if (now.isBefore(sunTimes.sunset)) {
nextEvent = sunTimes.sunset;
} else {
const tomorrowSunTimes = SunCalc.getTimes(now.clone().add(1, "day"), this.config.lat, this.config.lon);
nextEvent = tomorrowSunTimes.sunrise;
}
const untilNextEvent = moment.duration(moment(nextEvent).diff(now));
const untilNextEventString = `${untilNextEvent.hours()}h ${untilNextEvent.minutes()}m`;
sunWrapper.innerHTML =
`<span class="${isVisible ? "bright" : ""}"><i class="fas fa-sun" aria-hidden="true"></i> ${untilNextEventString}</span>` +
`<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${formatTime(this.config, sunTimes.sunrise)}</span>` +
`<span><i class="fas 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;
let moonSet;
if (moment(moonTimes.set).isAfter(moonTimes.rise)) {
moonSet = moonTimes.set;
} else {
const nextMoonTimes = SunCalc.getMoonTimes(now.clone().add(1, "day"), this.config.lat, this.config.lon);
moonSet = nextMoonTimes.set;
}
const isVisible = now.isBetween(moonRise, moonSet) || moonTimes.alwaysUp === true;
const illuminatedFractionString = `${Math.round(moonIllumination.fraction * 100)}%`;
moonWrapper.innerHTML =
`<span class="${isVisible ? "bright" : ""}"><i class="fas fa-moon" aria-hidden="true"></i> ${illuminatedFractionString}</span>` +
`<span><i class="fas fa-arrow-up" aria-hidden="true"></i> ${moonRise ? formatTime(this.config, moonRise) : "..."}</span>` +
`<span><i class="fas 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
// Calculate the degree offset for each hand of the clock
if (this.config.timezone) {
now.tz(this.config.timezone);
}
const second = now.seconds() * 6,
minute = now.minute() * 6 + second / 60,
hour = ((now.hours() % 12) / 12) * 360 + 90 + minute / 12;
// Create wrappers
analogWrapper.style.width = this.config.analogSize;
analogWrapper.style.height = this.config.analogSize;
if (this.config.analogFace !== "" && this.config.analogFace !== "simple" && this.config.analogFace !== "none") {
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
// 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") {
analogWrapper.style.border = "2px solid white";
}
const clockFace = document.createElement("div");
clockFace.className = "clock-face";
const clockHour = document.createElement("div");
clockHour.id = "clock-hour";
clockHour.style.transform = `rotate(${hour}deg)`;
clockHour.className = "clock-hour";
const clockMinute = document.createElement("div");
clockMinute.id = "clock-minute";
clockMinute.style.transform = `rotate(${minute}deg)`;
clockMinute.className = "clock-minute";
// Combine analog wrappers
clockFace.appendChild(clockHour);
clockFace.appendChild(clockMinute);
if (this.config.displaySeconds) {
const clockSecond = document.createElement("div");
clockSecond.id = "clock-second";
clockSecond.style.transform = `rotate(${second}deg)`;
clockSecond.className = "clock-second";
clockSecond.style.backgroundColor = this.config.secondsColor;
clockFace.appendChild(clockSecond);
}
analogWrapper.appendChild(clockFace);
}
/*******************************************
* Update placement, respect old analogShowDate even if it's not needed anymore
*/
if (this.config.displayType === "analog") {
// Display only an analog clock
if (this.config.analogShowDate === "top") {
wrapper.classList.add("clock-grid-bottom");
} else if (this.config.analogShowDate === "bottom") {
wrapper.classList.add("clock-grid-top");
}
wrapper.appendChild(analogWrapper);
} else if (this.config.displayType === "digital") {
wrapper.appendChild(digitalWrapper);
} else if (this.config.displayType === "both") {
wrapper.classList.add(`clock-grid-${this.config.analogPlacement}`);
wrapper.appendChild(analogWrapper);
wrapper.appendChild(digitalWrapper);
}
// Return the wrapper to the dom.
return wrapper;
}
});