mirror of
https://github.com/MichMich/MagicMirror.git
synced 2026-04-23 14:27:01 +00:00
## Release Notes Thanks to: @angeldeejay, @in-voker, @JHWelch, @khassel, @KristjanESPERANTO, @rejas, @sdetweil > ⚠️ This release needs nodejs version >=22.21.1 <23 || >=24 (no change to previous release) [Compare to previous Release v2.34.0](https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.34.0...v2.25.0) > ⚠️ We introduced some internal changes with this release, please read [this forum post](https://forum.magicmirror.builders/topic/20138/upcoming-release-april-1-2026-breaking-changes-some-operational-changes) before upgrading! ### [core] - Prepare Release 2.35.0 (#4071) - docs: add security policy and vulnerability reporting guidelines (#4069) - refactor: simplify internal `require()` calls (#4056) - allow environment variables in cors urls (#4033) - fix cors proxy getting binary data (e.g. png, webp) (#4030) - fix: correct secret redaction and optimize loadConfig (#4031) - change loading config.js, allow variables in config.js and try to protect sensitive data (#4029) - remove kioskmode (#4027) - Add dark theme logo (#4026) - move custom.css from css to config (#4020) - move default modules from /modules/default to /defaultmodules (#4019) - update node versions in workflows (#4018) - [core] refactor: extract and centralize HTTP fetcher (#4016) - fix systeminformation not displaying electron version (#4012) - Update node-ical and support it's rrule-temporal changes (#4010) - Change default start scripts from X11 to Wayland (#4011) - refactor: unify favicon for index.html and Electron (#4006) - [core] run systeminformation in subprocess so the info is always displayed (#4002) - set next release dev number (#4000) ### [dependencies] - update dependencies (#4068) - update dependencies incl. electron to v41 (#4058) - chore: upgrade ESLint to v10 and fix newly surfaced issues (#4057) - chore: update ESLint and plugins, simplify config, apply new rules (#4052) - chore: update dependencies + add exports, files, and sideEffects fields to package.json (#4040) - [core] refactor: enable ESLint rule require-await and handle detected issues (#4038) - Update node-ical and other deps (#4025) - chore: update dependencies (#4021) - chore(eslint): migrate from eslint-plugin-vitest to @vitest/eslint-plugin and run rules only on test files (#4014) - Update deps as requested by dependabot (#4008) - update Collaboration.md and dependencies (#4001) ### [logging] - refactor: further logger clean-up (#4050) - Fix Node.js v25 logging prefix and modernize logger (#4049) ### [modules/calendar] - fix(calendar): make showEnd behavior more consistent across time formats (#4059) - test(calendar): fix hardcoded date in event shape test (#4055) - [calendar] refactor: delegate event expansion to node-ical's expandRecurringEvent (#4047) - calendar.js: remove useless hasCalendarURL function (#4028) - fix(calendar): update to node-ical 0.23.1 and fix full-day recurrence lookup (#4013) - fix(calendar): correct day-of-week for full-day recurring events across all timezones (#4004) ### [modules/newsfeed] - fix(newsfeed): fix full article view and add framing check (#4039) - [newsfeed] refactor: migrate to centralized HTTPFetcher (#4023) ### [modules/weather] - fix(weather): fix openmeteo forecast stuck in the past (#4064) - fix(weather): fix weathergov forecast day labels off by one (#4065) - weather: fixes for templates (#4054) - weather: add possibility to override njk's and css (#4051) - Use getDateString in openmeteo (#4046) - [weather] refactor: migrate to server-side providers with centralized HTTPFetcher (#4032) - [weather] feat: add Weather API Provider (#4036) ### [testing] - chore: remove obsolete Jest config and unit test global setup (#4044) - replace template_spec test with config_variables test (#4034) - refactor(clientonly): modernize code structure and add comprehensive tests (#4022) - Switch to undici Agent for HTTPS requests (#4015) - chore: migrate CI workflows to ubuntu-slim for faster startup times (#4007) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: Veeck <github@veeck.de> Co-authored-by: veeck <gitkraken@veeck.de> Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com> Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com> Co-authored-by: DevIncomin <56730075+Developer-Incoming@users.noreply.github.com> Co-authored-by: Nathan <n8nyoung@gmail.com> Co-authored-by: mixasgr <mixasgr@users.noreply.github.com> Co-authored-by: Savvas Adamtziloglou <savvas-gr@greeklug.gr> Co-authored-by: Konstantinos <geraki@gmail.com> Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com> Co-authored-by: BugHaver <43462320+bughaver@users.noreply.github.com> Co-authored-by: BugHaver <43462320+lsaadeh@users.noreply.github.com> Co-authored-by: Koen Konst <koenspero@gmail.com> Co-authored-by: Koen Konst <c.h.konst@avisi.nl> Co-authored-by: dathbe <github@beffa.us> Co-authored-by: Marcel <m-idler@users.noreply.github.com> Co-authored-by: Kevin G. <crazylegstoo@gmail.com> Co-authored-by: Jboucly <33218155+jboucly@users.noreply.github.com> Co-authored-by: Jboucly <contact@jboucly.fr> Co-authored-by: Jarno <54169345+jarnoml@users.noreply.github.com> Co-authored-by: Jordan Welch <JordanHWelch@gmail.com> Co-authored-by: Blackspirits <blackspirits@gmail.com> Co-authored-by: Samed Ozdemir <samed@xsor.io> Co-authored-by: in-voker <58696565+in-voker@users.noreply.github.com> Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com>
319 lines
12 KiB
JavaScript
319 lines
12 KiB
JavaScript
/* global SunCalc, formatTime */
|
|
|
|
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, // options: true, false, 'short'
|
|
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", // DEPRECATED, use CSS instead. Class "clock-second-digital" for digital clock, "clock-second" for analog clock.
|
|
|
|
showSunTimes: false, // options: true, false, 'disableNextEvent'
|
|
showMoonTimes: false, // options: false, 'times' (rise/set), 'percent' (lit percent), 'phase' (current phase), or 'both' (percent & phase)
|
|
lat: 47.630539,
|
|
lon: -122.344147
|
|
},
|
|
// Define required scripts.
|
|
getScripts () {
|
|
return ["moment.js", "moment-timezone.js", "suncalc.js"];
|
|
},
|
|
// Define styles.
|
|
getStyles () {
|
|
return ["clock_styles.css", "font-awesome.css"];
|
|
},
|
|
// Define start sequence.
|
|
start () {
|
|
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 () {
|
|
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";
|
|
|
|
/************************************
|
|
* Create wrappers for DIGITAL clock
|
|
*/
|
|
const dateWrapper = document.createElement("div");
|
|
const timeWrapper = document.createElement("div");
|
|
const hoursWrapper = document.createElement("span");
|
|
const minutesWrapper = document.createElement("span");
|
|
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";
|
|
hoursWrapper.className = "clock-hour-digital";
|
|
minutesWrapper.className = "clock-minute-digital";
|
|
secondsWrapper.className = "clock-second-digital dimmed";
|
|
sunWrapper.className = "sun dimmed small";
|
|
moonWrapper.className = "moon dimmed small";
|
|
weekWrapper.className = "week dimmed medium";
|
|
|
|
// Set content of wrappers.
|
|
const now = moment();
|
|
if (this.config.timezone) {
|
|
now.tz(this.config.timezone);
|
|
}
|
|
|
|
if (this.config.showDate) {
|
|
dateWrapper.innerHTML = now.format(this.config.dateFormat);
|
|
digitalWrapper.appendChild(dateWrapper);
|
|
}
|
|
|
|
if (this.config.displayType !== "analog" && this.config.showTime) {
|
|
let hourSymbol = "HH";
|
|
if (this.config.timeFormat !== 24) {
|
|
hourSymbol = "h";
|
|
}
|
|
|
|
hoursWrapper.innerHTML = now.format(hourSymbol);
|
|
minutesWrapper.innerHTML = now.format("mm");
|
|
|
|
timeWrapper.appendChild(hoursWrapper);
|
|
if (this.config.clockBold) {
|
|
minutesWrapper.classList.add("bold");
|
|
} else {
|
|
timeWrapper.innerHTML += ":";
|
|
}
|
|
timeWrapper.appendChild(minutesWrapper);
|
|
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);
|
|
}
|
|
|
|
/****************************************************************
|
|
* 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 sunWrapperInnerHTML = "";
|
|
|
|
if (this.config.showSunTimes !== "disableNextEvent") {
|
|
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`;
|
|
|
|
sunWrapperInnerHTML = `<span class="${isVisible ? "bright" : ""}"><i class="fas fa-sun" aria-hidden="true"></i> ${untilNextEventString}</span>`;
|
|
}
|
|
|
|
sunWrapperInnerHTML += `<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>`;
|
|
|
|
sunWrapper.innerHTML = sunWrapperInnerHTML;
|
|
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 showFraction = ["both", "percent"].includes(this.config.showMoonTimes);
|
|
const showUnicode = ["both", "phase"].includes(this.config.showMoonTimes);
|
|
const illuminatedFractionString = `${Math.round(moonIllumination.fraction * 100)}%`;
|
|
const image = showUnicode ? [..."🌑🌒🌓🌔🌕🌖🌗🌘"][Math.floor(moonIllumination.phase * 8)] : "<i class=\"fas fa-moon\" aria-hidden=\"true\"></i>";
|
|
|
|
moonWrapper.innerHTML
|
|
= `<span class="${isVisible ? "bright" : ""}">${image} ${showFraction ? 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) {
|
|
if (this.config.showWeek === "short") {
|
|
weekWrapper.innerHTML = this.translate("WEEK_SHORT", { weekNumber: now.week() });
|
|
} else {
|
|
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/MagicMirrorOrg/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; /* DEPRECATED, to be removed in a future version , use CSS instead */
|
|
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.showDate) {
|
|
// Add date to the analog clock
|
|
dateWrapper.innerHTML = now.format(this.config.dateFormat);
|
|
wrapper.appendChild(dateWrapper);
|
|
}
|
|
if (this.config.analogShowDate === "bottom") {
|
|
wrapper.classList.add("clock-grid-bottom");
|
|
} else if (this.config.analogShowDate === "top") {
|
|
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;
|
|
}
|
|
});
|