Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions devices/common/cicd.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions devices/common/firmware_status.h
Original file line number Diff line number Diff line change
@@ -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 <cstdio>
#include <optional>
#include <string>

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<long> 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<unsigned>(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<long>(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<int> days_behind(const std::string &current,
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<int>(diff) : 0;
}

} // namespace willowbird