mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-05-02 20:16:59 +00:00
This commit is contained in:
@@ -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';
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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({
|
||||||
.catch((error: unknown) => {
|
version: versions.dev.version,
|
||||||
if (cancelled) return;
|
date: versions.dev.date
|
||||||
toast.error(error instanceof Error ? error.message : 'An error occurred');
|
});
|
||||||
setInternetLive(false);
|
setDevUpgradeAvailable(versions.dev.upgradeable);
|
||||||
});
|
}
|
||||||
return () => {
|
setInternetLive(Boolean(versions.stable || versions.dev));
|
||||||
cancelled = true;
|
})
|
||||||
};
|
.onError(() => {
|
||||||
}, []);
|
setInternetLive(false);
|
||||||
|
});
|
||||||
|
|
||||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||||
immediate: false
|
immediate: false
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
@@ -302,10 +301,10 @@ function updateMask(entity: any, de: any, dd: any) {
|
|||||||
const old_custom_name = dd.nodes[dd_objIndex].cn;
|
const old_custom_name = dd.nodes[dd_objIndex].cn;
|
||||||
console.log(
|
console.log(
|
||||||
'comparing names, old (' +
|
'comparing names, old (' +
|
||||||
old_custom_name +
|
old_custom_name +
|
||||||
') with new (' +
|
') with new (' +
|
||||||
new_custom_name +
|
new_custom_name +
|
||||||
')'
|
')'
|
||||||
);
|
);
|
||||||
if (old_custom_name !== new_custom_name) {
|
if (old_custom_name !== new_custom_name) {
|
||||||
changed = true;
|
changed = true;
|
||||||
@@ -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;
|
||||||
|
function get_versions() {
|
||||||
|
const isDev = THIS_VERSION.includes('dev');
|
||||||
|
const data: {
|
||||||
|
current: { version: string; type: 'stable' | 'dev'; date: string };
|
||||||
|
stable?: { version: string; date: string; upgradeable: boolean };
|
||||||
|
dev?: { version: string; date: string; upgradeable: boolean };
|
||||||
|
} = {
|
||||||
|
current: {
|
||||||
|
version: THIS_VERSION,
|
||||||
|
type: isDev ? 'dev' : 'stable',
|
||||||
|
date: '2026-04-25T12:00:00'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
console.log(
|
if (!MOCK_OFFLINE) {
|
||||||
'Upgrade this version (' +
|
data.stable = {
|
||||||
THIS_VERSION +
|
version: LATEST_STABLE_VERSION,
|
||||||
') to dev (' +
|
date: '2026-04-25',
|
||||||
dev_version +
|
upgradeable: STABLE_VERSION_IS_UPGRADEABLE
|
||||||
') is ' +
|
|
||||||
(DEV_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') +
|
|
||||||
' and to stable (' +
|
|
||||||
stable_version +
|
|
||||||
') is ' +
|
|
||||||
(STABLE_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO')
|
|
||||||
);
|
|
||||||
data = {
|
|
||||||
emsesp_version: THIS_VERSION,
|
|
||||||
dev_upgradeable: DEV_VERSION_IS_UPGRADEABLE,
|
|
||||||
stable_upgradeable: STABLE_VERSION_IS_UPGRADEABLE
|
|
||||||
};
|
};
|
||||||
} else {
|
data.dev = {
|
||||||
console.log('requesting ems-esp version (' + THIS_VERSION + ')');
|
version: LATEST_DEV_VERSION,
|
||||||
data = {
|
date: '2026-04-25',
|
||||||
emsesp_version: THIS_VERSION
|
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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
version::EMSESP_Version current_version(current_version_s);
|
// the AsyncTCP callback (which has a tiny ~6 KB stack — far too small for an
|
||||||
version::EMSESP_Version latest_dev_version(version.substr(0, version.find(',')));
|
// HTTPS handshake). If we have no cached data yet (no internet, fetch still
|
||||||
version::EMSESP_Version latest_stable_version(version.substr(version.find(',') + 1));
|
// 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);
|
||||||
|
bool is_dev = current_version.prerelease().find("dev") != std::string::npos;
|
||||||
|
|
||||||
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, ¤t_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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user