From df6a1af20aacca9d45e9a815c4ab4a752018b1e9 Mon Sep 17 00:00:00 2001 From: Jared Bunting Date: Mon, 6 Jul 2026 23:48:56 -0400 Subject: [PATCH] Add "Firmware Days Behind" sensor (closes #8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the last piece of #8 — "how far out of date" — as a numeric sensor, and pilots the pattern from #7 for externalizing device logic that's too complex for a clean inline lambda. - devices/common/firmware_status.h: a real C++ header with the CalVer date math (parse YYYY.MM.DD -> civil days, diff current vs latest). Unknown for dev builds or before the first update check; never negative. - cicd.yaml: pull it via `esphome: includes:` and add a "Firmware Days Behind" template sensor whose lambda is a one-line call into the helper. Leaves the existing simple lambdas and the vendored TubesZB logic untouched. Verified with a local `esphome compile` (2026.6.4). Closes #8. Co-Authored-By: Claude Opus 4.8 (1M context) --- devices/common/cicd.yaml | 24 +++++++++++++++ devices/common/firmware_status.h | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 devices/common/firmware_status.h diff --git a/devices/common/cicd.yaml b/devices/common/cicd.yaml index bd51c05..606e4c9 100644 --- a/devices/common/cicd.yaml +++ b/devices/common/cicd.yaml @@ -1,5 +1,11 @@ substitutions: friendly_name: ${name} +esphome: + # Externalized firmware-status helpers (date math for "days behind"), kept in + # a real C++ header rather than an inline lambda. Path is resolved relative to + # the device config dir (devices/). + includes: + - common/firmware_status.h ota: - platform: esphome - platform: http_request @@ -63,6 +69,24 @@ text_sensor: return {}; return {latest}; +sensor: + - platform: template + name: Firmware Days Behind + id: firmware_days_behind + icon: mdi:calendar-clock + unit_of_measurement: d + accuracy_decimals: 0 + entity_category: diagnostic + # How far out of date this device is. Date math lives in the C++ helper + # (common/firmware_status.h); unknown for dev builds or before the first + # update check. + lambda: |- + auto behind = willowbird::days_behind( + "${version}", id(firmware_update).update_info.latest_version); + if (behind.has_value()) + return (float) behind.value(); + return {}; + binary_sensor: - platform: template name: Firmware Up To Date diff --git a/devices/common/firmware_status.h b/devices/common/firmware_status.h new file mode 100644 index 0000000..2b58e36 --- /dev/null +++ b/devices/common/firmware_status.h @@ -0,0 +1,51 @@ +#pragma once +// +// Externalized on-device logic for willowbird firmware status (issue #7/#8). +// +// When device logic gets more involved than a one-liner, put it here (a real +// C++ header with proper tooling) instead of an inline YAML lambda, and call it +// from a thin lambda. Pulled into a build with: +// +// esphome: +// includes: +// - common/firmware_status.h +// +#include +#include +#include + +namespace willowbird { + +// Convert the "YYYY.MM.DD" date prefix of a CalVer version into a count of days +// since the civil epoch (1970-01-01), using Howard Hinnant's days_from_civil. +// Returns nullopt when the string isn't a YYYY.MM.DD date (e.g. a "dev" build). +inline std::optional calver_to_days(const std::string &version) { + int y = 0, m = 0, d = 0; + if (sscanf(version.c_str(), "%d.%d.%d", &y, &m, &d) != 3) + return std::nullopt; + if (m < 1 || m > 12 || d < 1 || d > 31) + return std::nullopt; + + y -= m <= 2; + const long era = (y >= 0 ? y : y - 399) / 400; + const unsigned yoe = static_cast(y - era * 400); + const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097L + static_cast(doe) - 719468L; +} + +// How many calendar days `current` is behind `latest` (both CalVer versions). +// Returns nullopt when either version isn't a date (dev build, or no update +// check has completed yet). Never negative — a device at or ahead of the latest +// release reports 0. +inline std::optional days_behind(const std::string ¤t, + const std::string &latest) { + const auto c = calver_to_days(current); + const auto l = calver_to_days(latest); + if (!c.has_value() || !l.has_value()) + return std::nullopt; + const long diff = *l - *c; + return diff > 0 ? static_cast(diff) : 0; +} + +} // namespace willowbird