-
-
Notifications
You must be signed in to change notification settings - Fork 2
Fix: Theme change during sleep causes mixed theme on wake-up #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
1ba1c8f
New design - Draft
edwardtfn 4223a39
Reduced fonts
edwardtfn 2b68af8
Reduced fonts
edwardtfn b4c41eb
Always fall back to page home
edwardtfn e5e7dfa
Add support for downloading the new model
edwardtfn de49028
Add weather library to component
edwardtfn fb381c8
Fix script name
edwardtfn 78440de
Add "Where to buy" page
edwardtfn c864baf
Define `NSPANEL_EASY_USE_WEATHER`
edwardtfn b85c148
Update theme while in screensaver
edwardtfn 8aec0ef
Inform new design is under construction
edwardtfn a535552
Lint
edwardtfn 6d02dfd
Update docs/where_to_buy.md
edwardtfn 4674303
fix(weather): ensure null-termination after strncpy in get_weather_index
edwardtfn 3ad0b13
Merge branch 'v9999.99.9' of https://github.com/edwardtfn/NSPanel-Eas…
edwardtfn 96ee959
Fix incorrect region label for the UK row
edwardtfn f863a78
Fix off-by-one when trimming the first log line
edwardtfn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| // weather.cpp | ||
|
|
||
| #ifdef NSPANEL_EASY_USE_WEATHER | ||
|
|
||
| #include "weather.h" | ||
|
|
||
| namespace nspanel_easy { | ||
|
|
||
| SunInfo sun_info = { | ||
| .is_up = true, // Safe daytime default before first blueprint update or SNTP sync | ||
| .coord_received = false, // Coordinates not yet received - time proxy active | ||
| }; | ||
|
|
||
| uint8_t weather_condition_index = 0; // Defaults to fallback until first blueprint update | ||
|
|
||
| } // namespace nspanel_easy | ||
|
|
||
| #endif // NSPANEL_EASY_USE_WEATHER |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,232 @@ | ||
| // weather.h | ||
|
|
||
| #pragma once | ||
|
|
||
| #ifdef NSPANEL_EASY_USE_WEATHER | ||
|
|
||
| #include <cstdint> | ||
| #include <cstring> | ||
|
|
||
| /** | ||
| * @file weather.h | ||
| * @brief Weather condition to Nextion picture ID mapping. | ||
| * | ||
| * Provides the lookup table and helper functions to resolve a Home Assistant | ||
| * weather condition string to the correct Nextion picture ID, taking into | ||
| * account device generation (legacy / new), active theme (light / dark), and | ||
| * sun elevation (above / below horizon). | ||
| * | ||
| * Condition strings are normalised before lookup: hyphens are replaced with | ||
| * underscores, so "clear-night" and "clear_night" resolve to the same entry. | ||
| * Unrecognised conditions fall back to index 0 (the sentinel / fallback entry). | ||
| * | ||
| * Sun elevation is tracked via @ref SunInfo. When valid coordinates have not | ||
| * yet been received from the blueprint, @ref SunInfo::is_up is derived from | ||
| * the local time using a simple 06:00-18:00 proxy. Once coordinates are | ||
| * available, the ESPHome @c sun component drives the state via its | ||
| * @c on_sunrise and @c on_sunset triggers. | ||
| */ | ||
|
|
||
| namespace nspanel_easy { | ||
|
|
||
| // ============================================================================= | ||
| // Sun elevation state | ||
| // ============================================================================= | ||
|
|
||
| /** | ||
| * @brief Tracks sun elevation state and coordinate availability. | ||
| * | ||
| * When @p coord_received is @c false, @p is_up is derived from the local | ||
| * time as a rough proxy (06:00-18:00 = above horizon) until the blueprint | ||
| * sends valid coordinates and the ESPHome @c sun component takes over via | ||
| * its @c on_sunrise and @c on_sunset triggers. | ||
| * | ||
| * Both fields are intentionally kept together so callers can check | ||
| * @p coord_received and @p is_up in a single struct access. | ||
| */ | ||
| struct SunInfo { | ||
| bool is_up; ///< true when the sun is above the horizon | ||
| bool coord_received; ///< true once valid coordinates have been received from the blueprint | ||
| }; | ||
|
|
||
| extern SunInfo sun_info; ///< Global sun elevation state for the weather engine | ||
|
|
||
| // ============================================================================= | ||
| // Data structures | ||
| // ============================================================================= | ||
|
|
||
| /** | ||
| * @brief Picture IDs for one condition in one theme variant. | ||
| * | ||
| * Sun-unaware conditions set @p sun_down equal to @p sun_up. | ||
| * The caller selects between them using @ref SunInfo::is_up or | ||
| * @c sun_component->is_above_horizon(). | ||
| */ | ||
| struct WeatherPicVariant { | ||
| uint16_t sun_up; ///< Picture ID when the sun is above the horizon | ||
| uint16_t sun_down; ///< Picture ID when the sun is below the horizon | ||
| }; | ||
|
|
||
| /** | ||
| * @brief All picture variants for a single weather condition. | ||
| * | ||
| * Two device generations x two themes, each with a sun-up / sun-down split. | ||
| * A picture ID of 0 indicates the slot is not yet assigned. | ||
| */ | ||
| struct WeatherPics { | ||
| WeatherPicVariant legacy_light; ///< Legacy model, light theme | ||
| WeatherPicVariant legacy_dark; ///< Legacy model, dark theme | ||
| WeatherPicVariant new_light; ///< New model, light theme | ||
| WeatherPicVariant new_dark; ///< New model, dark theme | ||
| }; | ||
|
|
||
| /** | ||
| * @brief Associates a normalised condition string with its picture set. | ||
| * | ||
| * Index 0 is always the sentinel / fallback entry (null key). | ||
| * Named entries occupy indices 1 and above. | ||
| */ | ||
| struct WeatherConditionEntry { | ||
| const char* key; ///< Condition name (underscore-separated, lower-case), or null for the fallback entry | ||
| WeatherPics pics; ///< Picture IDs for this condition, or fallback IDs for the sentinel | ||
| }; | ||
|
|
||
| // ============================================================================= | ||
| // Lookup table | ||
| // ============================================================================= | ||
|
|
||
| /// @brief Sun-unaware variant: same picture regardless of sun elevation. | ||
| #define WPV(id) { (id), (id) } | ||
| /// @brief Sun-aware variant: distinct pictures for day and night. | ||
| #define WPV2(up, down) { (up), (down) } | ||
|
|
||
| /** | ||
| * @brief Lookup table mapping weather condition strings to picture IDs. | ||
| * | ||
| * Index 0 is the fallback entry (null key), used for unknown, unavailable, | ||
| * and any unrecognised condition strings Home Assistant may send. | ||
| * Named entries are sorted alphabetically from index 1 for readability. | ||
| * | ||
| * The linear search in @ref get_weather_index is O(n) over at most ~15 | ||
| * named entries - performance is not a concern at this scale. | ||
| * | ||
| * @note Picture IDs of 0 indicate unassigned slots - the display will | ||
| * not update for those combinations until IDs are filled in. | ||
| */ | ||
| constexpr WeatherConditionEntry WEATHER_CONDITIONS[] = { | ||
| // legacy_light legacy_dark new_light new_dark | ||
| { nullptr, { WPV(49), WPV(1), WPV(0), WPV(0) } }, ///< Index 0 - fallback | ||
| { "clear_night", { WPV2(50, 63), WPV2(2, 15), WPV2(14, 1), WPV2(27, 15) } }, ///< Index 1 | ||
| { "cloudy", { WPV(51), WPV(3), WPV(2), WPV(16) } }, ///< Index 2 | ||
| { "exceptional", { WPV2(61, 62), WPV2(13, 14), WPV(0), WPV(0) } }, ///< Index 3 | ||
| { "fog", { WPV(56), WPV(8), WPV(3), WPV(17) } }, ///< Index 4 | ||
| { "hail", { WPV(55), WPV(7), WPV(4), WPV(18) } }, ///< Index 5 | ||
| { "lightning", { WPV(58), WPV(10), WPV(5), WPV(19) } }, ///< Index 6 | ||
| { "lightning_rainy", { WPV2(61, 62), WPV2(13, 14), WPV(6), WPV(20) } }, ///< Index 7 | ||
| { "partlycloudy", { WPV2(59, 60), WPV2(11, 12), WPV2(7, 8), WPV2(21, 22) } }, ///< Index 8 | ||
| { "pouring", { WPV(53), WPV(5), WPV(9), WPV(23) } }, ///< Index 9 | ||
| { "rainy", { WPV(52), WPV(4), WPV(10), WPV(24) } }, ///< Index 10 | ||
| { "snowy", { WPV(54), WPV(6), WPV(11), WPV(25) } }, ///< Index 11 | ||
| { "snowy_rainy", { WPV(55), WPV(7), WPV(12), WPV(26) } }, ///< Index 12 | ||
| { "sunny", { WPV2(50, 63), WPV2(2, 15), WPV2(13, 1), WPV2(27, 15) } }, ///< Index 13 | ||
| { "windy", { WPV(57), WPV(9), WPV(14), WPV(28) } }, ///< Index 14 | ||
| { "windy_variant", { WPV(57), WPV(9), WPV(14), WPV(28) } }, ///< Index 15 | ||
| }; | ||
|
|
||
| #undef WPV | ||
| #undef WPV2 | ||
|
|
||
| // ============================================================================= | ||
| // Current condition state | ||
| // ============================================================================= | ||
|
|
||
| /** | ||
| * @brief Index into @ref WEATHER_CONDITIONS for the current weather condition. | ||
| * | ||
| * Set by @ref get_weather_index when a new condition string is received from | ||
| * the blueprint. Defaults to 0 (fallback) until the first update arrives. | ||
| */ | ||
| extern uint8_t weather_condition_index; | ||
|
|
||
| // ============================================================================= | ||
| // Helper functions | ||
| // ============================================================================= | ||
|
|
||
| /** | ||
| * @brief Normalises a weather condition string for table lookup. | ||
| * | ||
| * Replaces hyphens with underscores in-place so that "clear-night" and | ||
| * "clear_night" both resolve to the same key. | ||
| * | ||
| * @param[in,out] buf Null-terminated string to normalise (modified in place). | ||
| * @param[in] size Size of the buffer including the null terminator. | ||
| */ | ||
| inline void normalise_weather_condition(char* buf, size_t size) { | ||
| for (size_t i = 0; i < size && buf[i] != '\0'; ++i) { | ||
| if (buf[i] == '-') | ||
| buf[i] = '_'; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @brief Looks up the index of a weather condition string in @ref WEATHER_CONDITIONS. | ||
| * | ||
| * Normalises the input (hyphens to underscores) once before searching the | ||
| * table. The search starts at index 1, skipping the fallback entry at index 0. | ||
| * Returns 0 for any null, empty, or unrecognised condition string. | ||
| * | ||
| * @param condition Null-terminated condition string (hyphens or underscores). | ||
| * @return Index into @ref WEATHER_CONDITIONS, or 0 if not found. | ||
| */ | ||
| inline uint8_t get_weather_index(const char* condition) { | ||
| if (condition == nullptr || *condition == '\0') | ||
| return 0; | ||
|
|
||
| char buf[32] = {}; | ||
| strncpy(buf, condition, sizeof(buf) - 1); | ||
| normalise_weather_condition(buf, sizeof(buf)); | ||
|
|
||
| constexpr uint8_t count = | ||
| sizeof(WEATHER_CONDITIONS) / sizeof(WEATHER_CONDITIONS[0]); | ||
| for (uint8_t i = 1; i < count; ++i) { | ||
| if (strcmp(WEATHER_CONDITIONS[i].key, buf) == 0) | ||
| return i; | ||
| } | ||
| return 0; // Fallback | ||
| } | ||
|
|
||
| /** | ||
| * @brief Returns the picture set for a given @ref WEATHER_CONDITIONS index. | ||
| * | ||
| * Clamps out-of-range indices to 0 (fallback) to guard against stale state. | ||
| * | ||
| * @param index Index into @ref WEATHER_CONDITIONS. | ||
| * @return Reference to the matching @ref WeatherPics. | ||
| */ | ||
| inline const WeatherPics& get_weather_pics(uint8_t index) { | ||
| constexpr uint8_t count = | ||
| sizeof(WEATHER_CONDITIONS) / sizeof(WEATHER_CONDITIONS[0]); | ||
| if (index >= count) | ||
| index = 0; // Clamp to fallback | ||
| return WEATHER_CONDITIONS[index].pics; | ||
| } | ||
|
|
||
| /** | ||
| * @brief Selects the correct picture variant based on device and theme. | ||
| * | ||
| * @param pics Picture set returned by @ref get_weather_pics. | ||
| * @param is_new_device @c true for the new device generation, @c false for legacy. | ||
| * @param is_dark_theme @c true when the dark theme is active. | ||
| * @return Reference to the matching @ref WeatherPicVariant. | ||
| */ | ||
| inline const WeatherPicVariant& select_weather_variant(const WeatherPics& pics, | ||
| bool is_new_device, | ||
| bool is_dark_theme) { | ||
| if (is_new_device) | ||
| return is_dark_theme ? pics.new_dark : pics.new_light; | ||
| return is_dark_theme ? pics.legacy_dark : pics.legacy_light; | ||
| } | ||
|
|
||
| } // namespace nspanel_easy | ||
|
|
||
| #endif // NSPANEL_EASY_USE_WEATHER |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| # Where to Buy the Sonoff NSPanel | ||
|
|
||
| This guide helps you find the right hardware to run NSPanel Easy. | ||
|
|
||
| <!-- markdownlint-disable MD028 --> | ||
| > [!IMPORTANT] | ||
| > NSPanel Easy supports the **Sonoff NSPanel EU** and **Sonoff NSPanel US** models only. | ||
| > The **NSPanel Pro** is a completely different device and is **not supported**. | ||
|
|
||
| > [!NOTE] | ||
| > Links marked with 🤝 are affiliate links. | ||
| > Using them costs you nothing extra and may earn a small credit that helps fund this project. | ||
| > Plain links carry no affiliation. | ||
| <!-- markdownlint-enable MD028 --> | ||
|
|
||
| --- | ||
|
|
||
| ## Hardware Overview | ||
|
|
||
| | Model | Wall plate format | Typical region | | ||
| |-------|-------------------|----------------| | ||
| | NSPanel EU | 86 mm × 86 mm (standard EU/CN wall box) | Europe, Asia, most of the world | | ||
| | NSPanel US | 120 mm × 74 mm (standard US/CA wall box) | North America | | ||
|
|
||
| Both models run the same NSPanel Easy firmware. | ||
| Choose based on your wall box format, not your geography. | ||
| See [NSPanel Models](nspanel_models.md) for a detailed comparison. | ||
|
|
||
| --- | ||
|
|
||
| ## Where to Buy | ||
|
|
||
| <!-- markdownlint-disable MD013 MD033 --> | ||
| | Region / Ships from | NSPanel EU — Dim Gray | NSPanel EU — White | NSPanel US — Dim Gray | NSPanel US — White | Pre-flashed | | ||
| |---------------------|----------------------|--------------------|----------------------|--------------------|-------------| | ||
| | 🌍 Global (ships from China) | [itead.cc](https://itead.cc/product/sonoff-nspanel-smart-scene-wall-switch/ref/288/) 🤝 | [itead.cc](https://itead.cc/product/sonoff-nspanel-smart-scene-wall-switch/ref/288/) 🤝 | [itead.cc](https://itead.cc/product/sonoff-nspanel-smart-scene-wall-switch/ref/288/) 🤝 | [itead.cc](https://itead.cc/product/sonoff-nspanel-smart-scene-wall-switch/ref/288/) 🤝 | — | | ||
| | 🇸🇪 EU (Sweden) | [styrahem.se](https://www.styrahem.se/p/724)<br>[amazon.se](https://www.amazon.se/dp/B09MS7JDVQ?tag=edwardtfn-21) 🤝 | [styrahem.se](https://www.styrahem.se/p/808)<br>[amazon.se](https://www.amazon.se/dp/B0BNMRB2FZ?tag=edwardtfn-21) 🤝 | [styrahem.se](https://www.styrahem.se/p/725) | [styrahem.se](https://www.styrahem.se/p/809) | [styrahem.se](https://www.styrahem.se/p/3919) | | ||
| | 🇬🇧 EU (United Kingdom) | [amazon.co.uk](https://www.amazon.co.uk/dp/B09MS7JDVQ?tag=edwardtfn0c-21) 🤝 | [amazon.co.uk](https://www.amazon.co.uk/dp/B0BNMRB2FZ?tag=edwardtfn0c-21) 🤝 | — | [amazon.co.uk](https://www.amazon.co.uk/dp/B0BNMRHYG5?tag=edwardtfn0c-21) 🤝 | — | | ||
| | 🇩🇪 EU (Germany) | [amazon.de](https://www.amazon.de/dp/B09MS7JDVQ?tag=edwardtfn05-21) 🤝 | [amazon.de](https://www.amazon.de/dp/B0BNMRB2FZ?tag=edwardtfn05-21) 🤝 | [amazon.de](https://www.amazon.de/dp/B09MRZQCN2?tag=edwardtfn05-21) 🤝 | — | — | | ||
| | 🇪🇸 EU (Spain) | [amazon.es](https://www.amazon.es/dp/B09MS7JDVQ?tag=edwardtfn03-21) 🤝 | [amazon.es](https://www.amazon.es/dp/B0BNMRB2FZ?tag=edwardtfn03-21) 🤝 | [amazon.es](https://www.amazon.es/dp/B09MRZQCN2?tag=edwardtfn03-21) 🤝 | — | — | | ||
| | 🇫🇷 EU (France) | [amazon.fr](https://www.amazon.fr/dp/B09MS7JDVQ?tag=edwardtfn0b-21) 🤝 | [amazon.fr](https://www.amazon.fr/dp/B0BNMRB2FZ?tag=edwardtfn0b-21) 🤝 | — | — | — | | ||
| | 🇮🇹 EU (Italy) | [amazon.it](https://www.amazon.it/dp/B09MS7JDVQ?tag=edwardtfn0e-21) 🤝 | [amazon.it](https://www.amazon.it/dp/B0BNMRB2FZ?tag=edwardtfn0e-21) 🤝 | [amazon.it](https://www.amazon.it/dp/B09MRZQCN2?tag=edwardtfn0e-21) 🤝 | [amazon.it](https://www.amazon.it/dp/B0BNMRHYG5?tag=edwardtfn0e-21) 🤝 | — | | ||
| | 🇺🇸 US | — | — | [amazon.com](https://www.amazon.com/dp/B09MRZQCN2?tag=edwardtfn-20) 🤝 | [amazon.com](https://www.amazon.com/dp/B0BNMRHYG5?tag=edwardtfn-20) 🤝 | — | | ||
| | 🇧🇷 Brazil | — | — | [amazon.com.br](https://www.amazon.com.br/dp/B09MRZQCN2?tag=edwardtfn06-20) 🤝 | [amazon.com.br](https://www.amazon.com.br/dp/B0BNMRHYG5?tag=edwardtfn06-20) 🤝 | — | | ||
| <!-- markdownlint-enable MD013 MD033 --> | ||
|
|
||
| <!-- TODO: Confirm NSPanel US ASINs on amazon.se and add with ?tag=edwardtfn-21. --> | ||
| <!-- TODO: Confirm additional NSPanel ASINs on amazon.com.br (only US Dim Gray confirmed so far). --> | ||
| <!-- TODO: Add rows for NL, PL, IN and other regions as links and affiliate IDs are confirmed. --> | ||
|
|
||
| > [!NOTE] | ||
| > When buying from Amazon third-party sellers, verify the seller is reputable | ||
| > and confirm the listing explicitly states EU or US model before purchasing. | ||
|
|
||
| --- | ||
|
|
||
| *Know of another retailer, pre-flashed option, or service that should be listed here? | ||
| Open an [issue](https://github.com/edwardtfn/NSPanel-Easy/issues) or | ||
| [PR](https://github.com/edwardtfn/NSPanel-Easy/pulls) to get it added.* | ||
|
|
||
| --- | ||
|
|
||
| ## Supporting the Project | ||
|
|
||
| Using an affiliate link above is one way to support NSPanel Easy at no extra cost to you. | ||
| If you'd rather support directly, | ||
| consider [buying me an ice cream](https://www.buymeacoffee.com/edwardfirmo) 🍦 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.