import { useContext, useEffect, useState } from 'react'; import { toast } from 'react-toastify'; import CancelIcon from '@mui/icons-material/Cancel'; import CheckIcon from '@mui/icons-material/Done'; import DownloadIcon from '@mui/icons-material/GetApp'; import WarningIcon from '@mui/icons-material/Warning'; import { Box, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, Grid, Link, Typography } from '@mui/material'; import * as SystemApi from 'api/system'; import { API, callAction } from 'api/app'; import { getDevVersion, getStableVersion } from 'api/system'; import { dialogStyle } from 'CustomTheme'; import { useRequest } from 'alova/client'; import type { APIcall } from 'app/main/types'; import SystemMonitor from 'app/status/SystemMonitor'; import { FormLoader, SectionContent, SingleUpload, useLayoutTitle } from 'components'; import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; const Version = () => { const { LL, locale } = useI18nContext(); const { me } = useContext(AuthenticatedContext); const [restarting, setRestarting] = useState(false); const [openInstallDialog, setOpenInstallDialog] = useState(false); const [usingDevVersion, setUsingDevVersion] = useState(false); const [fetchDevVersion, setFetchDevVersion] = useState(false); const [devUpgradeAvailable, setDevUpgradeAvailable] = useState(false); const [stableUpgradeAvailable, setStableUpgradeAvailable] = useState(false); const [internetLive, setInternetLive] = useState(false); const [downloadOnly, setDownloadOnly] = useState(false); const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/'; const STABLE_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md'; const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/'; const DEV_RELNOTES_URL = 'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md'; const { send: sendCheckUpgrade } = useRequest( (versions: string) => callAction({ action: 'checkUpgrade', param: versions }), { immediate: false } ).onSuccess((event) => { const data = event.data as { emsesp_version: string; dev_upgradeable: boolean; stable_upgradeable: boolean; }; setDevUpgradeAvailable(data.dev_upgradeable); setStableUpgradeAvailable(data.stable_upgradeable); }); const { data: data, send: loadData, error } = useRequest(SystemApi.readSystemStatus).onSuccess((event) => { // older version of EMS-ESP on 4MB boards, can't use OTA because of SSL support in HttpClient if (event.data.arduino_version.startsWith('Tasmota')) { setDownloadOnly(true); } setUsingDevVersion(event.data.emsesp_version.includes('dev')); }); const { send: sendUploadURL } = useRequest( (url: string) => callAction({ action: 'uploadURL', param: url }), { immediate: false } ); // called immediately to get the latest versions on page load const { data: latestVersion } = useRequest(getStableVersion); const { data: latestDevVersion } = useRequest(getDevVersion); useEffect(() => { if (latestVersion && latestDevVersion) { sendCheckUpgrade(latestDevVersion.name + ',' + latestVersion.name) .catch((error: Error) => { toast.error('Failed to check for upgrades: ' + error.message); }) .finally(() => { setInternetLive(true); }); } }, [latestVersion, latestDevVersion]); const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }); const DIVISIONS = [ { amount: 60, name: 'seconds' }, { amount: 60, name: 'minutes' }, { amount: 24, name: 'hours' }, { amount: 7, name: 'days' }, { amount: 4.34524, name: 'weeks' }, { amount: 12, name: 'months' }, { amount: Number.POSITIVE_INFINITY, name: 'years' } ]; function formatTimeAgo(date) { let duration = (date.getTime() - new Date().getTime()) / 1000; for (let i = 0; i < DIVISIONS.length; i++) { const division = DIVISIONS[i]; if (Math.abs(duration) < division.amount) { return rtf.format( Math.round(duration), division.name as Intl.RelativeTimeFormatUnit ); } duration /= division.amount; } } const { send: sendAPI } = useRequest((data: APIcall) => API(data), { immediate: false }); const doRestart = async () => { setRestarting(true); await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch( (error: Error) => { toast.error(error.message); } ); }; const getBinURL = () => { if (!internetLive) { return ''; } const filename = 'EMS-ESP-' + (fetchDevVersion ? latestDevVersion.name : latestVersion.name).replaceAll( '.', '_' ) + '-' + getPlatform() + '.bin'; return fetchDevVersion ? DEV_URL + filename : STABLE_URL + 'v' + latestVersion.name + '/' + filename; }; const getPlatform = () => { return ( [data.esp_platform, data.flash_chip_size >= 16384 ? '16MB' : '4MB'].join('-') + (data.psram ? '+' : '') ); }; const installFirmwareURL = async (url: string) => { await sendUploadURL(url).catch((error: Error) => { toast.error(error.message); }); setRestarting(true); }; useLayoutTitle('EMS-ESP Firmware'); const renderInstallDialog = () => { const binURL = getBinURL(); return ( closeInstallDialog()} > {LL.INSTALL() + ' ' + (fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()) + ' Firmware'} {LL.INSTALL_VERSION( fetchDevVersion ? latestDevVersion?.name : latestVersion?.name )} ); }; const showFirmwareDialog = (useDevVersion: boolean) => { setFetchDevVersion(useDevVersion); setOpenInstallDialog(true); }; const closeInstallDialog = () => { setOpenInstallDialog(false); }; const showButtons = (showingDev: boolean) => { const choice = showingDev ? !usingDevVersion ? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT()) : devUpgradeAvailable ? LL.UPDATE_AVAILABLE() : undefined : usingDevVersion ? LL.SWITCH_RELEASE_TYPE(LL.STABLE()) : stableUpgradeAvailable ? LL.UPDATE_AVAILABLE() : undefined; if (!choice) { return ( <> {LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())} ); } if (!me.admin) { return; } if (downloadOnly) { return ( ); } return ( ); }; const content = () => { if (!data) { return ; } const isDev = data.emsesp_version.includes('dev'); return ( <> {LL.THIS_VERSION()} {LL.VERSION()} {data.emsesp_version} {data.build_flags && (   ({data.build_flags}) )} {LL.PLATFORM()} {getPlatform()}   ({data.psram ? '+PSRAM' : '-PSRAM'}) {LL.RELEASE_TYPE()} } slotProps={{ typography: { color: 'grey' } }} checked={!isDev} label={LL.STABLE()} sx={{ '& .MuiSvgIcon-root': { fontSize: 16 } }} /> } slotProps={{ typography: { color: 'grey' } }} checked={isDev} label={LL.DEVELOPMENT()} sx={{ '& .MuiSvgIcon-root': { fontSize: 16 } }} /> {internetLive ? ( <> {LL.AVAILABLE_VERSION()} {LL.STABLE()} {latestVersion.name} {latestVersion.published_at && (  ( {formatTimeAgo(new Date(latestVersion.published_at))}) )} {showButtons(false)} {LL.DEVELOPMENT()} {latestDevVersion.name} {latestDevVersion.published_at && (  ( {formatTimeAgo(new Date(latestDevVersion.published_at))}) )} {showButtons(true)} ) : ( {LL.INTERNET_CONNECTION_REQUIRED()} )} {me.admin && ( <> {renderInstallDialog()} {LL.UPLOAD()} )} ); }; return ( {restarting ? : content()} ); }; export default Version;