Skip to content

Commit 6fc1043

Browse files
authored
Merge pull request #87 from edwardtfn/v9999.99.9
Improve: Smarter versioning makes updates easier and less disruptive
2 parents 8209a6d + 714f8c3 commit 6fc1043

12 files changed

+383
-127
lines changed

.github/workflows/versioning.yml

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
#
33
# Triggered by merging a PR to the main branch, it automatically:
44
# 1. Bumps the CalVer version (YYYY.M.seq)
5-
# 2. Updates the bug report template with current version placeholders
6-
# 3. Commits the changes and pushes to main
7-
# 4. Creates a version tag and GitHub Release
8-
# 5. Updates the stable and latest floating tags
9-
# 6. Announces the release on Discord
5+
# 2. Calculates the minimum upstream version (current month minus 2, YYYY.M.0)
6+
# 3. Resolves the "next" sentinel in min_blueprint_version, if set
7+
# 4. Updates min_esphome_compiler_version and blueprint homeassistant.min_version
8+
# 5. Updates the bug report template with current version placeholders
9+
# 6. Updates the blueprint version and display name
10+
# 7. Commits the changes and pushes to main
11+
# 8. Creates a version tag and GitHub Release
12+
# 9. Updates the stable and latest floating tags
13+
# 10. Announces the release on Discord
1014
#
1115
# The workflow skips execution if the merge commit message contains
1216
# [skip-versioning] to prevent loops from its own version bump commits.
@@ -144,13 +148,63 @@ jobs:
144148
echo "version=${NEXT_VERSION}" >> "$GITHUB_OUTPUT"
145149
echo "Bumping version: $CURRENT_VERSION -> $NEXT_VERSION"
146150
151+
- name: Calculate minimum upstream versions
152+
id: min_upstream
153+
if: steps.skip_check.outputs.skip == 'false'
154+
run: |
155+
# Minimum upstream version is always 2 months behind the current month,
156+
# with patch fixed at 0 (e.g. April 2026 -> 2026.2.0, Jan 2026 -> 2025.11.0).
157+
# This is the contract for the minimum ESPHome compiler and Home Assistant
158+
# versions required to build and run NSPanel Easy.
159+
CURRENT_YEAR=$(date +%Y)
160+
CURRENT_MONTH=$(date +%-m) # No leading zero
161+
162+
MIN_MONTH=$((CURRENT_MONTH - 2))
163+
MIN_YEAR=$CURRENT_YEAR
164+
if [[ $MIN_MONTH -le 0 ]]; then
165+
MIN_MONTH=$((MIN_MONTH + 12))
166+
MIN_YEAR=$((MIN_YEAR - 1))
167+
fi
168+
169+
MIN_UPSTREAM_VERSION="${MIN_YEAR}.${MIN_MONTH}.0"
170+
echo "version=${MIN_UPSTREAM_VERSION}" >> "$GITHUB_OUTPUT"
171+
echo "Minimum upstream version: ${MIN_UPSTREAM_VERSION}"
172+
147173
- name: Update version.yaml file
148174
if: steps.skip_check.outputs.skip == 'false'
149175
env:
150176
NEW_VERSION: ${{ steps.next_version.outputs.version }}
151177
run: |
152178
yq eval '.version = strenv(NEW_VERSION)' -i ./versioning/version.yaml
153179
180+
- name: Resolve min_blueprint_version sentinel and update upstream versions
181+
if: steps.skip_check.outputs.skip == 'false'
182+
env:
183+
NEW_VERSION: ${{ steps.next_version.outputs.version }}
184+
MIN_UPSTREAM_VERSION: ${{ steps.min_upstream.outputs.version }}
185+
run: |
186+
ESPHOME_VERSION_FILE="esphome/nspanel_esphome_version.yaml"
187+
MIN_BP=$(yq eval '.substitutions.min_blueprint_version' "$ESPHOME_VERSION_FILE")
188+
189+
# If min_blueprint_version is set to the sentinel "next", replace it
190+
# with the actual version being released, tying the compatibility
191+
# requirement to this exact release.
192+
if [[ "$MIN_BP" == "next" || "$MIN_BP" == '${version}' ]]; then
193+
yq eval \
194+
'.substitutions.min_blueprint_version = strenv(NEW_VERSION)' \
195+
-i "$ESPHOME_VERSION_FILE"
196+
echo "Resolved min_blueprint_version sentinel to ${NEW_VERSION}"
197+
else
198+
echo "min_blueprint_version is already set to ${MIN_BP}, no sentinel to resolve"
199+
fi
200+
201+
# Always update the minimum ESPHome compiler version to 2 months ago,
202+
# keeping the upstream contract in sync with the release date.
203+
yq eval \
204+
'.substitutions.min_esphome_compiler_version = strenv(MIN_UPSTREAM_VERSION)' \
205+
-i "$ESPHOME_VERSION_FILE"
206+
echo "Updated min_esphome_compiler_version to ${MIN_UPSTREAM_VERSION}"
207+
154208
- name: Extract cross-component version information
155209
id: versions
156210
if: steps.skip_check.outputs.skip == 'false'
@@ -191,12 +245,42 @@ jobs:
191245
'.body[4].attributes.placeholder = ("e.g., " + strenv(MIN_BLUEPRINT_VERSION))' \
192246
-i "$TEMPLATE"
193247
248+
- name: Update blueprint version and name
249+
if: steps.skip_check.outputs.skip == 'false'
250+
env:
251+
NEW_VERSION: ${{ steps.next_version.outputs.version }}
252+
MIN_UPSTREAM_VERSION: ${{ steps.min_upstream.outputs.version }}
253+
run: |
254+
BLUEPRINT="nspanel_easy_blueprint.yaml"
255+
256+
# sed is used intentionally here instead of yq — the blueprint contains
257+
# a large icon table with unicode escape sequences (\uXXXX) that yq would
258+
# silently convert to their literal UTF-8 characters, corrupting the file.
259+
260+
# Update the blueprint display name shown in the HA Blueprints dashboard.
261+
sed -i \
262+
"s/^ name: NSPanel Easy Configuration.*/ name: NSPanel Easy Configuration (v${NEW_VERSION})/" \
263+
"$BLUEPRINT"
264+
265+
# Update the blueprint's own version number, used by ESPHome to verify
266+
# compatibility against min_blueprint_version at runtime.
267+
sed -i \
268+
"s/^ blueprint_version: .*/ blueprint_version: ${NEW_VERSION}/" \
269+
"$BLUEPRINT"
270+
271+
# Update the minimum Home Assistant version required to run this blueprint,
272+
# kept 2 months behind the release date to match the upstream contract.
273+
sed -i \
274+
"s/^ min_version: .*/ min_version: ${MIN_UPSTREAM_VERSION}/" \
275+
"$BLUEPRINT"
276+
194277
- name: Restore YAML document end markers
195278
if: steps.skip_check.outputs.skip == 'false'
196279
run: |
197280
# yq strips the YAML document end marker (...) on in-place edits.
198281
# Re-append it to all files modified by yq.
199-
for file in ./versioning/version.yaml .github/ISSUE_TEMPLATE/bug.yml; do
282+
for file in ./versioning/version.yaml .github/ISSUE_TEMPLATE/bug.yml \
283+
esphome/nspanel_esphome_version.yaml; do
200284
if [ -f "$file" ] && ! tail -1 "$file" | grep -qx '\.\.\.'; then
201285
echo '...' >> "$file"
202286
fi
@@ -211,7 +295,8 @@ jobs:
211295
env:
212296
NEW_VERSION: ${{ steps.next_version.outputs.version }}
213297
run: |
214-
git add ./versioning/version.yaml .github/ISSUE_TEMPLATE/bug.yml
298+
git add ./versioning/version.yaml .github/ISSUE_TEMPLATE/bug.yml \
299+
esphome/nspanel_esphome_version.yaml nspanel_easy_blueprint.yaml
215300
git commit -m "Bump version to ${NEW_VERSION} [skip-versioning]"
216301
217302
- name: Build tag message from PR

components/nspanel_easy/base.h

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,12 @@ struct SystemFlags {
3131
uint16_t version_check_ok : 1; ///< All component versions verified
3232
uint16_t display_settings_received : 1; ///< All display settings received
3333

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

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

4241
// Default constructor - all flags start as false (zero-initialized)
4342
SystemFlags()
@@ -52,7 +51,6 @@ struct SystemFlags {
5251
display_settings_received(0),
5352
tft_upload_active(0),
5453
ota_in_progress(0),
55-
display_sleep(0),
5654
reserved(0) {}
5755
};
5856

components/nspanel_easy/versioning.cpp

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,28 @@
77
namespace esphome {
88
namespace nspanel_easy {
99

10-
uint8_t version_blueprint = 0;
11-
uint8_t version_tft = 0;
10+
bool calver_gte(const std::string &version, const std::string &min_version) {
11+
int ver_y = 0, ver_m = 0, ver_s = 0;
12+
int min_y = 0, min_m = 0, min_s = 0;
13+
14+
// Parse both version strings — fail safe if either is malformed or empty.
15+
// An empty string produces 0 fields parsed, triggering the != 3 guard below.
16+
// sscanf with exact field count check (!=3) is safe here; NOLINT
17+
// suppresses cert-err34-c which flags sscanf for non-numeric input,
18+
// but malformed input is handled by the field count check below.
19+
if (sscanf(version.c_str(), "%d.%d.%d", &ver_y, &ver_m, &ver_s) != 3 || // NOLINT(cert-err34-c)
20+
sscanf(min_version.c_str(), "%d.%d.%d", &min_y, &min_m, &min_s) != 3) { // NOLINT(cert-err34-c)
21+
return false;
22+
}
23+
24+
// Compare year first, then month, then sequence number.
25+
// Returns false conservatively on any older segment.
26+
if (ver_y != min_y)
27+
return ver_y > min_y;
28+
if (ver_m != min_m)
29+
return ver_m > min_m;
30+
return ver_s >= min_s;
31+
}
1232

1333
} // namespace nspanel_easy
1434
} // namespace esphome

components/nspanel_easy/versioning.h

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,43 @@
44

55
#ifdef NSPANEL_EASY_VERSIONING
66

7-
#include <cstdint>
7+
#include <string>
88

99
namespace esphome {
1010
namespace nspanel_easy {
1111

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

1545
} // namespace nspanel_easy
1646
} // namespace esphome

esphome/nspanel_esphome_addon_upload_tft.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ script:
9191
// Automatic upload is enabled
9292
#if NSPANEL_EASY_ADDON_UPLOAD_TFT_AUTOMATICALLY
9393
// Return if sensor has value and versions match (no update needed)
94-
if (version_tft == ${min_tft_version}) {
95-
ESP_LOGV("${TAG_UPLOAD_TFT}", "Same version");
94+
if (version_tft->state >= ${min_tft_version}) {
95+
ESP_LOGV("${TAG_UPLOAD_TFT}", "Supported version");
9696
return;
9797
}
9898
@@ -102,7 +102,7 @@ script:
102102
return;
103103
}
104104
105-
ESP_LOGI("${TAG_UPLOAD_TFT}", "Auto updating TFT from '%" PRIu8 "' to '${min_tft_version}'", version_tft);
105+
ESP_LOGI("${TAG_UPLOAD_TFT}", "Auto updating TFT from '%.0f' to '${min_tft_version}'", version_tft->state);
106106
upload_tft->execute("");
107107
#else // Automatic upload is disabled
108108
ESP_LOGV("${TAG_UPLOAD_TFT}", "Auto updating is disabled");

esphome/nspanel_esphome_boot.yaml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ script:
3939
- script.wait: boot_critical_components
4040
- script.execute: boot_early_routines
4141
- script.wait: boot_early_routines
42-
- script.execute: boot_nextion
43-
- script.wait: boot_nextion
4442
- delay: 1s
4543
# This should be moved somewhere else
4644
- lambda: |-
@@ -77,14 +75,10 @@ script:
7775
7876
# Wait for a response from the blueprint
7977
- delay: ${DELAY_DEFAULT}ms
80-
- if:
81-
condition:
82-
- lambda: return not system_flags.blueprint_ready;
83-
then:
84-
- lambda: |-
85-
boot_request_blueprint_settings->execute("start");
86-
wait_for_blueprint->execute();
87-
- script.wait: wait_for_blueprint
78+
- lambda: |-
79+
boot_request_blueprint_settings->execute("start");
80+
wait_for_blueprint->execute();
81+
- script.wait: wait_for_blueprint
8882
- lambda: |-
8983
if (not system_flags.blueprint_ready) {
9084
boot_log->execute("Boot", "Blueprint not available");
@@ -97,7 +91,12 @@ script:
9791
disp1->send_command_printf("brightness_dim=%i", int(display_dim_brightness->state));
9892
disp1->send_command_printf("brightness_sleep=%i", int(display_sleep_brightness->state));
9993
disp1->send_command_printf("wakeup_page_id=%" PRIu8, get_page_id(wakeup_page_name->current_option()));
94+
feed_wdt_delay(${DELAY_DEFAULT});
95+
96+
- script.execute: boot_nextion
97+
- script.wait: boot_nextion
10098

99+
- lambda: |-
101100
// Wrap-up
102101
feed_wdt_delay(${DELAY_DEFAULT});
103102
boot_log->execute("Boot", "Wait to finish");
@@ -158,15 +157,16 @@ script:
158157
ESP_LOGI("${TAG_BOOT}", "Starting boot preparation sequence");
159158
160159
- id: boot_request_blueprint_settings
161-
mode: single
160+
mode: restart
162161
parameters:
163162
step: string
164163
then:
165164
- lambda: |-
166165
if (system_flags.blueprint_ready) return;
167166
static uint32_t last_boot_event_fired = 0;
168167
const uint32_t now = App.get_loop_component_start_time();
169-
if ((uint32_t)(now - last_boot_event_fired) < ${BOOT_EVENT_REPEAT_INTERVAL_MS}) return;
168+
if (step != "start" &&
169+
(uint32_t)(now - last_boot_event_fired) < ${BOOT_EVENT_REPEAT_INTERVAL_MS}) return;
170170
ESP_LOGD("${TAG_BOOT}", "Request blueprint settings");
171171
fire_ha_event("boot", {{"step", step.c_str()}});
172172
last_boot_event_fired = now;

esphome/nspanel_esphome_core.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ script: # Scripts
166166
count: 60 # seconds
167167
then:
168168
- lambda: |-
169-
if (system_flags.wifi_ready) wait_for_api->stop();
169+
if (system_flags.api_ready) wait_for_api->stop();
170170
if (iteration % 5 == 1) boot_log->execute("Boot", "Waiting for API");
171171
- delay: 1s
172172

esphome/nspanel_esphome_hw_display.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,8 @@ script:
207207
ESP_LOGCONFIG("${TAG_HW_DISPLAY}", " Init: True");
208208
} else
209209
ESP_LOGW("${TAG_HW_DISPLAY}", " Init: False");
210-
if (version_tft > 0)
211-
ESP_LOGCONFIG("${TAG_HW_DISPLAY}", " TFT: %" PRIu8, version_tft);
210+
if (version_tft->state > 0)
211+
ESP_LOGCONFIG("${TAG_HW_DISPLAY}", " TFT: %" PRIu16, static_cast<uint16_t>(version_tft->state));
212212
else
213213
ESP_LOGW("${TAG_HW_DISPLAY}", " TFT: UNKNOWN");
214214
@@ -540,7 +540,7 @@ script:
540540
count: 180 # seconds
541541
then:
542542
- lambda: |-
543-
if (version_tft > 0) wait_for_version_tft->stop();
543+
if (version_tft->state > 0) wait_for_version_tft->stop();
544544
if (iteration % 5 == 0) boot_log->execute("Boot", "Waiting for TFT version");
545545
- delay: 1s
546546

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

0 commit comments

Comments
 (0)