Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4fc5456
Set blueprint version and name on release
edwardtfn Apr 1, 2026
223559b
Update upstream requirements on release
edwardtfn Apr 1, 2026
a36de64
Supports `${version}` as sentinel to update blueprint version contract
edwardtfn Apr 1, 2026
25e51b6
New versioning: blueprint
edwardtfn Apr 1, 2026
e5f8309
New blueprint versioning
edwardtfn Apr 1, 2026
0430151
Merge branch 'main' into v9999.99.9
edwardtfn Apr 1, 2026
c49fa38
style: apply clang-format
edwardtfn Apr 1, 2026
56d496a
Review conditions for `Blueprint version mismatch`
edwardtfn Apr 1, 2026
9bafe14
Restore the echoed-version equality check before declaring the panel …
edwardtfn Apr 1, 2026
d78cddb
Validate the TFT version before narrowing it
edwardtfn Apr 1, 2026
1c832c9
Fix var name mismatch
edwardtfn Apr 1, 2026
1258d7a
use `calver_gte` for version comparisons (string - Blueprint)
edwardtfn Apr 1, 2026
38f77f2
Remove `display_sleep` flag
edwardtfn Apr 1, 2026
88c7a02
Fix `wait_for_api` monitoring flag
edwardtfn Apr 1, 2026
4ee6016
Clean-up code to fir pattern used on other pages
edwardtfn Apr 1, 2026
6e1c6f8
Never limits sending boot "start" event
edwardtfn Apr 1, 2026
d3a9755
Move `page_boot_show_blueprint_version` to sensor's `on_value`
edwardtfn Apr 1, 2026
3719e05
style: apply clang-format
edwardtfn Apr 1, 2026
ccac5e6
Fix number of reserved bits
edwardtfn Apr 1, 2026
5143a28
Add explicit NaN check to detect uninitialized TFT version.
edwardtfn Apr 1, 2026
5a8dd74
`version_esphome` was potentially published twice
edwardtfn Apr 1, 2026
714f8c3
Cast from NaN to unsigned int is undefined behavior
edwardtfn Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 92 additions & 7 deletions .github/workflows/versioning.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
#
# Triggered by merging a PR to the main branch, it automatically:
# 1. Bumps the CalVer version (YYYY.M.seq)
# 2. Updates the bug report template with current version placeholders
# 3. Commits the changes and pushes to main
# 4. Creates a version tag and GitHub Release
# 5. Updates the stable and latest floating tags
# 6. Announces the release on Discord
# 2. Calculates the minimum upstream version (current month minus 2, YYYY.M.0)
# 3. Resolves the "next" sentinel in min_blueprint_version, if set
# 4. Updates min_esphome_compiler_version and blueprint homeassistant.min_version
# 5. Updates the bug report template with current version placeholders
# 6. Updates the blueprint version and display name
# 7. Commits the changes and pushes to main
# 8. Creates a version tag and GitHub Release
# 9. Updates the stable and latest floating tags
# 10. Announces the release on Discord
#
# The workflow skips execution if the merge commit message contains
# [skip-versioning] to prevent loops from its own version bump commits.
Expand Down Expand Up @@ -144,13 +148,63 @@ jobs:
echo "version=${NEXT_VERSION}" >> "$GITHUB_OUTPUT"
echo "Bumping version: $CURRENT_VERSION -> $NEXT_VERSION"

- name: Calculate minimum upstream versions
id: min_upstream
if: steps.skip_check.outputs.skip == 'false'
run: |
# Minimum upstream version is always 2 months behind the current month,
# with patch fixed at 0 (e.g. April 2026 -> 2026.2.0, Jan 2026 -> 2025.11.0).
# This is the contract for the minimum ESPHome compiler and Home Assistant
# versions required to build and run NSPanel Easy.
CURRENT_YEAR=$(date +%Y)
CURRENT_MONTH=$(date +%-m) # No leading zero

MIN_MONTH=$((CURRENT_MONTH - 2))
MIN_YEAR=$CURRENT_YEAR
if [[ $MIN_MONTH -le 0 ]]; then
MIN_MONTH=$((MIN_MONTH + 12))
MIN_YEAR=$((MIN_YEAR - 1))
fi

MIN_UPSTREAM_VERSION="${MIN_YEAR}.${MIN_MONTH}.0"
echo "version=${MIN_UPSTREAM_VERSION}" >> "$GITHUB_OUTPUT"
echo "Minimum upstream version: ${MIN_UPSTREAM_VERSION}"

- name: Update version.yaml file
if: steps.skip_check.outputs.skip == 'false'
env:
NEW_VERSION: ${{ steps.next_version.outputs.version }}
run: |
yq eval '.version = strenv(NEW_VERSION)' -i ./versioning/version.yaml

- name: Resolve min_blueprint_version sentinel and update upstream versions
if: steps.skip_check.outputs.skip == 'false'
env:
NEW_VERSION: ${{ steps.next_version.outputs.version }}
MIN_UPSTREAM_VERSION: ${{ steps.min_upstream.outputs.version }}
run: |
ESPHOME_VERSION_FILE="esphome/nspanel_esphome_version.yaml"
MIN_BP=$(yq eval '.substitutions.min_blueprint_version' "$ESPHOME_VERSION_FILE")

# If min_blueprint_version is set to the sentinel "next", replace it
# with the actual version being released, tying the compatibility
# requirement to this exact release.
if [[ "$MIN_BP" == "next" || "$MIN_BP" == '${version}' ]]; then
yq eval \
'.substitutions.min_blueprint_version = strenv(NEW_VERSION)' \
-i "$ESPHOME_VERSION_FILE"
echo "Resolved min_blueprint_version sentinel to ${NEW_VERSION}"
else
echo "min_blueprint_version is already set to ${MIN_BP}, no sentinel to resolve"
fi

# Always update the minimum ESPHome compiler version to 2 months ago,
# keeping the upstream contract in sync with the release date.
yq eval \
'.substitutions.min_esphome_compiler_version = strenv(MIN_UPSTREAM_VERSION)' \
-i "$ESPHOME_VERSION_FILE"
echo "Updated min_esphome_compiler_version to ${MIN_UPSTREAM_VERSION}"

- name: Extract cross-component version information
id: versions
if: steps.skip_check.outputs.skip == 'false'
Expand Down Expand Up @@ -191,12 +245,42 @@ jobs:
'.body[4].attributes.placeholder = ("e.g., " + strenv(MIN_BLUEPRINT_VERSION))' \
-i "$TEMPLATE"

- name: Update blueprint version and name
if: steps.skip_check.outputs.skip == 'false'
env:
NEW_VERSION: ${{ steps.next_version.outputs.version }}
MIN_UPSTREAM_VERSION: ${{ steps.min_upstream.outputs.version }}
run: |
BLUEPRINT="nspanel_easy_blueprint.yaml"

# sed is used intentionally here instead of yq — the blueprint contains
# a large icon table with unicode escape sequences (\uXXXX) that yq would
# silently convert to their literal UTF-8 characters, corrupting the file.

# Update the blueprint display name shown in the HA Blueprints dashboard.
sed -i \
"s/^ name: NSPanel Easy Configuration.*/ name: NSPanel Easy Configuration (v${NEW_VERSION})/" \
"$BLUEPRINT"

# Update the blueprint's own version number, used by ESPHome to verify
# compatibility against min_blueprint_version at runtime.
sed -i \
"s/^ blueprint_version: .*/ blueprint_version: ${NEW_VERSION}/" \
"$BLUEPRINT"

# Update the minimum Home Assistant version required to run this blueprint,
# kept 2 months behind the release date to match the upstream contract.
sed -i \
"s/^ min_version: .*/ min_version: ${MIN_UPSTREAM_VERSION}/" \
"$BLUEPRINT"

- name: Restore YAML document end markers
if: steps.skip_check.outputs.skip == 'false'
run: |
# yq strips the YAML document end marker (...) on in-place edits.
# Re-append it to all files modified by yq.
for file in ./versioning/version.yaml .github/ISSUE_TEMPLATE/bug.yml; do
for file in ./versioning/version.yaml .github/ISSUE_TEMPLATE/bug.yml \
esphome/nspanel_esphome_version.yaml; do
if [ -f "$file" ] && ! tail -1 "$file" | grep -qx '\.\.\.'; then
echo '...' >> "$file"
fi
Expand All @@ -211,7 +295,8 @@ jobs:
env:
NEW_VERSION: ${{ steps.next_version.outputs.version }}
run: |
git add ./versioning/version.yaml .github/ISSUE_TEMPLATE/bug.yml
git add ./versioning/version.yaml .github/ISSUE_TEMPLATE/bug.yml \
esphome/nspanel_esphome_version.yaml nspanel_easy_blueprint.yaml
git commit -m "Bump version to ${NEW_VERSION} [skip-versioning]"

- name: Build tag message from PR
Expand Down
8 changes: 3 additions & 5 deletions components/nspanel_easy/base.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,12 @@ struct SystemFlags {
uint16_t version_check_ok : 1; ///< All component versions verified
uint16_t display_settings_received : 1; ///< All display settings received

// Runtime operation flags (bits 9-11)
// Runtime operation flags (bits 9-10)
uint16_t tft_upload_active : 1; ///< TFT firmware upload in progress
uint16_t ota_in_progress : 1; ///< Over-the-air update active
uint16_t display_sleep : 1; ///< Display is in sleep mode

// Reserved flags (bits 12-15)
uint16_t reserved : 4; ///< Reserved for future expansion
// Reserved flags (bits 11-15)
uint16_t reserved : 5; ///< Reserved for future expansion

// Default constructor - all flags start as false (zero-initialized)
SystemFlags()
Expand All @@ -52,7 +51,6 @@ struct SystemFlags {
display_settings_received(0),
tft_upload_active(0),
ota_in_progress(0),
display_sleep(0),
reserved(0) {}
};

Expand Down
24 changes: 22 additions & 2 deletions components/nspanel_easy/versioning.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,28 @@
namespace esphome {
namespace nspanel_easy {

uint8_t version_blueprint = 0;
uint8_t version_tft = 0;
bool calver_gte(const std::string &version, const std::string &min_version) {
int ver_y = 0, ver_m = 0, ver_s = 0;
int min_y = 0, min_m = 0, min_s = 0;

// Parse both version strings — fail safe if either is malformed or empty.
// An empty string produces 0 fields parsed, triggering the != 3 guard below.
// sscanf with exact field count check (!=3) is safe here; NOLINT
// suppresses cert-err34-c which flags sscanf for non-numeric input,
// but malformed input is handled by the field count check below.
if (sscanf(version.c_str(), "%d.%d.%d", &ver_y, &ver_m, &ver_s) != 3 || // NOLINT(cert-err34-c)
sscanf(min_version.c_str(), "%d.%d.%d", &min_y, &min_m, &min_s) != 3) { // NOLINT(cert-err34-c)
return false;
}

// Compare year first, then month, then sequence number.
// Returns false conservatively on any older segment.
if (ver_y != min_y)
return ver_y > min_y;
if (ver_m != min_m)
return ver_m > min_m;
return ver_s >= min_s;
}

} // namespace nspanel_easy
} // namespace esphome
Expand Down
36 changes: 33 additions & 3 deletions components/nspanel_easy/versioning.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,43 @@

#ifdef NSPANEL_EASY_VERSIONING

#include <cstdint>
#include <string>

namespace esphome {
namespace nspanel_easy {

extern uint8_t version_blueprint; // Blueprint version/revision
extern uint8_t version_tft; // TFT version/revision
/**
* @brief Compare two CalVer version strings segment by segment.
*
* Compares two version strings in the format `YYYY.M.seq` (e.g. "2026.10.2")
* using numeric comparison per segment. This avoids the lexicographic ordering
* pitfall where "2026.9.0" would incorrectly sort after "2026.10.0" as strings.
*
* The comparison evaluates as: version >= min_version
*
* Segment priority (left to right):
* 1. Year (YYYY) — most significant
* 2. Month (M) — no leading zero, 1–12
* 3. Seq (seq) — release sequence within the month
*
* @param version The version string to test (e.g. the installed blueprint version).
* @param min_version The minimum required version string to compare against.
* @return true if version is equal to or newer than min_version.
* @return false if version is older than min_version, or if either string is malformed.
*
* @note Returns false conservatively when either string cannot be parsed,
* to avoid incorrectly passing a version check on bad input.
* @note Both strings must follow the `YYYY.M.seq` format with no leading
* zeros on month or sequence (as produced by the CI/CD versioning workflow).
*
* @code
* calver_gte("2026.10.1", "2026.2.3") // true — month 10 > month 2
* calver_gte("2026.2.3", "2026.10.1") // false — month 2 < month 10
* calver_gte("2026.4.1", "2026.4.1") // true — equal
* calver_gte("", "2026.4.1") // false — malformed input
* @endcode
*/
bool calver_gte(const std::string &version, const std::string &min_version);

} // namespace nspanel_easy
} // namespace esphome
Expand Down
6 changes: 3 additions & 3 deletions esphome/nspanel_esphome_addon_upload_tft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ script:
// Automatic upload is enabled
#if NSPANEL_EASY_ADDON_UPLOAD_TFT_AUTOMATICALLY
// Return if sensor has value and versions match (no update needed)
if (version_tft == ${min_tft_version}) {
ESP_LOGV("${TAG_UPLOAD_TFT}", "Same version");
if (version_tft->state >= ${min_tft_version}) {
ESP_LOGV("${TAG_UPLOAD_TFT}", "Supported version");
return;
}

Expand All @@ -102,7 +102,7 @@ script:
return;
}

ESP_LOGI("${TAG_UPLOAD_TFT}", "Auto updating TFT from '%" PRIu8 "' to '${min_tft_version}'", version_tft);
ESP_LOGI("${TAG_UPLOAD_TFT}", "Auto updating TFT from '%.0f' to '${min_tft_version}'", version_tft->state);
upload_tft->execute("");
#else // Automatic upload is disabled
ESP_LOGV("${TAG_UPLOAD_TFT}", "Auto updating is disabled");
Expand Down
24 changes: 12 additions & 12 deletions esphome/nspanel_esphome_boot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ script:
- script.wait: boot_critical_components
- script.execute: boot_early_routines
- script.wait: boot_early_routines
- script.execute: boot_nextion
- script.wait: boot_nextion
- delay: 1s
# This should be moved somewhere else
- lambda: |-
Expand Down Expand Up @@ -77,14 +75,10 @@ script:

# Wait for a response from the blueprint
- delay: ${DELAY_DEFAULT}ms
- if:
condition:
- lambda: return not system_flags.blueprint_ready;
then:
- lambda: |-
boot_request_blueprint_settings->execute("start");
wait_for_blueprint->execute();
- script.wait: wait_for_blueprint
- lambda: |-
boot_request_blueprint_settings->execute("start");
wait_for_blueprint->execute();
- script.wait: wait_for_blueprint
- lambda: |-
if (not system_flags.blueprint_ready) {
boot_log->execute("Boot", "Blueprint not available");
Expand All @@ -97,7 +91,12 @@ script:
disp1->send_command_printf("brightness_dim=%i", int(display_dim_brightness->state));
disp1->send_command_printf("brightness_sleep=%i", int(display_sleep_brightness->state));
disp1->send_command_printf("wakeup_page_id=%" PRIu8, get_page_id(wakeup_page_name->current_option()));
feed_wdt_delay(${DELAY_DEFAULT});

- script.execute: boot_nextion
- script.wait: boot_nextion

- lambda: |-
// Wrap-up
feed_wdt_delay(${DELAY_DEFAULT});
boot_log->execute("Boot", "Wait to finish");
Expand Down Expand Up @@ -158,15 +157,16 @@ script:
ESP_LOGI("${TAG_BOOT}", "Starting boot preparation sequence");

- id: boot_request_blueprint_settings
mode: single
mode: restart
parameters:
step: string
then:
- lambda: |-
if (system_flags.blueprint_ready) return;
static uint32_t last_boot_event_fired = 0;
const uint32_t now = App.get_loop_component_start_time();
if ((uint32_t)(now - last_boot_event_fired) < ${BOOT_EVENT_REPEAT_INTERVAL_MS}) return;
if (step != "start" &&
(uint32_t)(now - last_boot_event_fired) < ${BOOT_EVENT_REPEAT_INTERVAL_MS}) return;
ESP_LOGD("${TAG_BOOT}", "Request blueprint settings");
fire_ha_event("boot", {{"step", step.c_str()}});
last_boot_event_fired = now;
Expand Down
2 changes: 1 addition & 1 deletion esphome/nspanel_esphome_core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ script: # Scripts
count: 60 # seconds
then:
- lambda: |-
if (system_flags.wifi_ready) wait_for_api->stop();
if (system_flags.api_ready) wait_for_api->stop();
if (iteration % 5 == 1) boot_log->execute("Boot", "Waiting for API");
- delay: 1s

Expand Down
8 changes: 4 additions & 4 deletions esphome/nspanel_esphome_hw_display.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ script:
ESP_LOGCONFIG("${TAG_HW_DISPLAY}", " Init: True");
} else
ESP_LOGW("${TAG_HW_DISPLAY}", " Init: False");
if (version_tft > 0)
ESP_LOGCONFIG("${TAG_HW_DISPLAY}", " TFT: %" PRIu8, version_tft);
if (version_tft->state > 0)
ESP_LOGCONFIG("${TAG_HW_DISPLAY}", " TFT: %" PRIu16, static_cast<uint16_t>(version_tft->state));
else
ESP_LOGW("${TAG_HW_DISPLAY}", " TFT: UNKNOWN");
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down Expand Up @@ -540,7 +540,7 @@ script:
count: 180 # seconds
then:
- lambda: |-
if (version_tft > 0) wait_for_version_tft->stop();
if (version_tft->state > 0) wait_for_version_tft->stop();
if (iteration % 5 == 0) boot_log->execute("Boot", "Waiting for TFT version");
- delay: 1s

Expand All @@ -557,7 +557,7 @@ script:
- lambda: |-
ESP_LOGV("${TAG_HW_DISPLAY}", "Wake-up (%s->%s)", page_names[current_page_id], page_names[wakeup_page_id]);
ESP_LOGV("${TAG_HW_DISPLAY}", " reset timer: %s", YESNO(reset_timer));
if (version_tft == 0) {
if (version_tft->state <= 0) {
ESP_LOGE("${TAG_HW_DISPLAY}", "TFT version invalid");
return;
}
Expand Down
Loading
Loading