proddy
2026-04-26 16:10:30 +02:00
parent 74062bab57
commit 3a11327e7e
9 changed files with 290 additions and 168 deletions

View File

@@ -57,6 +57,3 @@ export const alovaInstance = createAlova({
onSuccess: handleResponse onSuccess: handleResponse
} }
}); });
export const DOCS_BASE_URL =
process.env.NODE_ENV === 'development' ? '/emsesp.org' : 'https://emsesp.org';

View File

@@ -1,6 +1,6 @@
import type { LogSettings, SystemStatus } from 'types'; import type { LogSettings, SystemStatus } from 'types';
import { DOCS_BASE_URL, alovaInstance } from './endpoints'; import { alovaInstance } from './endpoints';
// systemStatus - also used to ping in System Monitor for pinging // systemStatus - also used to ping in System Monitor for pinging
export const readSystemStatus = () => export const readSystemStatus = () =>
@@ -13,14 +13,6 @@ export const updateLogSettings = (data: LogSettings) =>
alovaInstance.Post('/rest/logSettings', data); alovaInstance.Post('/rest/logSettings', data);
export const fetchLogES = () => alovaInstance.Get('/es/log'); export const fetchLogES = () => alovaInstance.Get('/es/log');
// get versions from emsesp.org/versions.json
// uses native fetch (no custom headers) to keep this as a "simple" CORS
export const getVersions = async <T = unknown>(): Promise<T> => {
const res = await fetch(`${DOCS_BASE_URL}/versions.json`);
if (!res.ok) throw new Error(res.statusText);
return res.json() as Promise<T>;
};
const UPLOAD_TIMEOUT = 60000; // 1 minute const UPLOAD_TIMEOUT = 60000; // 1 minute
export const uploadFile = (file: File) => { export const uploadFile = (file: File) => {

View File

@@ -1,12 +1,4 @@
import { import { memo, useCallback, useContext, useMemo, useState } from 'react';
memo,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -34,7 +26,6 @@ import {
import * as SystemApi from 'api/system'; import * as SystemApi from 'api/system';
import { API, callAction } from 'api/app'; import { API, callAction } from 'api/app';
import { getVersions } from 'api/system';
import { dialogStyle } from 'CustomTheme'; import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client'; import { useRequest } from 'alova/client';
@@ -79,21 +70,24 @@ interface VersionData {
developer_mode: boolean; developer_mode: boolean;
} }
interface UpgradeCheckData {
emsesp_version: string;
dev_upgradeable: boolean;
stable_upgradeable: boolean;
}
interface VersionInfo { interface VersionInfo {
version: string; version: string;
date: string; date: string;
} }
interface Versions { interface RemoteVersionInfo extends VersionInfo {
stable: VersionInfo; upgradeable: boolean;
dev: VersionInfo; }
last_updated: string;
interface CurrentVersionInfo extends VersionInfo {
type: 'stable' | 'dev';
}
// Response payload from the `getVersions` action
interface VersionsResponse {
current: CurrentVersionInfo;
stable?: RemoteVersionInfo;
dev?: RemoteVersionInfo;
} }
// Memoized components for better performance // Memoized components for better performance
@@ -465,15 +459,6 @@ const Version = () => {
const [showVersionInfo, setShowVersionInfo] = useState<number>(0); // 1 = stable, 2 = dev, 3 = partition const [showVersionInfo, setShowVersionInfo] = useState<number>(0); // 1 = stable, 2 = dev, 3 = partition
const [firmwareSize, setFirmwareSize] = useState<number>(0); const [firmwareSize, setFirmwareSize] = useState<number>(0);
const { send: sendCheckUpgrade } = useRequest(
(versions: string) => callAction({ action: 'checkUpgrade', param: versions }),
{ immediate: false }
).onSuccess((event) => {
const data = event.data as UpgradeCheckData;
setDevUpgradeAvailable(data.dev_upgradeable);
setStableUpgradeAvailable(data.stable_upgradeable);
});
const { send: sendSetPartition } = useRequest( const { send: sendSetPartition } = useRequest(
(partition: string) => callAction({ action: 'setPartition', param: partition }), (partition: string) => callAction({ action: 'setPartition', param: partition }),
{ immediate: false } { immediate: false }
@@ -490,7 +475,6 @@ const Version = () => {
if (systemData.arduino_version.startsWith('Tasmota')) { if (systemData.arduino_version.startsWith('Tasmota')) {
setDownloadOnly(true); setDownloadOnly(true);
} }
setUsingDevVersion(systemData.emsesp_version.includes('dev'));
}); });
const { send: sendUploadURL } = useRequest( const { send: sendUploadURL } = useRequest(
@@ -498,32 +482,33 @@ const Version = () => {
{ immediate: false } { immediate: false }
); );
// Fetch versions.json from emsesp.org once on mount. // Fetch latest stable/dev versions via the device. The ESP32 calls
// Uses plain fetch (not alova) so the request stays a "simple" CORS request and avoids a preflight that emsesp.org rejects. // emsesp.org/versions.json itself and includes its own `current` info plus
// sendCheckUpgrade is stored in a ref because alova's useRequest returns a new function reference each render // upgradeable flags. If the device has no internet, `stable`/`dev` are
const sendCheckUpgradeRef = useRef(sendCheckUpgrade); // absent and we surface that as "internet not live".
sendCheckUpgradeRef.current = sendCheckUpgrade; useRequest(() => callAction({ action: 'getVersions' }))
useEffect(() => { .onSuccess((event) => {
let cancelled = false; const versions = event.data as VersionsResponse;
getVersions<Versions>() setUsingDevVersion(versions.current?.type === 'dev');
.then((versions) => { if (versions.stable) {
if (cancelled) return; setLatestVersion({
setLatestVersion(versions.stable); version: versions.stable.version,
setLatestDevVersion(versions.dev); date: versions.stable.date
sendCheckUpgradeRef.current( });
`${versions.stable.version},${versions.dev.version}` setStableUpgradeAvailable(versions.stable.upgradeable);
); }
setInternetLive(true); if (versions.dev) {
setLatestDevVersion({
version: versions.dev.version,
date: versions.dev.date
});
setDevUpgradeAvailable(versions.dev.upgradeable);
}
setInternetLive(Boolean(versions.stable || versions.dev));
}) })
.catch((error: unknown) => { .onError(() => {
if (cancelled) return;
toast.error(error instanceof Error ? error.message : 'An error occurred');
setInternetLive(false); setInternetLive(false);
}); });
return () => {
cancelled = true;
};
}, []);
const { send: sendAPI } = useRequest((data: APIcall) => API(data), { const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false immediate: false

View File

@@ -229,8 +229,7 @@ export default defineConfig(
changeOrigin: true, changeOrigin: true,
secure: false secure: false
}, },
'/rest': 'http://localhost:3080', '/rest': 'http://localhost:3080'
'/emsesp.org': 'http://localhost:3080'
} }
}, },
build: { build: {

View File

@@ -6,7 +6,6 @@ const router = AutoRouter();
const REST_ENDPOINT_ROOT = '/rest/'; const REST_ENDPOINT_ROOT = '/rest/';
const API_ENDPOINT_ROOT = '/api/'; const API_ENDPOINT_ROOT = '/api/';
const EMSESP_DOCS_ENDPOINT = '/emsesp.org/'; // for mock emsesp.org/versions.json
// HTTP HEADERS for msgpack // HTTP HEADERS for msgpack
const headers = { const headers = {
@@ -415,36 +414,54 @@ function upgradeImportantMessages(version: string) {
return { upgradeImportantMessageType: upgradeImportantMessageType_n }; return { upgradeImportantMessageType: upgradeImportantMessageType_n };
} }
// called by Action endpoint checkUpgrade // called by Action endpoint getVersions
function check_upgrade(version: string) { // Mirrors the C++ WebStatusService::getVersions() payload:
let data = {}; // { current: { version, type, date },
if (version) { // stable?: { version, date, upgradeable },
const dev_version = version.split(',')[0]; // dev?: { version, date, upgradeable } }
const stable_version = version.split(',')[1]; // Set MOCK_OFFLINE = true to simulate a device with no internet (omits stable/dev).
const MOCK_OFFLINE = false;
console.log( function get_versions() {
'Upgrade this version (' + const isDev = THIS_VERSION.includes('dev');
THIS_VERSION + const data: {
') to dev (' + current: { version: string; type: 'stable' | 'dev'; date: string };
dev_version + stable?: { version: string; date: string; upgradeable: boolean };
') is ' + dev?: { version: string; date: string; upgradeable: boolean };
(DEV_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') + } = {
' and to stable (' + current: {
stable_version + version: THIS_VERSION,
') is ' + type: isDev ? 'dev' : 'stable',
(STABLE_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') date: '2026-04-25T12:00:00'
); }
data = {
emsesp_version: THIS_VERSION,
dev_upgradeable: DEV_VERSION_IS_UPGRADEABLE,
stable_upgradeable: STABLE_VERSION_IS_UPGRADEABLE
}; };
} else {
console.log('requesting ems-esp version (' + THIS_VERSION + ')'); if (!MOCK_OFFLINE) {
data = { data.stable = {
emsesp_version: THIS_VERSION version: LATEST_STABLE_VERSION,
date: '2026-04-25',
upgradeable: STABLE_VERSION_IS_UPGRADEABLE
};
data.dev = {
version: LATEST_DEV_VERSION,
date: '2026-04-25',
upgradeable: DEV_VERSION_IS_UPGRADEABLE
}; };
} }
console.log(
'getVersions: current=' +
THIS_VERSION +
' stable=' +
LATEST_STABLE_VERSION +
' (upgradeable=' +
(STABLE_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') +
') dev=' +
LATEST_DEV_VERSION +
' (upgradeable=' +
(DEV_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') +
')' +
(MOCK_OFFLINE ? ' [offline]' : '')
);
return data; return data;
} }
@@ -706,7 +723,6 @@ const EMSESP_ACTION_ENDPOINT = REST_ENDPOINT_ROOT + 'action';
// these are used in the API calls only // these are used in the API calls only
const EMSESP_SYSTEM_INFO_ENDPOINT = API_ENDPOINT_ROOT + 'system/info'; const EMSESP_SYSTEM_INFO_ENDPOINT = API_ENDPOINT_ROOT + 'system/info';
const EMSESP_VERSIONS_ENDPOINT = EMSESP_DOCS_ENDPOINT + 'versions.json';
const emsesp_info = { const emsesp_info = {
System: { System: {
@@ -5173,13 +5189,9 @@ router
} else if (action === 'getCustomSupport') { } else if (action === 'getCustomSupport') {
// send custom support // send custom support
return custom_support(); return custom_support();
} else if (action === 'checkUpgrade') { } else if (action === 'getVersions') {
// check upgrade // get versions
// check if content has a param return get_versions();
if (!content.param) {
return check_upgrade('');
}
return check_upgrade(content.param);
} else if (action === 'uploadURL') { } else if (action === 'uploadURL') {
// upload URL // upload URL
console.log('upload File from URL', content.param); console.log('upload File from URL', content.param);
@@ -5234,23 +5246,6 @@ router
return status(404); // not found return status(404); // not found
}); });
// Mock emsesp.org/versions.json
router.get(EMSESP_VERSIONS_ENDPOINT, () => {
const data = {
stable: {
version: LATEST_STABLE_VERSION,
date: '2026-04-25'
},
dev: {
version: LATEST_DEV_VERSION,
date: '2026-04-25'
},
last_updated: new Date().toISOString()
};
console.log('sending versions.json: ', data);
return data;
});
// const logger: ResponseHandler = (response, request) => { // const logger: ResponseHandler = (response, request) => {
// console.log( // console.log(
// response.status, // response.status,

View File

@@ -1865,6 +1865,7 @@ void EMSESP::loop() {
} }
// loop through the services // loop through the services
webStatusService.loop(); // periodic refresh of cached versions.json
rxservice_.loop(); // process any incoming Rx telegrams rxservice_.loop(); // process any incoming Rx telegrams
shower_.loop(); // check for shower on/off shower_.loop(); // check for shower on/off
temperaturesensor_.loop(); // read sensor temperatures temperaturesensor_.loop(); // read sensor temperatures

View File

@@ -899,9 +899,7 @@ void System::heartbeat_json(JsonObject output) {
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
output["temperature"] = (int)temperature_; output["temperature"] = (int)temperature_;
#endif #endif
#endif
#ifndef EMSESP_STANDALONE
if (!ethernet_connected_) { if (!ethernet_connected_) {
int8_t rssi = WiFi.RSSI(); int8_t rssi = WiFi.RSSI();
output["rssi"] = rssi; output["rssi"] = rssi;
@@ -909,6 +907,11 @@ void System::heartbeat_json(JsonObject output) {
output["wifireconnects"] = EMSESP::esp32React.getWifiReconnects(); output["wifireconnects"] = EMSESP::esp32React.getWifiReconnects();
} }
#endif #endif
// see if there is a newer version available
if (EMSESP::webStatusService.versions_cache_valid()) {
output["upgradeable"] = EMSESP::webStatusService.current_upgradeable();
}
} }
// send periodic MQTT message with system information // send periodic MQTT message with system information

View File

@@ -20,6 +20,7 @@
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
#include <esp_ota_ops.h> #include <esp_ota_ops.h>
#include <HTTPClient.h>
#endif #endif
namespace emsesp { namespace emsesp {
@@ -205,11 +206,11 @@ void WebStatusService::action(AsyncWebServerRequest * request, JsonVariant json)
bool is_admin = AuthenticationPredicates::IS_ADMIN(authentication); bool is_admin = AuthenticationPredicates::IS_ADMIN(authentication);
// call action command // call action command
bool ok = false; bool ok = true;
std::string action = json["action"]; std::string action = json["action"];
if (action == "checkUpgrade") { if (action == "getVersions") {
ok = checkUpgrade(root, param); // param could be empty, if so only send back version getVersions(root);
} else if (action == "setPartition") { } else if (action == "setPartition") {
ok = EMSESP::system_.set_partition(param.c_str()); ok = EMSESP::system_.set_partition(param.c_str());
} else if (action == "export") { } else if (action == "export") {
@@ -224,10 +225,8 @@ void WebStatusService::action(AsyncWebServerRequest * request, JsonVariant json)
ok = setSystemStatus(param.c_str()); ok = setSystemStatus(param.c_str());
} else if (action == "resetMQTT" && is_admin) { } else if (action == "resetMQTT" && is_admin) {
EMSESP::mqtt_.reset_mqtt(); EMSESP::mqtt_.reset_mqtt();
ok = true;
} else if (action == "upgradeImportantMessages") { } else if (action == "upgradeImportantMessages") {
root["upgradeImportantMessageType"] = upgradeImportantMessages(param); root["upgradeImportantMessageType"] = upgradeImportantMessages(param);
ok = true;
} }
#if defined(EMSESP_STANDALONE) && !defined(EMSESP_UNITY) #if defined(EMSESP_STANDALONE) && !defined(EMSESP_UNITY)
@@ -311,46 +310,169 @@ uint8_t WebStatusService::upgradeImportantMessages(std::string & version) {
return 0; // if it's not a valid version upgrade return 0 return 0; // if it's not a valid version upgrade return 0
} }
// action = checkUpgrade // action = getVersions
// versions holds the latest development version and stable version in one string, comma separated // returns the device's current version info plus the cached "stable" and "dev"
bool WebStatusService::checkUpgrade(JsonObject root, std::string & version) { // entries from emsesp.org/versions.json. The remote fetch is NOT done here: it
if (!version.empty()) { // runs from the main loop task via WebStatusService::loop() so we never block
// the AsyncTCP callback (which has a tiny ~6 KB stack — far too small for an
// HTTPS handshake). If we have no cached data yet (no internet, fetch still
// pending, parse error) the "stable" and "dev" sections are simply omitted so
// the client can detect the offline case.
void WebStatusService::getVersions(JsonObject root) {
version::EMSESP_Version current_version(current_version_s); version::EMSESP_Version current_version(current_version_s);
version::EMSESP_Version latest_dev_version(version.substr(0, version.find(','))); bool is_dev = current_version.prerelease().find("dev") != std::string::npos;
version::EMSESP_Version latest_stable_version(version.substr(version.find(',') + 1));
bool dev_upgradeable = latest_dev_version > current_version; JsonObject current = root["current"].to<JsonObject>();
bool stable_upgradeable = latest_stable_version > current_version; current["version"] = current_version_s;
current["type"] = is_dev ? "dev" : "stable";
current["date"] = "";
current["upgradeable"] = current_upgradeable(); // false if cache not valid yet
#if defined(EMSESP_DEBUG) #ifndef EMSESP_STANDALONE
// look for dev in the name to determine if we're using a dev release // pull the install_date for the running partition (if known)
bool using_dev_version = !current_version.prerelease().find("dev"); const esp_partition_t * running = esp_ota_get_running_partition();
EMSESP::logger() if (running != nullptr) {
.debug("Checking version upgrade. This version=%d.%d.%d-%s (%s),latest dev=%d.%d.%d-%s (%s upgradeable),latest stable=%d.%d.%d-%s (%s upgradeable)", const auto & info = EMSESP::system_.partition_info_;
current_version.major(), auto it = info.find(running->label);
current_version.minor(), if (it != info.end() && it->second.install_date > 0) {
current_version.patch(), char time_string[25];
current_version.prerelease().c_str(), time_t d = it->second.install_date;
using_dev_version ? "Dev" : "Stable", strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d));
latest_dev_version.major(), current["date"] = time_string;
latest_dev_version.minor(), }
latest_dev_version.patch(),
latest_dev_version.prerelease().c_str(),
dev_upgradeable ? "is" : "is not",
latest_stable_version.major(),
latest_stable_version.minor(),
latest_stable_version.patch(),
latest_stable_version.prerelease().c_str(),
stable_upgradeable ? "is" : "is not");
#endif
root["dev_upgradeable"] = dev_upgradeable;
root["stable_upgradeable"] = stable_upgradeable;
} }
root["emsesp_version"] = current_version_s; // always send back current version if (!versions_cache_valid_) {
// no successful fetch yet (no network, fetch pending, or parse error)
return;
}
// copies a cached entry into root[key]. The upgradeable bool was computed
// once during refresh_versions_cache() so we just read it here.
auto add_section = [&](const char * key, const VersionInfo & info) {
if (info.version.empty()) {
return;
}
JsonObject out = root[key].to<JsonObject>();
out["version"] = info.version;
out["date"] = info.date;
out["upgradeable"] = info.upgradeable;
};
add_section("stable", versions_stable_);
add_section("dev", versions_dev_);
#else
// standalone/test build: provide deterministic dummy data
JsonObject stable_out = root["stable"].to<JsonObject>();
stable_out["version"] = "3.8.2";
stable_out["date"] = "2026-04-25";
stable_out["upgradeable"] = version::EMSESP_Version("3.8.2") > current_version;
JsonObject dev_out = root["dev"].to<JsonObject>();
dev_out["version"] = "3.8.3-dev.2";
dev_out["date"] = "2026-04-25";
dev_out["upgradeable"] = version::EMSESP_Version("3.8.3-dev.2") > current_version;
#endif
}
// periodic refresh (1 hour) of the cached versions.json. Runs on the main loop task,
// which has a much bigger stack than AsyncTCP, so it's safe to do HTTPS here.
void WebStatusService::loop() {
#ifndef EMSESP_STANDALONE
// need a network
if (!EMSESP::system_.ethernet_connected() && (WiFi.status() != WL_CONNECTED)) {
return;
}
uint32_t now = uuid::get_uptime();
// first call after we have a network: schedule the initial fetch a little
// later so we give NTP / DNS a chance to settle
if (versions_next_fetch_ms_ == 0) {
versions_next_fetch_ms_ = now + VERSIONS_INITIAL_DELAY_MS;
if (versions_next_fetch_ms_ == 0) {
versions_next_fetch_ms_ = 1; // avoid the "never scheduled" sentinel
}
return;
}
// not time yet (signed difference handles uint32 wrap)
if ((int32_t)(now - versions_next_fetch_ms_) < 0) {
return;
}
bool ok = refresh_versions_cache();
uint32_t next = uuid::get_uptime() + (ok ? VERSIONS_REFRESH_INTERVAL_MS : VERSIONS_RETRY_INTERVAL_MS);
if (next == 0) {
next = 1;
}
versions_next_fetch_ms_ = next;
#endif
}
// runs on the main loop task — never call this from an AsyncWebServer handler
bool WebStatusService::refresh_versions_cache() {
#ifdef EMSESP_STANDALONE
return false;
#else
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setTimeout(5000);
http.useHTTP10(true);
if (!http.begin("https://emsesp.org/versions.json")) {
EMSESP::logger().debug("versions.json: failed to start HTTPS request");
return false;
}
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
EMSESP::logger().debug("versions.json: HTTP %d", httpCode);
http.end();
return false;
}
JsonDocument doc;
DeserializationError err = deserializeJson(doc, http.getStream());
http.end();
if (err) {
EMSESP::logger().debug("versions.json: parse error (%s)", err.c_str());
return false;
}
version::EMSESP_Version current_version(current_version_s);
auto read_section = [&doc, &current_version](const char * key, VersionInfo & out) {
JsonObjectConst section = doc[key];
if (section.isNull()) {
out = {};
return;
}
out.version = section["version"] | "";
out.date = section["date"] | "";
out.upgradeable = !out.version.empty() && version::EMSESP_Version(out.version) > current_version;
};
read_section("stable", versions_stable_);
read_section("dev", versions_dev_);
versions_cache_valid_ = true;
#if defined(EMSESP_DEBUG)
EMSESP::logger().debug("versions.json refreshed (stable=%s dev=%s)", versions_stable_.version.c_str(), versions_dev_.version.c_str());
#endif
return true; return true;
#endif
}
// returns if current dev/stable is upgradeable
bool WebStatusService::current_upgradeable() const {
if (!versions_cache_valid_) {
return false;
}
version::EMSESP_Version current_version(current_version_s);
bool is_dev = current_version.prerelease().find("dev") != std::string::npos;
return is_dev ? versions_dev_.upgradeable : versions_stable_.upgradeable;
} }
// action = allvalues // action = allvalues

View File

@@ -19,6 +19,17 @@ class WebStatusService {
return current_version_s; return current_version_s;
} }
// called from EMSESP::loop() to refresh the cached versions.json from emsesp.org so that the web
// request handler never has to do blocking HTTPS on the small AsyncTCP stack
void loop();
// true once we've had at least one successful versions.json fetch
bool versions_cache_valid() const {
return versions_cache_valid_;
}
bool current_upgradeable() const; // true if a newer version is available
// make action function public so we can test in the debug and standalone mode // make action function public so we can test in the debug and standalone mode
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
protected: protected:
@@ -30,7 +41,7 @@ class WebStatusService {
SecurityManager * _securityManager; SecurityManager * _securityManager;
// actions // actions
bool checkUpgrade(JsonObject root, std::string & latest_version); void getVersions(JsonObject root);
bool exportData(JsonObject root, std::string & type); bool exportData(JsonObject root, std::string & type);
bool getCustomSupport(JsonObject root); bool getCustomSupport(JsonObject root);
bool uploadURL(const char * url); bool uploadURL(const char * url);
@@ -39,6 +50,23 @@ class WebStatusService {
uint8_t upgradeImportantMessages(std::string & version); uint8_t upgradeImportantMessages(std::string & version);
std::string current_version_s = EMSESP_APP_VERSION; std::string current_version_s = EMSESP_APP_VERSION;
// cached emsesp.org/versions.json. Refreshed from the main loop task, which has more stack.
struct VersionInfo {
std::string version;
std::string date;
bool upgradeable = false;
};
VersionInfo versions_stable_;
VersionInfo versions_dev_;
bool versions_cache_valid_ = false; // true once we've had at least one successful fetch
uint32_t versions_next_fetch_ms_ = 0; // uuid::get_uptime() of the next attempt; 0 = ASAP
bool refresh_versions_cache(); // does the actual HTTPS fetch + parse, returns true on success
static constexpr uint32_t VERSIONS_REFRESH_INTERVAL_MS = 60UL * 60UL * 1000UL; // 1 hour on success
static constexpr uint32_t VERSIONS_RETRY_INTERVAL_MS = 5UL * 60UL * 1000UL; // 5 min after failure
static constexpr uint32_t VERSIONS_INITIAL_DELAY_MS = 30UL * 1000UL; // wait 30s after boot
}; };
} // namespace emsesp } // namespace emsesp