While looking at the WeatherFlow provider (to evaluate #4107), I noticed
a few things that weren't quite right.
1. **UV index was broken for most providers in forecast/hourly views.**
The templates read `uv_index`, but only the WeatherAPI provider actually
wrote that key. All other providers (OpenWeatherMap, WeatherFlow,
PirateWeather, etc.) use `uvIndex` - so UV was silently never displayed
for them. This went unnoticed because `showUVIndex` defaults to `false`
and there were no test assertions for it. Standardized everything on
`uvIndex` and added test coverage.
2. **WeatherFlow didn't map precipitation for current weather.** The API
provides `precip_accum_local_day` and `precip_probability`, but they
weren't passed through. While adding them I also noticed the template
used truthiness checks, which hid valid zero values. Fixed both.
3. **`||` vs `??` in WeatherFlow provider.** Several numeric fields used
`|| null`, replacing valid `0` with `null`. Switched to `??` for
correctness.
After the big weather refactor (#4032), OpenWeatherMap was effectively
hard-wired to One Call v3. One Call 2.5 is deprecated and no longer
available, so it looked like v2.5 support was effectively over — but the
classic `/weather` and `/forecast` endpoints were never actually
dropped. This restores support for those.
Fixes#4100.
## What this PR does
- handles OpenWeatherMap responses by endpoint again (`/onecall`,
`/weather`, `/forecast`)
- restores v2.5 current and forecast support (including hourly via
3-hour forecast slots)
- filters outdated hourly entries centrally while keeping the current
hour visible (if available)
## Screenshot
<img width="768" height="481" alt="bildo"
src="https://github.com/user-attachments/assets/9bce3531-3731-4fd7-b41e-e20603afa725"
/>
During the server-side migration (PR #4032), the `instanceId` was built
with `Date.now()`, making it unique per client reload. This approach was
fine in the old browser-based architecture where reloads cleaned up
everything automatically. But now the node_helper persists across
reloads, so each reconnect created a new `HTTPFetcher` while the old one
kept polling - silently multiplying API calls over time.
The fix is simple: use `this.identifier` as the `instanceId`, which is
already stable and unique per module slot. This is the same approach the
Calendar module uses.
On the node_helper side, when a provider already exists for the same
`instanceId`, we now skip re-creation and just resend
`WEATHER_INITIALIZED` so the client picks up where it left off.
Fixes https://forum.magicmirror.builders/topic/20199
In PR #4072 GitHub Bot complained that `newsfeedfetcher.js` used the old
`.pipe()` method to connect streams (HTTP body → iconv decoding → RSS
parser). `.pipe()` has a weakness: errors in one stream are **not**
automatically forwarded to downstream streams. An I/O or decoding error
would silently disappear.
## Solution
Replaced `.pipe()` with `await stream.promises.pipeline()`. The
`pipeline()` API is designed to propagate errors correctly through the
entire chain and to clean up all streams on failure. Errors now reliably
land in the `catch` block and call `fetchFailedCallback` exactly once.
The redundant `parser.on("error")` handler was removed, as it would have
caught the same error again and called the callback a second time.
## Why not the bot's suggested fix?
The GitHub Bot suggested the older callback-based
`stream.pipeline(callback)` variant:
```js
stream.pipeline(nodeStream, iconv.decodeStream(...), parser, (error) => {
if (!error) return;
// error handling...
});
```
This has two drawbacks compared to my approach:
1. It uses the older callback style — `stream.promises.pipeline()` with
`async/await` is the modern, more readable API.
2. The bot's suggestion kept both the `parser.on("error")` handler
**and** the `catch` block, which would not have fixed the
double-callback problem.
----
Related to #4073
## Summary
This PR migrates the SMHI weather provider from the deprecated PMP3gv2
API to the new SNOW1gv1 API.
The old API (pmp3g/version/2) started returning HTTP 404 on 2026-03-31.
## Changes
- Updated API endpoint:
- `pmp3g/version/2` → `snow1g/version/1`
- Updated response parsing:
- `validTime` → `time`
- `parameters[]` → `data` (flat structure)
- Updated parameter names:
- `t` → `air_temperature`
- `ws` → `wind_speed`
- etc.
- Updated precipitation handling to match new
`predominant_precipitation_type_at_surface`
- Updated coordinate parsing (flat `[lon, lat]`)
- Added missing value handling (`9999 → null`)
## Notes
- Maintains backward compatibility for `precipitationValue` config
- No UI changes
- Symbol mapping unchanged (same codes 1–27)
## Testing
- Verified against live SMHI SNOW1gv1 API responses
- Confirmed old API returns HTTP 404
## Impact
Fixes broken SMHI provider due to API deprecation.
In PR #4072 GitHub Bot complained about an unused var. Instead of just
removing that one, I checked why ESLint hadn't complained about it: We
had disabled the rule for it.
So I enabled rule and resolved the issues that ESLint then detected.
Related to #4073
The URL was built once at startup with a hardcoded start_date. Since
HTTPFetcher reuses the same URL, the forecast never advanced past day
one.
Use forecast_days instead — Open-Meteo resolves it relative to today on
every request. Other providers are not affected as they don't use dates
in their URLs.
Fixes#4063
While looking at all weather providers for #4063, I noticed another
unrelated bug: The weathergov forecast has been off by one day. This has
been since its first commit in 2019 and was not caused by the recent
weather refactor.
The provider built the days array correctly, but then threw away the
first entry (`slice(1)`) because it was considered "incomplete". The
problem: the template uses index position to decide what to label
"Today" and "Tomorrow" — so dropping today made Monday show up as
"Today", Tuesday as "Tomorrow", and one day disappeared entirely.
The fix is a one-liner: remove `slice(1)`. Today's entry now shows
min/max from the remaining hours of the day, which is the right
behaviour anyway. In my understanding it's not a problem at all that the
first day is incomplete.
## Before
Notice in the screenshot that it is Sunday. So after today and tomorrow,
Tuesday should come next, but it says **Wednesday** instead.
<img width="385" height="365" alt="Ekrankopio de 2026-03-22 14-37-55"
src="https://github.com/user-attachments/assets/02295cc6-4421-40a8-929e-6c6721dece97"
/>
## After
<img width="385" height="365" alt="Ekrankopio de 2026-03-22 14-38-34"
src="https://github.com/user-attachments/assets/cb51ca01-7882-4805-8cf4-a78f6721038a"
/>
Closes#4053
This started as a small fix.
After feedback and more debugging, I found more issues and
inconsistencies and went a bit down the rabbit hole.
The PR is bigger now, but I think the result is better: behavior is more
predictable, and the output is more consistent.
## Changes
- Multi-day full-day events now show an end date in `relative` and
`dateheaders` when `showEnd` is enabled.
- With `absolute` + `nextDaysRelative`, full-day events now keep the end
date when the start is replaced by TODAY/TOMORROW/etc.
- Timed events in `absolute` now also respect
`showEndsOnlyWithDuration`.
- Tests were expanded and refactored to cover more showEnd cases and
keep the setup easier to maintain.
- I also refactored parts of the calendar time rendering to reduce
duplicate logic.
## Before
<img width="1816" height="756" alt="before"
src="https://github.com/user-attachments/assets/ebec81fd-0c4a-4f9f-bbe3-e2b32ef6756e"
/>
## After
<img width="1816" height="756" alt="after"
src="https://github.com/user-attachments/assets/8a2c652d-dddc-4f6b-9074-fbef3411f9ed"
/>
`eslint-plugin-import-x` was the last thing blocking the ESLint v10
upgrade - it just got v10 support. So here we go.
The upgrade itself is tiny. The rest of the diff is cleanup from issues
ESLint v10 now catches: a few `let` declarations with initial values
that were immediately overwritten anyway (`no-useless-assignment`), and
`Translator` listed in `/* global */` in `main.js` and `module.js`.
Working through those `no-useless-assignment` warnings also surfaced a
dead default in `openmeteo`: `maxEntries: 5` in the constructor, which
was never actually doing anything - `openmeteo` never reads
`this.config.maxEntries` anywhere. And `weather.js` already sets that
default for all providers, so it was just a redundant duplicate. Removed
that too.
No runtime behavior changes.
Follow up for #4051
- fix loading default weather.css (the construction with `./weather.css`
gave a 404)
- accept `themeDir` with and without trailing slash
This is an approach for #2909
It adds 2 values to the config:
```js
themeDir: "./",
themeCustomScripts: []
```
`themeDir` must be specified relative to the weather dir.
Example config:
```js
{
module: "weather",
position: "top_center",
config: {
weatherProvider: "openmeteo",
type: "current",
lat: 40.776676,
lon: -73.971321,
themeDir: "../../../modules/MyWeatherTemplate/",
themeCustomScripts: [ "skycons.js", "weathertheme.js" ],
}
},
```
The `themeDir` must contain the 4 files
- current.njk
- forecast.njk
- hourly.njk
- weather.css
You can add more files (if needed) and add them to the
`themeCustomScripts` so they are loaded as script.
There are 2 methods inserted which are called if defined:
- initWeatherTheme: For doing special things when starting the module
- updateWeatherTheme: For doing special things before updating the dom
I see this as a simple approach for overriding the default njk templates
and css. I did already convert my
[MMM-WeatherBridge](https://gitlab.com/khassel/MMM-WeatherBridge) into a
template.
This PR updates ESLint and the ESLint plugins to their latest versions
and takes advantage of the new versions to simplify the config.
The main cleanup: removed all explicit `plugins: {}` registrations from
`eslint.config.mjs`. When passing direct config objects like
`js.configs.recommended`, the plugin registration is already included –
we were just doing it twice.
Two lint warnings are also fixed:
- A wrong import style for `eslint-plugin-package-json` (named vs.
default)
- `playwright/no-duplicate-hooks` is disabled for e2e tests – the rule
doesn't handle plain `beforeAll()`/`afterAll()` (Vitest style) correctly
and produces false positives. I've created an issue for that:
https://github.com/mskelton/eslint-plugin-playwright/issues/443.
Built-in Node.js imports were manually updated to use the `node:` prefix
(e.g. `require("fs")` → `require("node:fs")`). Minor formatting fixes
were applied automatically by `eslint --fix`.
node-ical 0.25.x added `expandRecurringEvent()` — a proper API for
expanding both recurring and non-recurring events, including EXDATE
filtering and RECURRENCE-ID overrides. This PR replaces our hand-rolled
equivalent with it.
`calendarfetcherutils.js` loses ~125 lines of code. What's left only
deals with MagicMirror-specific concerns: timezone conversion,
config-based filtering, and output formatting. The extra lines in the
diff come from new tests.
## What was removed
- `getMomentsFromRecurringEvent()` — manual rrule.js wrapping with
custom date extraction
- `isFullDayEvent()` — heuristic with multiple fallback checks
- `isFacebookBirthday` workaround — patched years < 1900 and
special-cased `@facebook.com` UIDs
- The `if (event.rrule) / else` split — all events now go through a
single code path
## Bugs fixed along the way
Both were subtle enough to go unnoticed before:
- **`[object Object]` in event titles/description/location** — node-ical
represents ICS properties with parameters (e.g.
`DESCRIPTION;LANGUAGE=de:Text`) as `{val, params}` objects. The old code
passed them straight through. Mainly affected multilingual Exchange/O365
setups. Fixed with `unwrapParameterValue()`.
- **`excludedEvents` with `until` never worked** —
`shouldEventBeExcluded()` returned `{ excluded, until }` but the caller
destructured it as `{ excluded, eventFilterUntil }`, so the until date
was always `undefined` and events were never hidden. Fixed by correcting
the destructuring key.
The expansion loop also gets error isolation: a single broken event is
logged and skipped instead of aborting the whole feed.
## Other clean-ups
- Replaced `this.shouldEventBeExcluded` with
`CalendarFetcherUtils.shouldEventBeExcluded` — avoids context-loss bugs
when the method is destructured or called indirectly.
- Replaced deprecated `substr()` with `slice()`.
- Replaced `now < filterUntil` (operator overloading) with
`now.isBefore(filterUntil)` — idiomatic Moment.js comparison.
- Fixed `@returns` JSDoc: `string[]` → `object[]`.
- Moved verbose `Log.debug("Processing entry...")` after the `VEVENT`
type guard to reduce log noise from non-event entries.
- Replaced `JSON.stringify(event)` debug log with a lightweight summary
to avoid unnecessary serialization cost.
- Added comment explaining the 0-duration → end-of-day fallback for
events without DTEND.
## Tests
24 unit tests, all passing (`npx vitest run
tests/unit/modules/default/calendar/`).
New coverage: `excludedEvents` with/without `until`, Facebook birthday
year expansion, output object shape, no-DTEND fallback, error isolation,
`unwrapParameterValue`, `getTitleFromEvent`, ParameterValue properties,
RECURRENCE-ID overrides, DURATION (single and recurring).
I was playing around with the newsfeed notification system
(`ARTICLE_MORE_DETAILS`, `ARTICLE_TOGGLE_FULL`, …) and discovered some
issues with the full article view:
The iframe was loading the CORS proxy URL instead of the actual article
URL, which could cause blank screens depending on the feed. Also, many
news sites block iframes entirely (`X-Frame-Options: DENY`) and the user
got no feedback at all — just an empty page. On top of that, scrolling
used `window.scrollTo()` which moved the entire MagicMirror page instead
of just the article.
This PR cleans that up:
- Use the raw article URL for the iframe (CORS proxy is only needed for
server-side feed fetching)
- Check `X-Frame-Options` / `Content-Security-Policy` headers
server-side before showing the iframe — if the site blocks it, show a
brief "Article cannot be displayed here." message and return to normal
view
- Show the iframe as a fixed full-screen overlay so other modules aren't
affected, scroll via `container.scrollTop`
- Keep the progressive disclosure behavior for `ARTICLE_MORE_DETAILS`
(title → description → iframe → scroll)
- Delete `fullarticle.njk`, replace with `getDom()` override
- Fix `ARTICLE_INFO_RESPONSE` returning proxy URL instead of real URL
- A few smaller fixes (negative scroll, null guard)
- Add `NEWSFEED_ARTICLE_UNAVAILABLE` translation to all 47 language
files
- Add e2e tests for the notification handlers (`ARTICLE_NEXT`,
`ARTICLE_PREVIOUS`, `ARTICLE_INFO_REQUEST`, `ARTICLE_LESS_DETAILS`)
## What this means for users
- The full article view now works reliably across different feeds
- If a news site blocks iframes, the user sees a brief message instead
of a blank screen
- Additional e2e tests make the module more robust and less likely to
break silently in future MagicMirror versions
Enable the `require-await` ESLint rule. Async functions without `await`
are just regular functions with extra overhead — marking them `async`
adds implicit Promise wrapping, can hide missing `return` statements,
and misleads readers into expecting asynchronous behavior where there is
none.
While fixing the violations, I removed unnecessary `async` keywords from
source files and from various test callbacks that never used `await`.
This migrates the Weather module from client-side fetching to use the
server-side centralized HTTPFetcher (introduced in #4016), following the
same pattern as the Calendar and Newsfeed modules.
## Motivation
This brings consistent error handling and better maintainability and
completes the refactoring effort to centralize HTTP error handling
across all default modules.
Migrating to server-side providers with HTTPFetcher brings:
- **Centralized error handling**: Inherits smart retry strategies
(401/403, 429, 5xx backoff) and timeout handling (30s)
- **Consistency**: Same architecture as Calendar and Newsfeed modules
- **Security**: Possibility to hide API keys/secrets from client-side
- **Performance**: Reduced API calls in multi-client setups - one server
fetch instead of one per client
- **Enabling possible future features**: e.g. server-side caching, rate
limit monitoring, and data sharing with third-party modules
## Changes
- All 10 weather providers now use HTTPFetcher for server-side fetching
- Consistent error handling like Calendar and Newsfeed modules
## Breaking Changes
None. Existing configurations continue to work.
## Testing
To ensure proper functionality, I obtained API keys and credentials for
all providers that require them. I configured all 10 providers in a
carousel setup and tested each one individually. Screenshots for each
provider are attached below demonstrating their working state.
I even requested developer access from the Tempest/WeatherFlow team to
properly test this provider.
**Comprehensive test coverage**: A major advantage of the server-side
architecture is the ability to thoroughly test providers with unit tests
using real API response snapshots. Don't be alarmed by the many lines
added in this PR - they are primarily test files and real-data mocks
that ensure provider reliability.
## Review Notes
I know this is an enormous change - I've been working on this for quite
some time. Unfortunately, breaking it into smaller incremental PRs
wasn't feasible due to the interdependencies between providers and the
shared architecture.
Given the scope, it's nearly impossible to manually review every change.
To ensure quality, I've used both CodeRabbit and GitHub Copilot to
review the code multiple times in my fork, and both provided extensive
and valuable feedback. Most importantly, my test setup with all 10
providers working successfully is very encouraging.
## Related
Part of the HTTPFetcher migration #4016.
## Screenshots
<img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-06-54"
src="https://github.com/user-attachments/assets/2139f4d2-2a9b-4e49-8d0a-e4436983ed6e"
/>
<img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-02"
src="https://github.com/user-attachments/assets/880f7ce2-4e44-42d5-bfe4-5ce475cca7c2"
/>
<img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-07"
src="https://github.com/user-attachments/assets/abd89933-fe03-40ab-8a7c-41ae1ff99255"
/>
<img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-12"
src="https://github.com/user-attachments/assets/22225852-f0a9-4d33-87ab-0733ba30fad3"
/>
<img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-17"
src="https://github.com/user-attachments/assets/7a7192a5-f237-4060-85d7-6f50b9bef5af"
/>
<img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-22"
src="https://github.com/user-attachments/assets/df84d9f1-e531-4995-8da8-d6f2601b6a08"
/>
<img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-27"
src="https://github.com/user-attachments/assets/4cf391ac-db43-4b52-95f4-f5eadc5ea34d"
/>
<img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-32"
src="https://github.com/user-attachments/assets/8dd8e688-d47f-4815-87f6-7f2630f15d58"
/>
<img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-37"
src="https://github.com/user-attachments/assets/ee84a8bc-6b35-405a-b311-88658d9268dd"
/>
<img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-42"
src="https://github.com/user-attachments/assets/f941f341-453f-4d4d-a8d9-6b9158eb2681"
/>
Provider "Weather API" added later:
<img width="1910" height="1080" alt="Ekrankopio de 2026-02-15 19-39-06"
src="https://github.com/user-attachments/assets/3f0c8ba3-105c-4f90-8b2e-3a1be543d3d2"
/>
Found this while debugging.
The `hasCalendarURL` function does a check if the url is in the config.
But the calendar sees only his own config part, so this check is always
true (tested with more than one calendar module in `config.js`).
This migrates the Newsfeed module to use the centralized HTTPFetcher
class (introduced in #4016), following the same pattern as the Calendar
module.
This continues the refactoring effort to centralize HTTP error handling
across all modules.
## Changes
**NewsfeedFetcher:**
- Refactored from function constructor to ES6 class (like the calendar
module in #3959)
- Replaced manual fetch() + timer handling with HTTPFetcher composition
- Uses structured error objects with translation keys
- Inherits smart retry strategies (401/403, 429, 5xx backoff)
- Inherits timeout handling (30s) and AbortController
**node_helper.js:**
- Updated error handler to use `errorInfo.translationKey`
- Simplified property access (`fetcher.url`, `fetcher.items`)
**Cleanup:**
- Removed `js/module_functions.js` (`scheduleTimer` no longer needed)
- Removed `#module_functions` import from package.json
## Related
Part of the HTTPFetcher migration effort started in #4016.
Next candidate: Weather module (client-side → server-side migration).
Since the project's inception, I've missed a clear separation between
default and third-party modules.
This increases complexity within the project (exclude `modules`, but not
`modules/default`), but the mixed use is particularly problematic in
Docker setups.
Therefore, with this pull request, I'm moving the default modules to a
different directory.
~~I've chosen `default/modules`, but I'm not bothered about it;
`defaultmodules` or something similar would work just as well.~~
Changed to `defaultmodules`.
Let me know if there's a majority in favor of this change.