mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-06-19 14:26:27 +03:00
Compare commits
26 Commits
0867e166e1
...
f97f4d83f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f97f4d83f1 | ||
|
|
aaff759227 | ||
|
|
1314f643f7 | ||
|
|
55a5ffc6df | ||
|
|
e6a04b2c4f | ||
|
|
c2f0cdab15 | ||
|
|
dc1a9ad993 | ||
|
|
2522218385 | ||
|
|
258daef1cf | ||
|
|
df7e95b644 | ||
|
|
e67ce25eb9 | ||
|
|
88768aaf75 | ||
|
|
2543d2f484 | ||
|
|
96f5510153 | ||
|
|
1d6813fe30 | ||
|
|
48b3ab9085 | ||
|
|
7f78e27649 | ||
|
|
46c5560222 | ||
|
|
cef5e69aa1 | ||
|
|
601395079e | ||
|
|
bbb05594e4 | ||
|
|
2f5400b49a | ||
|
|
ace071e974 | ||
|
|
a406d6d911 | ||
|
|
7409c7286f | ||
|
|
51c0157d1d |
@@ -12,8 +12,11 @@ For more details go to [emsesp.org](https://emsesp.org/).
|
|||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
- signed value for solarInfuence [#3077](https://github.com/emsesp/EMS-ESP32/issues/3077)
|
- signed value for solarInfuence [#3077](https://github.com/emsesp/EMS-ESP32/issues/3077)
|
||||||
|
- TLS support with 4MB boards without PSRAM
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
- various memory optimizations [#3083](https://github.com/emsesp/EMS-ESP32/issues/3083)
|
- various memory optimizations [#3083](https://github.com/emsesp/EMS-ESP32/issues/3083)
|
||||||
|
- Scheduler name is mandatory
|
||||||
|
- Scheduler with type "Immediate", click Execute does not run the command [#3092](https://github.com/emsesp/EMS-ESP32/issues/3092)
|
||||||
- network fallback to AP only after start [#3090](https://github.com/emsesp/EMS-ESP32/issues/3090)
|
- network fallback to AP only after start [#3090](https://github.com/emsesp/EMS-ESP32/issues/3090)
|
||||||
|
|||||||
1
Makefile
1
Makefile
@@ -65,7 +65,6 @@ CXX_STANDARD := -std=gnu++20
|
|||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
DEFINES += -DARDUINOJSON_ENABLE -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
|
DEFINES += -DARDUINOJSON_ENABLE -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
|
||||||
DEFINES += -DEMSESP_STANDALONE -DEMSESP_TEST -DEMSESP_DEBUG -DEMC_RX_BUFFER_SIZE=1500
|
DEFINES += -DEMSESP_STANDALONE -DEMSESP_TEST -DEMSESP_DEBUG -DEMC_RX_BUFFER_SIZE=1500
|
||||||
DEFINES += -DNO_TLS_SUPPORT
|
|
||||||
DEFINES += $(ARGS)
|
DEFINES += $(ARGS)
|
||||||
|
|
||||||
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32S3\"
|
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32S3\"
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
},
|
},
|
||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": [
|
"extra_flags": [
|
||||||
"-DNO_TLS_SUPPORT",
|
|
||||||
"-DARDUINO_LOLIN_C3_MINI",
|
"-DARDUINO_LOLIN_C3_MINI",
|
||||||
"-DARDUINO_USB_MODE=1",
|
"-DARDUINO_USB_MODE=1",
|
||||||
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": [
|
"extra_flags": [
|
||||||
"-DBOARD_HAS_PSRAM",
|
"-DBOARD_HAS_PSRAM",
|
||||||
"-DNO_TLS_SUPPORT",
|
|
||||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||||
"-DARDUINO_USB_MODE=0"
|
"-DARDUINO_USB_MODE=0"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"build": {
|
"build": {
|
||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": "-DNO_TLS_SUPPORT",
|
|
||||||
"f_cpu": "240000000L",
|
"f_cpu": "240000000L",
|
||||||
"f_flash": "40000000L",
|
"f_flash": "40000000L",
|
||||||
"flash_mode": "dio",
|
"flash_mode": "dio",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"build": {
|
"build": {
|
||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": "-DNO_TLS_SUPPORT",
|
|
||||||
"f_cpu": "240000000L",
|
"f_cpu": "240000000L",
|
||||||
"f_flash": "40000000L",
|
"f_flash": "40000000L",
|
||||||
"flash_mode": "dio",
|
"flash_mode": "dio",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": [
|
"extra_flags": [
|
||||||
"-DNO_TLS_SUPPORT",
|
|
||||||
"-DARDUINO_XIAO_ESP32C6",
|
"-DARDUINO_XIAO_ESP32C6",
|
||||||
"-DARDUINO_USB_MODE=1",
|
"-DARDUINO_USB_MODE=1",
|
||||||
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"react-icons": "^5.6.0",
|
"react-icons": "^5.6.0",
|
||||||
"react-router": "^7.15.1",
|
"react-router": "^7.16.0",
|
||||||
"react-toastify": "^11.1.0",
|
"react-toastify": "^11.1.0",
|
||||||
"typesafe-i18n": "^5.27.1",
|
"typesafe-i18n": "^5.27.1",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
@@ -50,15 +50,15 @@
|
|||||||
"@types/node": "^25.9.1",
|
"@types/node": "^25.9.1",
|
||||||
"@types/react": "^19.2.15",
|
"@types/react": "^19.2.15",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^10.0.0",
|
||||||
"eslint": "^10.4.0",
|
"eslint": "^10.4.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
"rollup-plugin-visualizer": "^7.0.1",
|
"rollup-plugin-visualizer": "^7.0.1",
|
||||||
"terser": "^5.47.1",
|
"terser": "^5.48.0",
|
||||||
"typescript-eslint": "^8.59.4",
|
"typescript-eslint": "^8.60.0",
|
||||||
"vite": "^8.0.13",
|
"vite": "^8.0.14",
|
||||||
"vite-plugin-imagemin": "^0.6.1"
|
"vite-plugin-imagemin": "^0.6.1"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800"
|
"packageManager": "pnpm@10.34.1+sha512.b58fbde6dca66a929538021581f648b4570b6ca19b18e7cbd7f2c07a7b24454155388dacdf08f2af3678e88a6d1fe04f9d609df24bf51735a060ea041b374ab7"
|
||||||
}
|
}
|
||||||
|
|||||||
1065
interface/pnpm-lock.yaml
generated
1065
interface/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
|||||||
allowBuilds:
|
|
||||||
cwebp-bin: true
|
|
||||||
esbuild: true
|
|
||||||
gifsicle: true
|
|
||||||
jpegtran-bin: true
|
|
||||||
mozjpeg: true
|
|
||||||
optipng-bin: true
|
|
||||||
pngquant-bin: true
|
|
||||||
onlyBuiltDependencies:
|
|
||||||
- cwebp-bin
|
|
||||||
- esbuild
|
|
||||||
- gifsicle
|
|
||||||
- jpegtran-bin
|
|
||||||
- mozjpeg
|
|
||||||
- optipng-bin
|
|
||||||
- pngquant-bin
|
|
||||||
@@ -240,7 +240,7 @@ const Scheduler = () => {
|
|||||||
.filter((si: ScheduleItem) => !si.deleted)
|
.filter((si: ScheduleItem) => !si.deleted)
|
||||||
.sort((a: ScheduleItem, b: ScheduleItem) => a.flags - b.flags);
|
.sort((a: ScheduleItem, b: ScheduleItem) => a.flags - b.flags);
|
||||||
|
|
||||||
const dayBox = (si: ScheduleItem, flag: number) => {
|
const dayBox = (si: ScheduleItem, flag: number, isLast = false) => {
|
||||||
const dayIndex = Math.log(flag) / LOG_2;
|
const dayIndex = Math.log(flag) / LOG_2;
|
||||||
const isActive = (si.flags & flag) === flag;
|
const isActive = (si.flags & flag) === flag;
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ const Scheduler = () => {
|
|||||||
{dow[dayIndex]}
|
{dow[dayIndex]}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider orientation="vertical" flexItem />
|
{!isLast && <Divider orientation="vertical" flexItem />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -297,14 +297,15 @@ const Scheduler = () => {
|
|||||||
{tableList.map((si: ScheduleItem) => (
|
{tableList.map((si: ScheduleItem) => (
|
||||||
<Row key={si.id} item={si} onClick={() => editScheduleItem(si)}>
|
<Row key={si.id} item={si} onClick={() => editScheduleItem(si)}>
|
||||||
<Cell stiff>
|
<Cell stiff>
|
||||||
<CircleIcon
|
{si.flags !== ScheduleFlag.SCHEDULE_IMMEDIATE && (
|
||||||
color={si.active ? 'success' : 'error'}
|
<CircleIcon
|
||||||
sx={{ fontSize: ICON_SIZE, verticalAlign: 'middle' }}
|
color={si.active ? 'success' : 'error'}
|
||||||
/>
|
sx={{ fontSize: ICON_SIZE, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell stiff>
|
<Cell stiff>
|
||||||
<Stack spacing={0.5} direction="row">
|
<Stack spacing={0.5} direction="row">
|
||||||
<Divider orientation="vertical" flexItem />
|
|
||||||
{si.flags > SCHEDULE_FLAG_THRESHOLD ? (
|
{si.flags > SCHEDULE_FLAG_THRESHOLD ? (
|
||||||
scheduleType(si)
|
scheduleType(si)
|
||||||
) : (
|
) : (
|
||||||
@@ -315,7 +316,7 @@ const Scheduler = () => {
|
|||||||
{dayBox(si, ScheduleFlag.SCHEDULE_THU)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_THU)}
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_FRI)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_FRI)}
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_SAT)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_SAT)}
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_SUN)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_SUN, true)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
@@ -20,7 +21,9 @@ import {
|
|||||||
Typography
|
Typography
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import { callAction } from '@/api/app';
|
||||||
import { dialogStyle } from 'CustomTheme';
|
import { dialogStyle } from 'CustomTheme';
|
||||||
|
import { useRequest } from 'alova/client';
|
||||||
import type Schema from 'async-validator';
|
import type Schema from 'async-validator';
|
||||||
import type { ValidateFieldsError } from 'async-validator';
|
import type { ValidateFieldsError } from 'async-validator';
|
||||||
import { BlockFormControlLabel, ValidatedTextField } from 'components';
|
import { BlockFormControlLabel, ValidatedTextField } from 'components';
|
||||||
@@ -128,8 +131,15 @@ const SchedulerDialog = ({
|
|||||||
await handleSave(editItem);
|
await handleSave(editItem);
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveandactivate = async () => {
|
const { send: executeSchedule } = useRequest(
|
||||||
await handleSave({ ...editItem, active: true });
|
(id: string) => callAction({ action: 'executeSchedule', param: id }),
|
||||||
|
{ immediate: false }
|
||||||
|
).onError((error) => {
|
||||||
|
toast.error(String(error.error?.message || 'An error occurred'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const execute = async () => {
|
||||||
|
await executeSchedule(editItem.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
const remove = () => {
|
const remove = () => {
|
||||||
@@ -351,7 +361,7 @@ const SchedulerDialog = ({
|
|||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors || {}}
|
fieldErrors={fieldErrors || {}}
|
||||||
name="name"
|
name="name"
|
||||||
label={LL.NAME(0) + ' (' + LL.OPTIONAL() + ')'}
|
label={LL.NAME(0)}
|
||||||
value={editItem.name}
|
value={editItem.name}
|
||||||
fullWidth
|
fullWidth
|
||||||
margin="normal"
|
margin="normal"
|
||||||
@@ -388,11 +398,11 @@ const SchedulerDialog = ({
|
|||||||
>
|
>
|
||||||
{creating ? LL.ADD(0) : LL.UPDATE()}
|
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||||
</Button>
|
</Button>
|
||||||
{isImmediateSchedule && editItem.cmd !== '' && (
|
{isImmediateSchedule && !creating && editItem.cmd !== '' && (
|
||||||
<Button
|
<Button
|
||||||
startIcon={<PlayArrowIcon />}
|
startIcon={<PlayArrowIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={saveandactivate}
|
onClick={execute}
|
||||||
color="success"
|
color="success"
|
||||||
>
|
>
|
||||||
{LL.EXECUTE()}
|
{LL.EXECUTE()}
|
||||||
|
|||||||
@@ -348,14 +348,14 @@ export enum DeviceEntityMask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ScheduleItem {
|
export interface ScheduleItem {
|
||||||
id: number; // unique index
|
id: number; // unique index for table
|
||||||
active: boolean;
|
active: boolean;
|
||||||
deleted?: boolean;
|
deleted?: boolean;
|
||||||
flags: number;
|
flags: number;
|
||||||
time: string; // also used for Condition and On Change
|
time: string; // also used for Condition and On Change
|
||||||
cmd: string;
|
cmd: string;
|
||||||
value: string;
|
value: string;
|
||||||
name: string; // can be empty
|
name: string;
|
||||||
o_id?: number;
|
o_id?: number;
|
||||||
o_active?: boolean;
|
o_active?: boolean;
|
||||||
o_deleted?: boolean;
|
o_deleted?: boolean;
|
||||||
|
|||||||
@@ -232,7 +232,11 @@ export const schedulerItemValidation = (
|
|||||||
scheduleItem: ScheduleItem
|
scheduleItem: ScheduleItem
|
||||||
) =>
|
) =>
|
||||||
new Schema({
|
new Schema({
|
||||||
name: [NAME_PATTERN, uniqueNameValidator(schedule, scheduleItem.o_name)],
|
name: [
|
||||||
|
{ required: true, message: 'Name is required' },
|
||||||
|
NAME_PATTERN_REQUIRED,
|
||||||
|
uniqueNameValidator(schedule, scheduleItem.o_name)
|
||||||
|
],
|
||||||
cmd: [
|
cmd: [
|
||||||
{ required: true, message: 'Command is required' },
|
{ required: true, message: 'Command is required' },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -258,7 +258,6 @@ const InstallDialog = memo(
|
|||||||
latestVersion,
|
latestVersion,
|
||||||
latestDevVersion,
|
latestDevVersion,
|
||||||
upgradeImportantMessageType,
|
upgradeImportantMessageType,
|
||||||
downloadOnly,
|
|
||||||
platform,
|
platform,
|
||||||
LL,
|
LL,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -269,7 +268,6 @@ const InstallDialog = memo(
|
|||||||
latestVersion: VersionInfo | undefined;
|
latestVersion: VersionInfo | undefined;
|
||||||
latestDevVersion: VersionInfo | undefined;
|
latestDevVersion: VersionInfo | undefined;
|
||||||
upgradeImportantMessageType: number;
|
upgradeImportantMessageType: number;
|
||||||
downloadOnly: boolean;
|
|
||||||
platform: string;
|
platform: string;
|
||||||
LL: TranslationFunctions;
|
LL: TranslationFunctions;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -292,7 +290,7 @@ const InstallDialog = memo(
|
|||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Typography sx={{ mb: 2 }}>
|
<Typography sx={{ mb: 2 }}>
|
||||||
{LL.INSTALL_VERSION(
|
{LL.INSTALL_VERSION(
|
||||||
downloadOnly ? LL.DOWNLOAD(1) : LL.INSTALL(),
|
LL.INSTALL(),
|
||||||
fetchDevVersion ? latestDevVersion?.version : latestVersion?.version
|
fetchDevVersion ? latestDevVersion?.version : latestVersion?.version
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -342,16 +340,14 @@ const InstallDialog = memo(
|
|||||||
{LL.DOWNLOAD(0)}
|
{LL.DOWNLOAD(0)}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
{!downloadOnly && (
|
<Button
|
||||||
<Button
|
startIcon={<WarningIcon color="warning" />}
|
||||||
startIcon={<WarningIcon color="warning" />}
|
variant="outlined"
|
||||||
variant="outlined"
|
onClick={() => onInstall(binURL)}
|
||||||
onClick={() => onInstall(binURL)}
|
color="primary"
|
||||||
color="primary"
|
>
|
||||||
>
|
{LL.INSTALL()}
|
||||||
{LL.INSTALL()}
|
</Button>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
@@ -430,7 +426,6 @@ const Version = () => {
|
|||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
|
|
||||||
const [fetchDevVersion, setFetchDevVersion] = useState<boolean>(false);
|
const [fetchDevVersion, setFetchDevVersion] = useState<boolean>(false);
|
||||||
const [downloadOnly, setDownloadOnly] = useState<boolean>(false);
|
|
||||||
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);
|
||||||
|
|
||||||
@@ -460,16 +455,7 @@ const Version = () => {
|
|||||||
toast.error(String(error.error?.message || 'An error occurred'));
|
toast.error(String(error.error?.message || 'An error occurred'));
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus);
|
||||||
data,
|
|
||||||
send: loadData,
|
|
||||||
error
|
|
||||||
} = useRequest(SystemApi.readSystemStatus).onSuccess((event) => {
|
|
||||||
const systemData = event.data as VersionData;
|
|
||||||
if (systemData.arduino_version.startsWith('Tasmota')) {
|
|
||||||
setDownloadOnly(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { send: sendUploadURL } = useRequest(
|
const { send: sendUploadURL } = useRequest(
|
||||||
(url: string) => callAction({ action: 'uploadURL', param: url }),
|
(url: string) => callAction({ action: 'uploadURL', param: url }),
|
||||||
@@ -842,7 +828,6 @@ const Version = () => {
|
|||||||
latestVersion={latestVersion}
|
latestVersion={latestVersion}
|
||||||
latestDevVersion={latestDevVersion}
|
latestDevVersion={latestDevVersion}
|
||||||
upgradeImportantMessageType={upgradeImportantMessageType}
|
upgradeImportantMessageType={upgradeImportantMessageType}
|
||||||
downloadOnly={downloadOnly}
|
|
||||||
platform={platform}
|
platform={platform}
|
||||||
LL={LL}
|
LL={LL}
|
||||||
onClose={closeInstallDialog}
|
onClose={closeInstallDialog}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ For a copy, see <https://opensource.org/licenses/MIT> or
|
|||||||
the LICENSE file.
|
the LICENSE file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef NO_TLS_SUPPORT
|
|
||||||
|
|
||||||
#include "ClientSecureSync.h"
|
#include "ClientSecureSync.h"
|
||||||
#include <lwip/sockets.h>
|
#include <lwip/sockets.h>
|
||||||
#include "../Config.h"
|
#include "../Config.h"
|
||||||
@@ -66,5 +64,3 @@ bool ClientSecureSync::disconnected() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace espMqttClientInternals
|
} // namespace espMqttClientInternals
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -8,7 +8,6 @@ the LICENSE file.
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#ifndef NO_TLS_SUPPORT
|
|
||||||
|
|
||||||
// #include "esp_tls.h"
|
// #include "esp_tls.h"
|
||||||
#include <WiFiClient.h>
|
#include <WiFiClient.h>
|
||||||
@@ -34,5 +33,3 @@ class ClientSecureSync : public Transport {
|
|||||||
};
|
};
|
||||||
|
|
||||||
} // namespace espMqttClientInternals
|
} // namespace espMqttClientInternals
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -34,36 +34,26 @@ espMqttClientSecure::espMqttClientSecure(uint8_t priority, uint8_t core)
|
|||||||
}
|
}
|
||||||
|
|
||||||
espMqttClientSecure & espMqttClientSecure::setInsecure() {
|
espMqttClientSecure & espMqttClientSecure::setInsecure() {
|
||||||
#ifndef NO_TLS_SUPPORT
|
|
||||||
_client.client.setInsecure();
|
_client.client.setInsecure();
|
||||||
#endif
|
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
espMqttClientSecure & espMqttClientSecure::setCACert(const char * rootCA) {
|
espMqttClientSecure & espMqttClientSecure::setCACert(const char * rootCA) {
|
||||||
#ifndef NO_TLS_SUPPORT
|
|
||||||
_client.client.setCACert(rootCA);
|
_client.client.setCACert(rootCA);
|
||||||
#endif
|
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
espMqttClientSecure & espMqttClientSecure::setCertificate(const char * clientCa) {
|
espMqttClientSecure & espMqttClientSecure::setCertificate(const char * clientCa) {
|
||||||
#ifndef NO_TLS_SUPPORT
|
|
||||||
_client.client.setCertificate(clientCa);
|
_client.client.setCertificate(clientCa);
|
||||||
#endif
|
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
espMqttClientSecure & espMqttClientSecure::setPrivateKey(const char * privateKey) {
|
espMqttClientSecure & espMqttClientSecure::setPrivateKey(const char * privateKey) {
|
||||||
#ifndef NO_TLS_SUPPORT
|
|
||||||
_client.client.setPrivateKey(privateKey);
|
_client.client.setPrivateKey(privateKey);
|
||||||
#endif
|
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
espMqttClientSecure & espMqttClientSecure::setPreSharedKey(const char * pskIdent, const char * psKey) {
|
espMqttClientSecure & espMqttClientSecure::setPreSharedKey(const char * pskIdent, const char * psKey) {
|
||||||
#ifndef NO_TLS_SUPPORT
|
|
||||||
#endif
|
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,11 +65,7 @@ class espMqttClientSecure : public MqttClientSetup<espMqttClientSecure> {
|
|||||||
espMqttClientSecure & setPreSharedKey(const char * pskIdent, const char * psKey);
|
espMqttClientSecure & setPreSharedKey(const char * pskIdent, const char * psKey);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
#ifndef NO_TLS_SUPPORT
|
|
||||||
espMqttClientInternals::ClientSecureSync _client;
|
espMqttClientInternals::ClientSecureSync _client;
|
||||||
#else
|
|
||||||
espMqttClientInternals::ClientSync _client;
|
|
||||||
#endif
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
72
lib_standalone/ESP_SSLClient.h
Normal file
72
lib_standalone/ESP_SSLClient.h
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// standalone stub for ESP_SSLClient (BearSSL) - no-op TLS on host build
|
||||||
|
// Inherits from Stream so it gets print/println via Print, matching the real API.
|
||||||
|
|
||||||
|
#ifndef ESP_SSLClient_h
|
||||||
|
#define ESP_SSLClient_h
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "Arduino.h"
|
||||||
|
#include "Stream.h"
|
||||||
|
#include "WiFiClient.h"
|
||||||
|
#include <ClientPosixIPAddress.h>
|
||||||
|
|
||||||
|
class ESP_SSLClient : public Stream {
|
||||||
|
public:
|
||||||
|
ESP_SSLClient() = default;
|
||||||
|
|
||||||
|
void setInsecure() {
|
||||||
|
}
|
||||||
|
void setCACert(const char *) {
|
||||||
|
}
|
||||||
|
void setCertificate(const char *) {
|
||||||
|
}
|
||||||
|
void setPrivateKey(const char *) {
|
||||||
|
}
|
||||||
|
void setBufferSizes(int, int) {
|
||||||
|
}
|
||||||
|
void setSessionTimeout(int) {
|
||||||
|
}
|
||||||
|
void setClient(WiFiClient *) {
|
||||||
|
}
|
||||||
|
void setClient(WiFiClient *, bool) {
|
||||||
|
}
|
||||||
|
|
||||||
|
int connect(IPAddress, uint16_t) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int connect(const char *, uint16_t) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
bool connected() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream / Print overrides
|
||||||
|
int available() override {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int read() override {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int read(uint8_t *, size_t) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int peek() override {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
size_t write(uint8_t) override {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
size_t write(const uint8_t *, size_t) override {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
void flush() override {
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // ESP_SSLClient_h
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#ifndef HTTPClient_H_
|
|
||||||
#define HTTPClient_H_
|
|
||||||
|
|
||||||
#include "WString.h"
|
|
||||||
|
|
||||||
class HTTPClient {
|
|
||||||
public:
|
|
||||||
bool begin(String url) {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
void end(void) {};
|
|
||||||
int GET() {
|
|
||||||
return 200;
|
|
||||||
};
|
|
||||||
int POST(String payload) {
|
|
||||||
return 200;
|
|
||||||
};
|
|
||||||
void addHeader(const String & name, const String & value, bool first = false, bool replace = true) {};
|
|
||||||
String getString(void) {
|
|
||||||
return "Hello, World!";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif /* HTTPClient_H_ */
|
|
||||||
49
lib_standalone/WiFiClient.h
Normal file
49
lib_standalone/WiFiClient.h
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// standalone stub for WiFiClient (no-op networking on host build)
|
||||||
|
|
||||||
|
#ifndef WiFiClient_h
|
||||||
|
#define WiFiClient_h
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "Arduino.h"
|
||||||
|
#include <ClientPosixIPAddress.h>
|
||||||
|
|
||||||
|
class WiFiClient {
|
||||||
|
public:
|
||||||
|
WiFiClient() = default;
|
||||||
|
|
||||||
|
int connect(IPAddress, uint16_t) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int connect(const char *, uint16_t) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
bool connected() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int available() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int read() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int read(uint8_t *, size_t) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
size_t write(uint8_t) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
size_t write(const uint8_t *, size_t) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
void stop() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESP32 socket option passthrough (e.g. TCP_NODELAY)
|
||||||
|
int setSocketOption(int, int, const void *, size_t) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // WiFiClient_h
|
||||||
10
lib_standalone/lwip/sockets.h
Normal file
10
lib_standalone/lwip/sockets.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// standalone stub for <lwip/sockets.h>
|
||||||
|
// pulls in POSIX equivalents on the host build for things like IPPROTO_TCP / TCP_NODELAY.
|
||||||
|
|
||||||
|
#ifndef LWIP_SOCKETS_STUB_H_
|
||||||
|
#define LWIP_SOCKETS_STUB_H_
|
||||||
|
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <netinet/tcp.h>
|
||||||
|
|
||||||
|
#endif // LWIP_SOCKETS_STUB_H_
|
||||||
@@ -15,5 +15,5 @@
|
|||||||
"itty-router": "^5.0.23",
|
"itty-router": "^5.0.23",
|
||||||
"prettier": "^3.8.3"
|
"prettier": "^3.8.3"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800"
|
"packageManager": "pnpm@10.34.1+sha512.b58fbde6dca66a929538021581f648b4570b6ca19b18e7cbd7f2c07a7b24454155388dacdf08f2af3678e88a6d1fe04f9d609df24bf51735a060ea041b374ab7"
|
||||||
}
|
}
|
||||||
|
|||||||
100
mock-api/pnpm-lock.yaml
generated
100
mock-api/pnpm-lock.yaml
generated
@@ -26,41 +26,41 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@babel/code-frame@7.29.0':
|
'@babel/code-frame@7.29.7':
|
||||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/generator@7.29.1':
|
'@babel/generator@7.29.7':
|
||||||
resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==}
|
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/helper-globals@7.28.0':
|
'@babel/helper-globals@7.29.7':
|
||||||
resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==}
|
resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.27.1':
|
'@babel/helper-string-parser@7.29.7':
|
||||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5':
|
'@babel/helper-validator-identifier@7.29.7':
|
||||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/parser@7.29.3':
|
'@babel/parser@7.29.7':
|
||||||
resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==}
|
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@babel/template@7.28.6':
|
'@babel/template@7.29.7':
|
||||||
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
|
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/traverse@7.29.0':
|
'@babel/traverse@7.29.7':
|
||||||
resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==}
|
resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/types@7.29.0':
|
'@babel/types@7.29.7':
|
||||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
@@ -112,8 +112,8 @@ packages:
|
|||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
brace-expansion@2.1.0:
|
brace-expansion@2.1.1:
|
||||||
resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==}
|
resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
@@ -177,52 +177,52 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@babel/code-frame@7.29.0':
|
'@babel/code-frame@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
'@babel/generator@7.29.1':
|
'@babel/generator@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.29.3
|
'@babel/parser': 7.29.7
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.7
|
||||||
'@jridgewell/gen-mapping': 0.3.13
|
'@jridgewell/gen-mapping': 0.3.13
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
jsesc: 3.1.0
|
jsesc: 3.1.0
|
||||||
|
|
||||||
'@babel/helper-globals@7.28.0': {}
|
'@babel/helper-globals@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.27.1': {}
|
'@babel/helper-string-parser@7.29.7': {}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5': {}
|
'@babel/helper-validator-identifier@7.29.7': {}
|
||||||
|
|
||||||
'@babel/parser@7.29.3':
|
'@babel/parser@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
'@babel/template@7.28.6':
|
'@babel/template@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.0
|
'@babel/code-frame': 7.29.7
|
||||||
'@babel/parser': 7.29.3
|
'@babel/parser': 7.29.7
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
'@babel/traverse@7.29.0':
|
'@babel/traverse@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.0
|
'@babel/code-frame': 7.29.7
|
||||||
'@babel/generator': 7.29.1
|
'@babel/generator': 7.29.7
|
||||||
'@babel/helper-globals': 7.28.0
|
'@babel/helper-globals': 7.29.7
|
||||||
'@babel/parser': 7.29.3
|
'@babel/parser': 7.29.7
|
||||||
'@babel/template': 7.28.6
|
'@babel/template': 7.29.7
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.7
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/types@7.29.0':
|
'@babel/types@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.29.7
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
|
|
||||||
'@jridgewell/gen-mapping@0.3.13':
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -248,10 +248,10 @@ snapshots:
|
|||||||
|
|
||||||
'@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.3)':
|
'@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/generator': 7.29.1
|
'@babel/generator': 7.29.7
|
||||||
'@babel/parser': 7.29.3
|
'@babel/parser': 7.29.7
|
||||||
'@babel/traverse': 7.29.0
|
'@babel/traverse': 7.29.7
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.7
|
||||||
javascript-natural-sort: 0.7.1
|
javascript-natural-sort: 0.7.1
|
||||||
lodash-es: 4.18.1
|
lodash-es: 4.18.1
|
||||||
minimatch: 9.0.9
|
minimatch: 9.0.9
|
||||||
@@ -264,7 +264,7 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
brace-expansion@2.1.0:
|
brace-expansion@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ snapshots:
|
|||||||
|
|
||||||
minimatch@9.0.9:
|
minimatch@9.0.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.1.0
|
brace-expansion: 2.1.1
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
|||||||
@@ -393,6 +393,12 @@ function custom_support() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// run a schedule
|
||||||
|
function executeSchedule(name: string) {
|
||||||
|
console.log('executing schedule', name);
|
||||||
|
return status(200);
|
||||||
|
}
|
||||||
|
|
||||||
// called by Action endpoint upgradeImportantMessages
|
// called by Action endpoint upgradeImportantMessages
|
||||||
function upgradeImportantMessages(version: string) {
|
function upgradeImportantMessages(version: string) {
|
||||||
// 0 is do nothing
|
// 0 is do nothing
|
||||||
@@ -5220,6 +5226,9 @@ router
|
|||||||
} else if (action === 'upgradeImportantMessages') {
|
} else if (action === 'upgradeImportantMessages') {
|
||||||
// check upgrade important messages
|
// check upgrade important messages
|
||||||
return upgradeImportantMessages(content.param);
|
return upgradeImportantMessages(content.param);
|
||||||
|
} else if (action === 'executeSchedule') {
|
||||||
|
// execute schedule
|
||||||
|
return executeSchedule(content.param);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return status(404); // cmd not found
|
return status(404); // cmd not found
|
||||||
|
|||||||
@@ -159,6 +159,9 @@ build_flags =
|
|||||||
extends = espressif32_base_4M
|
extends = espressif32_base_4M
|
||||||
board_build.app_partition_name = app0
|
board_build.app_partition_name = app0
|
||||||
board = seeed_xiao_esp32c6
|
board = seeed_xiao_esp32c6
|
||||||
|
build_flags =
|
||||||
|
${common.build_flags}
|
||||||
|
-DEMSESP_EN_ONLY
|
||||||
|
|
||||||
; foundation for building and testing natively, standalone without an ESP32
|
; foundation for building and testing natively, standalone without an ESP32
|
||||||
; use the `standalone` environment instead of `native` for testing
|
; use the `standalone` environment instead of `native` for testing
|
||||||
@@ -169,7 +172,6 @@ build_flags =
|
|||||||
build_src_flags =
|
build_src_flags =
|
||||||
-DEMSESP_STANDALONE -DEMSESP_TEST
|
-DEMSESP_STANDALONE -DEMSESP_TEST
|
||||||
-DARDUINOJSON_ENABLE_ARDUINO_STRING=1
|
-DARDUINOJSON_ENABLE_ARDUINO_STRING=1
|
||||||
-DNO_TLS_SUPPORT
|
|
||||||
-std=gnu++20 -Og -ggdb
|
-std=gnu++20 -Og -ggdb
|
||||||
-Wall -Wextra
|
-Wall -Wextra
|
||||||
-Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces
|
-Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces
|
||||||
@@ -208,7 +210,6 @@ build_src_flags =
|
|||||||
-DEMSESP_STANDALONE -DEMSESP_TEST
|
-DEMSESP_STANDALONE -DEMSESP_TEST
|
||||||
-DEMSESP_UNITY
|
-DEMSESP_UNITY
|
||||||
-DARDUINOJSON_ENABLE_ARDUINO_STRING=1
|
-DARDUINOJSON_ENABLE_ARDUINO_STRING=1
|
||||||
-DNO_TLS_SUPPORT
|
|
||||||
-DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
|
-DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
|
||||||
-std=gnu++20 -Og -ggdb
|
-std=gnu++20 -Og -ggdb
|
||||||
-Wall -Wextra
|
-Wall -Wextra
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ void MqttSettingsService::startClient() {
|
|||||||
delete _mqttClient;
|
delete _mqttClient;
|
||||||
_mqttClient = nullptr;
|
_mqttClient = nullptr;
|
||||||
}
|
}
|
||||||
#ifndef NO_TLS_SUPPORT
|
|
||||||
if (_state.enableTLS) {
|
if (_state.enableTLS) {
|
||||||
isSecure = true;
|
isSecure = true;
|
||||||
if (emsesp::EMSESP::system_.PSram() == 0) {
|
if (emsesp::EMSESP::system_.PSram() == 0) {
|
||||||
@@ -62,7 +61,6 @@ void MqttSettingsService::startClient() {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
isSecure = false;
|
isSecure = false;
|
||||||
if (emsesp::EMSESP::system_.PSram() == 0) {
|
if (emsesp::EMSESP::system_.PSram() == 0) {
|
||||||
_mqttClient = new espMqttClient(espMqttClientTypes::UseInternalTask::NO);
|
_mqttClient = new espMqttClient(espMqttClientTypes::UseInternalTask::NO);
|
||||||
@@ -164,7 +162,6 @@ bool MqttSettingsService::configureMqtt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_reconfigureMqtt = false;
|
_reconfigureMqtt = false;
|
||||||
#ifndef NO_TLS_SUPPORT
|
|
||||||
if (_state.enableTLS) {
|
if (_state.enableTLS) {
|
||||||
if (_state.rootCA == "insecure") {
|
if (_state.rootCA == "insecure") {
|
||||||
#if defined(EMSESP_DEBUG)
|
#if defined(EMSESP_DEBUG)
|
||||||
@@ -189,7 +186,6 @@ bool MqttSettingsService::configureMqtt() {
|
|||||||
static_cast<espMqttClientSecure *>(_mqttClient)->setWill(will_topic, 1, true, "offline"); // QOS 1, retain
|
static_cast<espMqttClientSecure *>(_mqttClient)->setWill(will_topic, 1, true, "offline"); // QOS 1, retain
|
||||||
return _mqttClient->connect();
|
return _mqttClient->connect();
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
static_cast<espMqttClient *>(_mqttClient)->setServer(_state.host.c_str(), _state.port);
|
static_cast<espMqttClient *>(_mqttClient)->setServer(_state.host.c_str(), _state.port);
|
||||||
if (_state.username.length() > 0) {
|
if (_state.username.length() > 0) {
|
||||||
static_cast<espMqttClient *>(_mqttClient)->setCredentials(_state.username.c_str(), _state.password.length() > 0 ? _state.password.c_str() : nullptr);
|
static_cast<espMqttClient *>(_mqttClient)->setCredentials(_state.username.c_str(), _state.password.length() > 0 ? _state.password.c_str() : nullptr);
|
||||||
@@ -205,10 +201,8 @@ bool MqttSettingsService::configureMqtt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MqttSettings::read(MqttSettings & settings, JsonObject root) {
|
void MqttSettings::read(MqttSettings & settings, JsonObject root) {
|
||||||
#ifndef NO_TLS_SUPPORT
|
root["enableTLS"] = settings.enableTLS;
|
||||||
root["enableTLS"] = settings.enableTLS;
|
root["rootCA"] = settings.rootCA;
|
||||||
root["rootCA"] = settings.rootCA;
|
|
||||||
#endif
|
|
||||||
root["enabled"] = settings.enabled;
|
root["enabled"] = settings.enabled;
|
||||||
root["host"] = settings.host;
|
root["host"] = settings.host;
|
||||||
root["port"] = settings.port;
|
root["port"] = settings.port;
|
||||||
@@ -244,12 +238,8 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings)
|
|||||||
MqttSettings newSettings;
|
MqttSettings newSettings;
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
|
|
||||||
#ifndef NO_TLS_SUPPORT
|
newSettings.enableTLS = root["enableTLS"];
|
||||||
newSettings.enableTLS = root["enableTLS"];
|
newSettings.rootCA = root["rootCA"] | "";
|
||||||
newSettings.rootCA = root["rootCA"] | "";
|
|
||||||
#else
|
|
||||||
newSettings.enableTLS = false;
|
|
||||||
#endif
|
|
||||||
newSettings.enabled = root["enabled"] | FACTORY_MQTT_ENABLED;
|
newSettings.enabled = root["enabled"] | FACTORY_MQTT_ENABLED;
|
||||||
newSettings.host = root["host"] | FACTORY_MQTT_HOST;
|
newSettings.host = root["host"] | FACTORY_MQTT_HOST;
|
||||||
newSettings.port = static_cast<uint16_t>(root["port"] | FACTORY_MQTT_PORT);
|
newSettings.port = static_cast<uint16_t>(root["port"] | FACTORY_MQTT_PORT);
|
||||||
@@ -375,7 +365,6 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings)
|
|||||||
emsesp::EMSESP::mqtt_.set_publish_time_heartbeat(newSettings.publish_time_heartbeat);
|
emsesp::EMSESP::mqtt_.set_publish_time_heartbeat(newSettings.publish_time_heartbeat);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef NO_TLS_SUPPORT
|
|
||||||
// strip down to certificate only
|
// strip down to certificate only
|
||||||
newSettings.rootCA.replace("\r", "");
|
newSettings.rootCA.replace("\r", "");
|
||||||
newSettings.rootCA.replace("\n", "");
|
newSettings.rootCA.replace("\n", "");
|
||||||
@@ -388,7 +377,6 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings)
|
|||||||
if (newSettings.enableTLS != settings.enableTLS || newSettings.rootCA != settings.rootCA) {
|
if (newSettings.enableTLS != settings.enableTLS || newSettings.rootCA != settings.rootCA) {
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
// save the new settings
|
// save the new settings
|
||||||
settings = newSettings;
|
settings = newSettings;
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
|
|
||||||
#include <emsesp.h>
|
#include <emsesp.h>
|
||||||
|
|
||||||
#ifdef NO_TLS_SUPPORT
|
|
||||||
#include "lwip/dns.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
NetworkStatus::NetworkStatus(AsyncWebServer * server, SecurityManager * securityManager) {
|
NetworkStatus::NetworkStatus(AsyncWebServer * server, SecurityManager * securityManager) {
|
||||||
securityManager->addEndpoint(server, NETWORK_STATUS_SERVICE_PATH, AuthenticationPredicates::IS_AUTHENTICATED, [this](AsyncWebServerRequest * request) {
|
securityManager->addEndpoint(server, NETWORK_STATUS_SERVICE_PATH, AuthenticationPredicates::IS_AUTHENTICATED, [this](AsyncWebServerRequest * request) {
|
||||||
networkStatus(request);
|
networkStatus(request);
|
||||||
|
|||||||
@@ -110,10 +110,12 @@ DeviceValue::DeviceValue(uint8_t device_type,
|
|||||||
const char * DeviceValue::DeviceValueUOM_s[] = {
|
const char * DeviceValue::DeviceValueUOM_s[] = {
|
||||||
|
|
||||||
F_(uom_blank), // 0
|
F_(uom_blank), // 0
|
||||||
F_(uom_degrees), F_(uom_degrees), F_(uom_percent), F_(uom_lmin), F_(uom_kwh), F_(uom_wh), FL_(hours)[0], FL_(minutes)[0],
|
F_(uom_degrees), F_(uom_degrees), F_(uom_percent), F_(uom_lmin), F_(uom_kwh), F_(uom_wh), FL_(hours)[0], FL_(minutes)[0], F_(uom_ua),
|
||||||
F_(uom_ua), F_(uom_bar), F_(uom_kw), F_(uom_w), F_(uom_kb), FL_(seconds)[0], F_(uom_dbm), F_(uom_fahrenheit),
|
F_(uom_bar), F_(uom_kw), F_(uom_w), F_(uom_kb), FL_(seconds)[0], F_(uom_dbm), F_(uom_fahrenheit), F_(uom_mv), F_(uom_sqm),
|
||||||
F_(uom_mv), F_(uom_sqm), F_(uom_m3), F_(uom_l), F_(uom_kmin), F_(uom_k), F_(uom_volts), F_(uom_mbar),
|
F_(uom_m3), F_(uom_l), F_(uom_kmin), F_(uom_k), F_(uom_volts), F_(uom_mbar), F_(uom_lh), F_(uom_ctkwh), F_(uom_hz),
|
||||||
F_(uom_lh), F_(uom_ctkwh), F_(uom_hz), F_(uom_blank)
|
F_(uom_blank), // connectivity
|
||||||
|
F_(uom_blank), // timestamp
|
||||||
|
F_(uom_blank) // blank
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ class DeviceValue {
|
|||||||
LH, // 25 - l/h - volume flow rate
|
LH, // 25 - l/h - volume flow rate
|
||||||
CTKWH, // 26 - ct/kWh - monetary
|
CTKWH, // 26 - ct/kWh - monetary
|
||||||
HERTZ, // 27 - Hz - frequency
|
HERTZ, // 27 - Hz - frequency
|
||||||
CONNECTIVITY // 28 - used in HA - connectivity
|
CONNECTIVITY, // 28 - used in HA - connectivity
|
||||||
|
TIMESTAMP, // 29 - used in HA - timestamp
|
||||||
};
|
};
|
||||||
|
|
||||||
// TAG mapping - maps to DeviceValueTAG_s in emsdevicevalue.cpp
|
// TAG mapping - maps to DeviceValueTAG_s in emsdevicevalue.cpp
|
||||||
|
|||||||
@@ -1757,6 +1757,12 @@ void EMSESP::start() {
|
|||||||
// start network services. This will initialise WiFi or Ethernet depending on the settings.
|
// start network services. This will initialise WiFi or Ethernet depending on the settings.
|
||||||
network_.begin();
|
network_.begin();
|
||||||
|
|
||||||
|
// start the core web services, as this loads the settings from the filesystem
|
||||||
|
// this will also handle any MQTT subscriptions
|
||||||
|
webCustomizationService.begin(); // load the customizations
|
||||||
|
webSchedulerService.begin(); // load the scheduler events
|
||||||
|
webCustomEntityService.begin(); // load the custom telegram reads
|
||||||
|
|
||||||
// perform any system upgrades
|
// perform any system upgrades
|
||||||
if (!factory_settings) {
|
if (!factory_settings) {
|
||||||
if (system_.check_upgrade()) {
|
if (system_.check_upgrade()) {
|
||||||
@@ -1771,10 +1777,6 @@ void EMSESP::start() {
|
|||||||
};
|
};
|
||||||
LOG_INFO("Library loaded: %d EMS devices, %d device entities, %s", device_library_.size(), EMSESP_TRANSLATION_COUNT, system_.languages_string().c_str());
|
LOG_INFO("Library loaded: %d EMS devices, %d device entities, %s", device_library_.size(), EMSESP_TRANSLATION_COUNT, system_.languages_string().c_str());
|
||||||
|
|
||||||
webCustomizationService.begin(); // load the customizations
|
|
||||||
webSchedulerService.begin(); // load the scheduler events
|
|
||||||
webCustomEntityService.begin(); // load the custom telegram reads
|
|
||||||
|
|
||||||
// start telnet service if it's enabled
|
// start telnet service if it's enabled
|
||||||
// default idle is 10 minutes, default write timeout is 0 (automatic)
|
// default idle is 10 minutes, default write timeout is 0 (automatic)
|
||||||
// note, this must be started after the network/wifi for ESP32 otherwise it'll crash
|
// note, this must be started after the network/wifi for ESP32 otherwise it'll crash
|
||||||
|
|||||||
@@ -268,6 +268,8 @@ MAKE_WORD_CUSTOM(uom_l, "l")
|
|||||||
MAKE_WORD_CUSTOM(uom_kmin, "K*min")
|
MAKE_WORD_CUSTOM(uom_kmin, "K*min")
|
||||||
MAKE_WORD_CUSTOM(uom_k, "K")
|
MAKE_WORD_CUSTOM(uom_k, "K")
|
||||||
MAKE_WORD_CUSTOM(uom_volts, "V")
|
MAKE_WORD_CUSTOM(uom_volts, "V")
|
||||||
|
MAKE_WORD_CUSTOM(uom_connectivity, "connectivity")
|
||||||
|
MAKE_WORD_CUSTOM(uom_timestamp, "timestamp")
|
||||||
MAKE_WORD_CUSTOM(uom_mbar, "mbar")
|
MAKE_WORD_CUSTOM(uom_mbar, "mbar")
|
||||||
MAKE_WORD_CUSTOM(uom_lh, "l/h")
|
MAKE_WORD_CUSTOM(uom_lh, "l/h")
|
||||||
MAKE_WORD_CUSTOM(uom_ctkwh, "ct/kWh")
|
MAKE_WORD_CUSTOM(uom_ctkwh, "ct/kWh")
|
||||||
|
|||||||
@@ -567,8 +567,6 @@ void Mqtt::ha_status() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
publish_system_ha_sensor_config(DeviceValueType::STRING, "EMS Bus", "bus_status", DeviceValueUOM::NONE);
|
publish_system_ha_sensor_config(DeviceValueType::STRING, "EMS Bus", "bus_status", DeviceValueUOM::NONE);
|
||||||
publish_system_ha_sensor_config(DeviceValueType::STRING, "Uptime", "uptime", DeviceValueUOM::NONE);
|
|
||||||
publish_system_ha_sensor_config(DeviceValueType::INT8, "Uptime (sec)", "uptime_sec", DeviceValueUOM::SECONDS);
|
|
||||||
publish_system_ha_sensor_config(DeviceValueType::INT8, "Free memory", "freemem", DeviceValueUOM::KB);
|
publish_system_ha_sensor_config(DeviceValueType::INT8, "Free memory", "freemem", DeviceValueUOM::KB);
|
||||||
publish_system_ha_sensor_config(DeviceValueType::INT8, "Max alloc", "max_alloc", DeviceValueUOM::KB);
|
publish_system_ha_sensor_config(DeviceValueType::INT8, "Max alloc", "max_alloc", DeviceValueUOM::KB);
|
||||||
publish_system_ha_sensor_config(DeviceValueType::INT8, "MQTT fails", "mqttfails", DeviceValueUOM::NONE);
|
publish_system_ha_sensor_config(DeviceValueType::INT8, "MQTT fails", "mqttfails", DeviceValueUOM::NONE);
|
||||||
@@ -585,8 +583,10 @@ void Mqtt::ha_status() {
|
|||||||
if (!EMSESP::network_.ethernet_connected()) {
|
if (!EMSESP::network_.ethernet_connected()) {
|
||||||
publish_system_ha_sensor_config(DeviceValueType::INT16, "WiFi reconnects", "wifireconnects", DeviceValueUOM::NONE);
|
publish_system_ha_sensor_config(DeviceValueType::INT16, "WiFi reconnects", "wifireconnects", DeviceValueUOM::NONE);
|
||||||
}
|
}
|
||||||
// This one comes from the info MQTT topic - and handled in the publish_ha_sensor_config function
|
|
||||||
|
// These come from the info MQTT topic - and handled in the publish_ha_sensor_config function
|
||||||
publish_system_ha_sensor_config(DeviceValueType::STRING, "Version", "version", DeviceValueUOM::NONE);
|
publish_system_ha_sensor_config(DeviceValueType::STRING, "Version", "version", DeviceValueUOM::NONE);
|
||||||
|
publish_system_ha_sensor_config(DeviceValueType::STRING, "Boot time", "bootTime", DeviceValueUOM::TIMESTAMP);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add sub or pub task to the queue.
|
// add sub or pub task to the queue.
|
||||||
@@ -1061,15 +1061,19 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
|
|||||||
|
|
||||||
// add state_topic and it's value_template. This is not needed for commands, only sensors
|
// add state_topic and it's value_template. This is not needed for commands, only sensors
|
||||||
if (type != DeviceValueType::CMD || is_sensor) {
|
if (type != DeviceValueType::CMD || is_sensor) {
|
||||||
// state topic, except for commands
|
|
||||||
char stat_t[MQTT_TOPIC_MAX_SIZE];
|
|
||||||
|
|
||||||
// This is where we determine which MQTT topic to pull the data from
|
// This is where we determine which MQTT topic to pull the data from
|
||||||
// There is one exception for DeviceType::SYSTEM, which uses the heartbeat topic, and when fetching the version we want to take this from the info topic instead
|
char stat_t[MQTT_TOPIC_MAX_SIZE]; // state topic, except for commands
|
||||||
if ((device_type == EMSdevice::DeviceType::SYSTEM) && (strncmp(entity, "version", 7) == 0)) {
|
snprintf(stat_t, sizeof(stat_t), "~/%s", tag_to_topic(device_type, tag).c_str());
|
||||||
snprintf(stat_t, sizeof(stat_t), "~/%s", F_(info));
|
|
||||||
} else {
|
// Override - there are exceptions for DeviceType::SYSTEM, which uses the heartbeat topic
|
||||||
snprintf(stat_t, sizeof(stat_t), "~/%s", tag_to_topic(device_type, tag).c_str());
|
// and when fetching the version and bootTime we want to take this from the info topic instead
|
||||||
|
if (device_type == EMSdevice::DeviceType::SYSTEM) {
|
||||||
|
// handle the exceptions
|
||||||
|
if (strncmp(entity, "version", 7) == 0) {
|
||||||
|
snprintf(stat_t, sizeof(stat_t), "~/%s", F_(info));
|
||||||
|
} else if (strncmp(entity, "bootTime", 8) == 0) {
|
||||||
|
snprintf(stat_t, sizeof(stat_t), "~/%s", F_(info));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
doc["stat_t"] = stat_t;
|
doc["stat_t"] = stat_t;
|
||||||
|
|
||||||
@@ -1095,7 +1099,14 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
|
|||||||
|
|
||||||
// don't bother with value template conditions if using Domoticz which doesn't fully support MQTT Discovery
|
// don't bother with value template conditions if using Domoticz which doesn't fully support MQTT Discovery
|
||||||
if (discovery_type() == discoveryType::HOMEASSISTANT) {
|
if (discovery_type() == discoveryType::HOMEASSISTANT) {
|
||||||
doc["val_tpl"] = (std::string) "{{" + val_obj + " if " + val_cond + " else " + sample_val + "}}";
|
if (uom == DeviceValueUOM::TIMESTAMP) {
|
||||||
|
// special case for timestamp, using "value_template": "{{ (value_json.bootTime | as_datetime).isoformat() }}",
|
||||||
|
char val_tpl[100];
|
||||||
|
snprintf(val_tpl, sizeof(val_tpl), "{{ (value_json.%s | as_datetime).isoformat() }}", entity);
|
||||||
|
doc["val_tpl"] = val_tpl;
|
||||||
|
} else {
|
||||||
|
doc["val_tpl"] = (std::string) "{{" + val_obj + " if " + val_cond + " else " + sample_val + "}}";
|
||||||
|
}
|
||||||
add_ha_avty_section(doc.as<JsonObject>(), stat_t, val_cond); // adds availability section
|
add_ha_avty_section(doc.as<JsonObject>(), stat_t, val_cond); // adds availability section
|
||||||
} else {
|
} else {
|
||||||
// Domoticz doesn't support value templates, so we just use the value directly
|
// Domoticz doesn't support value templates, so we just use the value directly
|
||||||
@@ -1166,8 +1177,13 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8
|
|||||||
doc[uom_ha] = "L/h";
|
doc[uom_ha] = "L/h";
|
||||||
} else if (uom == DeviceValueUOM::L) {
|
} else if (uom == DeviceValueUOM::L) {
|
||||||
doc[uom_ha] = "L";
|
doc[uom_ha] = "L";
|
||||||
|
} else if (uom == DeviceValueUOM::TIMESTAMP) {
|
||||||
|
// do nothing
|
||||||
} else if (uom != DeviceValueUOM::NONE) {
|
} else if (uom != DeviceValueUOM::NONE) {
|
||||||
doc[uom_ha] = EMSdevice::uom_to_string(uom); // use default
|
auto uom_str = EMSdevice::uom_to_string(uom);
|
||||||
|
if (strlen(uom_str)) {
|
||||||
|
doc[uom_ha] = uom_str;
|
||||||
|
}
|
||||||
} else if (discovery_type() != discoveryType::HOMEASSISTANT) {
|
} else if (discovery_type() != discoveryType::HOMEASSISTANT) {
|
||||||
doc[uom_ha] = " "; // Domoticz uses " " for a no-uom
|
doc[uom_ha] = " "; // Domoticz uses " " for a no-uom
|
||||||
}
|
}
|
||||||
@@ -1259,6 +1275,10 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8
|
|||||||
doc[sc_ha] = sc_ha_measurement;
|
doc[sc_ha] = sc_ha_measurement;
|
||||||
doc[dc_ha] = "connectivity";
|
doc[dc_ha] = "connectivity";
|
||||||
break;
|
break;
|
||||||
|
case DeviceValueUOM::TIMESTAMP:
|
||||||
|
doc[sc_ha] = sc_ha_measurement;
|
||||||
|
doc[dc_ha] = "timestamp";
|
||||||
|
break;
|
||||||
case DeviceValueUOM::MV:
|
case DeviceValueUOM::MV:
|
||||||
case DeviceValueUOM::VOLTS:
|
case DeviceValueUOM::VOLTS:
|
||||||
doc[sc_ha] = sc_ha_measurement;
|
doc[sc_ha] = sc_ha_measurement;
|
||||||
|
|||||||
@@ -643,7 +643,7 @@ void Network::findNetworks() {
|
|||||||
} else if (network_iface_ == NetIface::AP) {
|
} else if (network_iface_ == NetIface::AP) {
|
||||||
phase_ = NetPhase::AP;
|
phase_ = NetPhase::AP;
|
||||||
}
|
}
|
||||||
if (!ethernet_ever_connected && ethernet_connected()) {
|
if (!ethernet_ever_connected_ && ethernet_connected()) {
|
||||||
ethernet_ever_connected_ = true;
|
ethernet_ever_connected_ = true;
|
||||||
}
|
}
|
||||||
LOG_INFO("Network connected via %s (IP: " IPSTR ")",
|
LOG_INFO("Network connected via %s (IP: " IPSTR ")",
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
|
|
||||||
#include "shuntingYard.h"
|
#include "shuntingYard.h"
|
||||||
|
|
||||||
|
#include <WiFiClient.h>
|
||||||
|
#include <ESP_SSLClient.h>
|
||||||
|
|
||||||
namespace emsesp {
|
namespace emsesp {
|
||||||
|
|
||||||
// find tokens - optimized to reduce string allocations
|
// find tokens - optimized to reduce string allocations
|
||||||
@@ -683,6 +686,94 @@ std::string calculate(const std::string & expr) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// perform an HTTP/HTTPS request; returns the HTTP status code (0 on failure or unsupported scheme)
|
||||||
|
// the response headers are always stripped, so `result` contains only the body
|
||||||
|
int http_request(std::string url, const std::string & method, const std::string & value, JsonObjectConst headers, std::string & result) {
|
||||||
|
int httpResult = 0;
|
||||||
|
const bool is_post = value.length() || Helpers::toLower(method) == "post";
|
||||||
|
const auto lower_url = Helpers::toLower(url.c_str());
|
||||||
|
|
||||||
|
const bool is_https = lower_url.starts_with("https://");
|
||||||
|
if (!is_https && !lower_url.starts_with("http://")) {
|
||||||
|
return 0; // unsupported scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
WiFiClient * basic_client = new WiFiClient;
|
||||||
|
ESP_SSLClient * ssl_client = new ESP_SSLClient;
|
||||||
|
if (is_https) {
|
||||||
|
ssl_client->setInsecure(); // with root CA we should set here: ssl_client->setCACert(rootCACert);
|
||||||
|
// NOTE: 1 KB RX buffer is fine for small JSON-style endpoints used by the scheduler/shunting-yard,
|
||||||
|
// but it is NOT enough for servers that send full-size TLS records (>1 KB), e.g. GitHub release
|
||||||
|
// assets / large CDN responses. Such servers do not negotiate max_fragment_length, so the body
|
||||||
|
// can't be decoded and reads return 0. If this path is ever used to fetch large or CDN-hosted
|
||||||
|
// payloads, bump the RX buffer to 16384 (see uploadFirmwareURL in core/system.cpp for reference).
|
||||||
|
ssl_client->setBufferSizes(1024, 1024);
|
||||||
|
ssl_client->setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds)
|
||||||
|
}
|
||||||
|
ssl_client->setClient(basic_client, is_https); // enableSSL = false for plain HTTP
|
||||||
|
|
||||||
|
url.replace(0, is_https ? 8 : 7, "");
|
||||||
|
std::string host = url;
|
||||||
|
auto index = url.find_first_of('/');
|
||||||
|
if (index != std::string::npos) {
|
||||||
|
host = url.substr(0, index);
|
||||||
|
url.replace(0, index, "");
|
||||||
|
} else {
|
||||||
|
url = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint16_t port = is_https ? 443 : 80;
|
||||||
|
if (ssl_client->connect(host.c_str(), port)) {
|
||||||
|
bool content_set = false;
|
||||||
|
ssl_client->print(is_post ? "POST " : "GET ");
|
||||||
|
ssl_client->print(url.c_str());
|
||||||
|
ssl_client->println(" HTTP/1.1");
|
||||||
|
ssl_client->print("Host: ");
|
||||||
|
ssl_client->println(host.c_str());
|
||||||
|
for (JsonPairConst p : headers) {
|
||||||
|
content_set |= (Helpers::toLower(p.key().c_str()) == "content-type");
|
||||||
|
ssl_client->print(p.key().c_str());
|
||||||
|
ssl_client->print(": ");
|
||||||
|
ssl_client->println(p.value().as<std::string>().c_str());
|
||||||
|
}
|
||||||
|
if (is_post) {
|
||||||
|
if (!content_set) {
|
||||||
|
ssl_client->print("Content-Type: ");
|
||||||
|
ssl_client->println(value.starts_with('{') ? asyncsrv::T_application_json : asyncsrv::T_text_plain);
|
||||||
|
}
|
||||||
|
ssl_client->print("Content-Length: ");
|
||||||
|
ssl_client->println(value.length());
|
||||||
|
ssl_client->println("Connection: close");
|
||||||
|
ssl_client->print("\r\n");
|
||||||
|
ssl_client->print(value.c_str());
|
||||||
|
} else {
|
||||||
|
ssl_client->println("Connection: close");
|
||||||
|
}
|
||||||
|
auto ms = millis();
|
||||||
|
while (ssl_client->connected() && !ssl_client->available() && millis() - ms < 3000) {
|
||||||
|
delay(0);
|
||||||
|
}
|
||||||
|
while (ssl_client->available()) {
|
||||||
|
result += (char)ssl_client->read();
|
||||||
|
}
|
||||||
|
ssl_client->stop();
|
||||||
|
index = result.find_first_of(' ');
|
||||||
|
if (index != std::string::npos) {
|
||||||
|
httpResult = stoi(result.substr(index + 1, 3));
|
||||||
|
}
|
||||||
|
index = result.find("\r\n\r\n");
|
||||||
|
if (index != std::string::npos) {
|
||||||
|
result.replace(0, index + 4, "");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EMSESP::logger().warning("%s connection failed", is_https ? "HTTPS" : "HTTP");
|
||||||
|
}
|
||||||
|
delete ssl_client;
|
||||||
|
delete basic_client;
|
||||||
|
|
||||||
|
return httpResult;
|
||||||
|
}
|
||||||
|
|
||||||
// check for multiple instances of <cond> ? <expr1> : <expr2>
|
// check for multiple instances of <cond> ? <expr1> : <expr2>
|
||||||
std::string compute(const std::string & expr) {
|
std::string compute(const std::string & expr) {
|
||||||
std::string expr_new = expr;
|
std::string expr_new = expr;
|
||||||
@@ -723,119 +814,10 @@ std::string compute(const std::string & expr) {
|
|||||||
keys_s = p.key().c_str();
|
keys_s = p.key().c_str();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bool content_set = false;
|
std::string value = doc[value_s] | "";
|
||||||
std::string value = doc[value_s] | "";
|
std::string method = doc[method_s] | "GET";
|
||||||
std::string method = doc[method_s] | "GET";
|
|
||||||
if (value.length()) {
|
|
||||||
method = "POST";
|
|
||||||
}
|
|
||||||
std::string result;
|
std::string result;
|
||||||
int httpResult = 0;
|
int httpResult = http_request(url, method, value, doc[header_s].as<JsonObjectConst>(), result);
|
||||||
#ifndef NO_TLS_SUPPORT
|
|
||||||
if (Helpers::toLower(url.c_str()).starts_with("https://")) {
|
|
||||||
WiFiClient * basic_client = new WiFiClient;
|
|
||||||
ESP_SSLClient * ssl_client = new ESP_SSLClient;
|
|
||||||
ssl_client->setInsecure(); // with root CA we should set here: ssl_client->setCACert(rootCACert);
|
|
||||||
ssl_client->setBufferSizes(1024, 1024);
|
|
||||||
ssl_client->setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds)
|
|
||||||
url.replace(0, 8, "");
|
|
||||||
std::string host = url;
|
|
||||||
auto index = url.find_first_of('/');
|
|
||||||
if (index != std::string::npos) {
|
|
||||||
host = url.substr(0, index);
|
|
||||||
url.replace(0, index, "");
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
index = host.find_first_of('@');
|
|
||||||
std::string auth;
|
|
||||||
if (index != std::string::npos) {
|
|
||||||
auth = base64::encode(host.substr(0, index));
|
|
||||||
host.replace(0, index, "");
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
ssl_client->setClient(basic_client);
|
|
||||||
if (ssl_client->connect(host.c_str(), 443)) {
|
|
||||||
if (value.length() || Helpers::toLower(method) == "post") {
|
|
||||||
ssl_client->print("POST ");
|
|
||||||
ssl_client->print(url.c_str());
|
|
||||||
ssl_client->println(" HTTP/1.1");
|
|
||||||
ssl_client->print("Host: ");
|
|
||||||
ssl_client->println(host.c_str());
|
|
||||||
for (JsonPair p : doc[header_s].as<JsonObject>()) {
|
|
||||||
content_set |= (emsesp::Helpers::toLower(p.key().c_str()) == "content-type");
|
|
||||||
ssl_client->print(p.key().c_str());
|
|
||||||
ssl_client->print(": ");
|
|
||||||
ssl_client->println(p.value().as<std::string>().c_str());
|
|
||||||
}
|
|
||||||
if (!content_set) {
|
|
||||||
ssl_client->print("Content-Type: ");
|
|
||||||
if (value.starts_with('{')) {
|
|
||||||
ssl_client->println(asyncsrv::T_application_json);
|
|
||||||
} else {
|
|
||||||
ssl_client->println(asyncsrv::T_text_plain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ssl_client->print("Content-Length: ");
|
|
||||||
ssl_client->println(value.length());
|
|
||||||
ssl_client->println("Connection: close");
|
|
||||||
ssl_client->print("\r\n");
|
|
||||||
ssl_client->print(value.c_str());
|
|
||||||
} else {
|
|
||||||
ssl_client->print("GET ");
|
|
||||||
ssl_client->print(url.c_str());
|
|
||||||
ssl_client->println(" HTTP/1.1");
|
|
||||||
ssl_client->print("Host: ");
|
|
||||||
ssl_client->println(host.c_str());
|
|
||||||
for (JsonPair p : doc[header_s].as<JsonObject>()) {
|
|
||||||
ssl_client->print(p.key().c_str());
|
|
||||||
ssl_client->print(": ");
|
|
||||||
ssl_client->println(p.value().as<std::string>().c_str());
|
|
||||||
}
|
|
||||||
ssl_client->println("Connection: close");
|
|
||||||
}
|
|
||||||
auto ms = millis();
|
|
||||||
while (!ssl_client->available() && millis() - ms < 3000) {
|
|
||||||
delay(0);
|
|
||||||
}
|
|
||||||
while (ssl_client->available()) {
|
|
||||||
result += (char)ssl_client->read();
|
|
||||||
}
|
|
||||||
ssl_client->stop();
|
|
||||||
index = result.find_first_of(' ');
|
|
||||||
if (index != std::string::npos) {
|
|
||||||
httpResult = stoi(result.substr(index + 1, 3));
|
|
||||||
}
|
|
||||||
index = result.find("\r\n\r\n");
|
|
||||||
if (index != std::string::npos) {
|
|
||||||
result.replace(0, index + 4, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete ssl_client;
|
|
||||||
delete basic_client;
|
|
||||||
} else
|
|
||||||
#endif
|
|
||||||
if (Helpers::toLower(url.c_str()).starts_with("http://")) {
|
|
||||||
HTTPClient * http = new HTTPClient;
|
|
||||||
if (http->begin(url.c_str())) {
|
|
||||||
for (JsonPair p : doc[header_s].as<JsonObject>()) {
|
|
||||||
http->addHeader(p.key().c_str(), p.value().as<std::string>().c_str());
|
|
||||||
content_set |= (emsesp::Helpers::toLower(p.key().c_str()) == "content-type");
|
|
||||||
}
|
|
||||||
if (value.length() || Helpers::toLower(method) == "post") {
|
|
||||||
if (!content_set) {
|
|
||||||
http->addHeader("Content-Type", value.starts_with('{') ? asyncsrv::T_application_json : asyncsrv::T_text_plain);
|
|
||||||
}
|
|
||||||
httpResult = http->POST(value.c_str());
|
|
||||||
} else {
|
|
||||||
httpResult = http->GET(); // normal GET
|
|
||||||
}
|
|
||||||
if (httpResult > 0) {
|
|
||||||
result = http->getString().c_str();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
http->end();
|
|
||||||
delete http;
|
|
||||||
}
|
|
||||||
if (httpResult == 200) {
|
if (httpResult == 200) {
|
||||||
std::string key = doc[key_s] | "";
|
std::string key = doc[key_s] | "";
|
||||||
JsonDocument keys_doc; // JsonDocument to hold "keys" after doc is parsed with HTTP body
|
JsonDocument keys_doc; // JsonDocument to hold "keys" after doc is parsed with HTTP body
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
#ifndef EMSESP_SHUNTING_YARD_H_
|
#ifndef EMSESP_SHUNTING_YARD_H_
|
||||||
#define EMSESP_SHUNTING_YARD_H_
|
#define EMSESP_SHUNTING_YARD_H_
|
||||||
|
|
||||||
#include <HTTPClient.h>
|
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -85,6 +84,8 @@ std::string calculate(const std::string & expr);
|
|||||||
// check for multiple instances of <cond> ? <expr1> : <expr2>
|
// check for multiple instances of <cond> ? <expr1> : <expr2>
|
||||||
std::string compute(const std::string & expr);
|
std::string compute(const std::string & expr);
|
||||||
|
|
||||||
|
int http_request(std::string url, const std::string & method, const std::string & value, JsonObjectConst headers, std::string & result);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace emsesp
|
} // namespace emsesp
|
||||||
@@ -29,7 +29,6 @@
|
|||||||
#include <nvs.h>
|
#include <nvs.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <HTTPClient.h>
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
#include "firmwareVersion.h"
|
#include "firmwareVersion.h"
|
||||||
@@ -38,7 +37,7 @@
|
|||||||
#include "../test/test.h"
|
#include "../test/test.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef NO_TLS_SUPPORT
|
#ifndef EMSESP_STANDALONE
|
||||||
#define ENABLE_SMTP
|
#define ENABLE_SMTP
|
||||||
#define USE_ESP_SSLCLIENT
|
#define USE_ESP_SSLCLIENT
|
||||||
#define READYCLIENT_SSL_CLIENT ESP_SSLClient
|
#define READYCLIENT_SSL_CLIENT ESP_SSLClient
|
||||||
@@ -138,7 +137,7 @@ bool System::command_sendmail(const char * value, const int8_t id) {
|
|||||||
|
|
||||||
bool success = false;
|
bool success = false;
|
||||||
|
|
||||||
#ifndef NO_TLS_SUPPORT
|
#ifndef EMSESP_STANDALONE
|
||||||
WiFiClient * basic_client = new WiFiClient;
|
WiFiClient * basic_client = new WiFiClient;
|
||||||
ESP_SSLClient * ssl_client = new ESP_SSLClient;
|
ESP_SSLClient * ssl_client = new ESP_SSLClient;
|
||||||
ReadyClient * r_client = new ReadyClient(*ssl_client);
|
ReadyClient * r_client = new ReadyClient(*ssl_client);
|
||||||
@@ -1523,14 +1522,27 @@ bool System::check_upgrade() {
|
|||||||
// changes going to v3.9 from an earlier version
|
// changes going to v3.9 from an earlier version
|
||||||
if (settings_version.major() == 3 && settings_version.minor() < 9) {
|
if (settings_version.major() == 3 && settings_version.minor() < 9) {
|
||||||
#ifndef EMSESP_STANDALONE
|
#ifndef EMSESP_STANDALONE
|
||||||
|
// AP_MODE_ALWAYS has been removed
|
||||||
EMSESP::esp32React.getAPSettingsService()->update([&](APSettings & apSettings) {
|
EMSESP::esp32React.getAPSettingsService()->update([&](APSettings & apSettings) {
|
||||||
if (apSettings.provisionMode == 0) {
|
if (apSettings.provisionMode == 0) {
|
||||||
apSettings.provisionMode = AP_MODE_DISCONNECTED; // AP_MODE_ALWAYS has been removed
|
apSettings.provisionMode = AP_MODE_DISCONNECTED; // AP_MODE_DISCONNECTED is the new default
|
||||||
LOG_INFO("Upgrade: Setting AP provision mode to auto");
|
LOG_INFO("Upgrade: Setting AP provision mode to auto");
|
||||||
return StateUpdateResult::CHANGED;
|
return StateUpdateResult::CHANGED;
|
||||||
}
|
}
|
||||||
return StateUpdateResult::UNCHANGED;
|
return StateUpdateResult::UNCHANGED;
|
||||||
});
|
});
|
||||||
|
// Scheduler name is now mandatory, update FS
|
||||||
|
uint8_t i = 0;
|
||||||
|
bool schedule_changed = false;
|
||||||
|
EMSESP::webSchedulerService.update([&](WebScheduler & scheduler) {
|
||||||
|
for (ScheduleItem & scheduleItem : scheduler.scheduleItems) {
|
||||||
|
if (scheduleItem.name[0] == '\0') {
|
||||||
|
snprintf(scheduleItem.name, sizeof(scheduleItem.name), "schedule_%d", i++);
|
||||||
|
schedule_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schedule_changed ? StateUpdateResult::CHANGED : StateUpdateResult::UNCHANGED;
|
||||||
|
});
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2956,65 +2968,252 @@ bool System::uploadFirmwareURL(const char * url) {
|
|||||||
|
|
||||||
Shell::loop_all(); // flush log buffers so latest messages are shown in console
|
Shell::loop_all(); // flush log buffers so latest messages are shown in console
|
||||||
|
|
||||||
// Configure temporary client
|
String scheme = saved_url.substring(0, 8);
|
||||||
HTTPClient http;
|
scheme.toLowerCase();
|
||||||
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); // important for GitHub 302's
|
const bool is_https = scheme.startsWith("https://");
|
||||||
http.setTimeout(8000);
|
const int scheme_len = is_https ? 8 : 7; // "https://" vs "http://"
|
||||||
http.useHTTP10(true); // use HTTP/1.0 for update since the update handler does not support any transfer Encoding
|
|
||||||
http.begin(saved_url);
|
|
||||||
|
|
||||||
// start a connection, returns -1 if fails
|
WiFiClient basic_client;
|
||||||
int httpCode = http.GET();
|
ESP_SSLClient ssl_client;
|
||||||
if (httpCode != HTTP_CODE_OK) {
|
|
||||||
LOG_ERROR("Firmware upload failed - HTTP code %d", httpCode);
|
Stream * stream = nullptr;
|
||||||
http.end();
|
int firmware_size = 0;
|
||||||
return false; // error
|
|
||||||
|
if (is_https) {
|
||||||
|
ssl_client.setInsecure(); // no CA validation, matches the rest of the project
|
||||||
|
// BearSSL needs a receive buffer large enough to hold one full TLS record.
|
||||||
|
// GitHub's release-assets CDN sends standard up-to-16 KB records and does NOT
|
||||||
|
// negotiate max_fragment_length, so a small (e.g. 1 KB) RX buffer makes the
|
||||||
|
// body unreadable (headers still fit one small record, hence Content-Length
|
||||||
|
// looks fine, but the first body record cannot be decoded). 16384 + overhead
|
||||||
|
// is the safe value the library itself uses by default; we go a bit smaller
|
||||||
|
// to be friendlier to 4 MB / no-PSRAM boards while still big enough for any
|
||||||
|
// record the CDN actually sends in practice.
|
||||||
|
ssl_client.setBufferSizes(16384, 1024);
|
||||||
|
ssl_client.setSessionTimeout(120);
|
||||||
|
}
|
||||||
|
basic_client.setTimeout(15000); // socket-level read timeout
|
||||||
|
ssl_client.setTimeout(15000); // Stream::readBytes timeout used by Update
|
||||||
|
ssl_client.setClient(&basic_client, is_https); // enableSSL = false for plain HTTP
|
||||||
|
|
||||||
|
const uint16_t port = is_https ? 443 : 80;
|
||||||
|
String url_remain = saved_url.substring(scheme_len);
|
||||||
|
int redirect_count = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// split url_remain into host and path
|
||||||
|
String host;
|
||||||
|
String path;
|
||||||
|
int s = url_remain.indexOf('/');
|
||||||
|
if (s < 0) {
|
||||||
|
host = url_remain;
|
||||||
|
path = "/";
|
||||||
|
} else {
|
||||||
|
host = url_remain.substring(0, s);
|
||||||
|
path = url_remain.substring(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("Connecting to %s", host.c_str());
|
||||||
|
if (!ssl_client.connect(host.c_str(), port)) {
|
||||||
|
LOG_ERROR("Firmware upload failed - connection failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// send a minimal HTTP/1.0 GET so we don't have to deal with chunked encoding
|
||||||
|
ssl_client.print("GET ");
|
||||||
|
ssl_client.print(path);
|
||||||
|
ssl_client.println(" HTTP/1.0");
|
||||||
|
ssl_client.print("Host: ");
|
||||||
|
ssl_client.println(host);
|
||||||
|
ssl_client.println("User-Agent: EMS-ESP");
|
||||||
|
ssl_client.println("Connection: close");
|
||||||
|
ssl_client.print("\r\n");
|
||||||
|
|
||||||
|
// wait for the first byte (up to 8s, matching the previous HTTP timeout)
|
||||||
|
uint32_t ms = millis();
|
||||||
|
while (ssl_client.connected() && !ssl_client.available() && millis() - ms < 8000) {
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse status line: "HTTP/1.x CODE TEXT"
|
||||||
|
String status_line = ssl_client.readStringUntil('\n');
|
||||||
|
int sp = status_line.indexOf(' ');
|
||||||
|
int http_code = (sp >= 0) ? status_line.substring(sp + 1, sp + 4).toInt() : 0;
|
||||||
|
|
||||||
|
// parse response headers, looking for Content-Length and Location
|
||||||
|
int content_length = -1;
|
||||||
|
String location;
|
||||||
|
while (ssl_client.connected() || ssl_client.available()) {
|
||||||
|
String line = ssl_client.readStringUntil('\n');
|
||||||
|
line.trim();
|
||||||
|
if (line.isEmpty()) {
|
||||||
|
break; // end of headers
|
||||||
|
}
|
||||||
|
int colon = line.indexOf(':');
|
||||||
|
if (colon < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String name = line.substring(0, colon);
|
||||||
|
name.toLowerCase();
|
||||||
|
String val = line.substring(colon + 1);
|
||||||
|
val.trim();
|
||||||
|
if (name == "content-length") {
|
||||||
|
content_length = val.toInt();
|
||||||
|
} else if (name == "location") {
|
||||||
|
location = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// follow redirects manually (GitHub releases redirect to objects.githubusercontent.com)
|
||||||
|
if (http_code == 301 || http_code == 302 || http_code == 303 || http_code == 307 || http_code == 308) {
|
||||||
|
ssl_client.stop();
|
||||||
|
if (location.isEmpty() || ++redirect_count > 5) {
|
||||||
|
LOG_ERROR("Firmware upload failed - too many redirects");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String lower_loc = location;
|
||||||
|
lower_loc.toLowerCase();
|
||||||
|
if (lower_loc.startsWith("https://") || lower_loc.startsWith("http://")) {
|
||||||
|
// scheme-changing redirect is not supported - the SSL state is
|
||||||
|
// baked in at setClient() time and we don't want to re-init mid-flight
|
||||||
|
const bool new_is_https = lower_loc.startsWith("https://");
|
||||||
|
if (new_is_https != is_https) {
|
||||||
|
LOG_ERROR("Firmware upload failed - cross-scheme redirect to %s", location.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
url_remain = location.substring(new_is_https ? 8 : 7);
|
||||||
|
} else if (location.startsWith("/")) {
|
||||||
|
url_remain = host + location; // relative redirect, same host
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("Firmware upload failed - unsupported redirect to %s", location.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LOG_DEBUG("Following redirect to %s", url_remain.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (http_code != 200) {
|
||||||
|
ssl_client.stop();
|
||||||
|
LOG_ERROR("Firmware upload failed - HTTP code %d", http_code);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content_length <= 0) {
|
||||||
|
ssl_client.stop();
|
||||||
|
LOG_ERROR("Firmware upload failed - missing Content-Length");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for the first byte of the body so the read loop sees real data
|
||||||
|
// (headers and body may arrive in separate TLS records)
|
||||||
|
uint32_t body_wait = millis();
|
||||||
|
while (ssl_client.connected() && !ssl_client.available() && millis() - body_wait < 8000) {
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
if (!ssl_client.available()) {
|
||||||
|
ssl_client.stop();
|
||||||
|
LOG_ERROR("Firmware upload failed - no body received");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream = &ssl_client;
|
||||||
|
firmware_size = content_length;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
int firmware_size = http.getSize();
|
|
||||||
|
|
||||||
// check we have a valid size
|
// check we have a valid size
|
||||||
if (firmware_size < 2097152) { // 2MB or greater is required
|
if (firmware_size < 1677721) { // 1.6MB or greater is required
|
||||||
LOG_ERROR("Firmware upload failed - invalid size");
|
LOG_ERROR("Firmware upload failed - invalid size");
|
||||||
http.end();
|
|
||||||
return false; // error
|
return false; // error
|
||||||
}
|
}
|
||||||
|
|
||||||
// check we have enough space for the upload in the ota partition
|
// check we have enough space for the upload in the ota partition
|
||||||
if (!Update.begin(firmware_size)) {
|
if (!Update.begin(firmware_size)) {
|
||||||
LOG_ERROR("Firmware upload failed - no space");
|
LOG_ERROR("Firmware upload failed - no space");
|
||||||
http.end();
|
|
||||||
return false; // error
|
return false; // error
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Firmware uploading (size: %d KB). Please wait...", firmware_size / 1024);
|
LOG_INFO("Firmware uploading (size: %d KB) over %s. Please wait...", firmware_size / 1024, is_https ? "HTTPS" : "HTTP");
|
||||||
|
|
||||||
Shell::loop_all(); // flush log buffers so latest messages are shown in console
|
Shell::loop_all(); // flush log buffers so latest messages are shown in console
|
||||||
|
|
||||||
// we're about to start the upload, set the status so the Web System Monitor spots it
|
// we're about to start the upload, set the status so the Web System Monitor spots it
|
||||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING);
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING);
|
||||||
|
|
||||||
// set a callback so we can monitor progress in the WebUI
|
// explicit chunked read loop instead of Update.writeStream():
|
||||||
Update.onProgress([](size_t progress, size_t total) { EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING + (progress * 100 / total)); });
|
constexpr size_t CHUNK_SIZE = 1024;
|
||||||
|
constexpr uint32_t READ_TIMEOUT_MS = 30000; // overall stall timeout per chunk
|
||||||
|
uint8_t buf[CHUNK_SIZE];
|
||||||
|
size_t total_read = 0;
|
||||||
|
bool magic_ok = false;
|
||||||
|
int last_pct = -1;
|
||||||
|
|
||||||
// get tcp stream and send it to Updater
|
while (total_read < (size_t)firmware_size) {
|
||||||
WiFiClient * stream = http.getStreamPtr();
|
// wait for some data or for the connection to drop
|
||||||
if (Update.writeStream(*stream) != firmware_size) {
|
uint32_t wait_start = millis();
|
||||||
LOG_ERROR("Firmware upload failed - size differences");
|
while (!stream->available()) {
|
||||||
http.end();
|
if (!ssl_client.connected()) {
|
||||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
break;
|
||||||
return false; // error
|
}
|
||||||
|
if (millis() - wait_start > READ_TIMEOUT_MS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stream->available()) {
|
||||||
|
LOG_ERROR("Firmware upload failed - read stalled at %u of %d bytes", (unsigned)total_read, firmware_size);
|
||||||
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t want = (size_t)firmware_size - total_read;
|
||||||
|
if (want > CHUNK_SIZE) {
|
||||||
|
want = CHUNK_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t n = stream->readBytes(buf, want);
|
||||||
|
if (n == 0) {
|
||||||
|
LOG_ERROR("Firmware upload failed - read returned 0 at %u of %d bytes", (unsigned)total_read, firmware_size);
|
||||||
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the ESP image magic byte the very first time so we fail fast with a
|
||||||
|
// clear message if the URL points at the wrong asset (HTML, archive, ...)
|
||||||
|
if (!magic_ok) {
|
||||||
|
if (buf[0] != 0xE9) {
|
||||||
|
LOG_ERROR("Firmware upload failed - bad magic byte 0x%02X (expected 0xE9, not an ESP32 firmware image?)", buf[0]);
|
||||||
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
magic_ok = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Update.write(buf, n) != n) {
|
||||||
|
LOG_ERROR("Firmware upload failed - flash write error at %u of %d bytes: %s", (unsigned)total_read, firmware_size, Update.errorString());
|
||||||
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
total_read += n;
|
||||||
|
|
||||||
|
// update the WebUI status, but only when the percentage actually changes
|
||||||
|
int pct = (int)(total_read * 100 / (size_t)firmware_size);
|
||||||
|
if (pct != last_pct) {
|
||||||
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING + pct);
|
||||||
|
last_pct = pct;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Update.end(true)) {
|
if (!Update.end(true)) {
|
||||||
LOG_ERROR("Firmware upload failed - general error");
|
LOG_ERROR("Firmware upload failed - %s", Update.errorString());
|
||||||
http.end();
|
|
||||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||||
return false; // error
|
return false; // error
|
||||||
}
|
}
|
||||||
|
|
||||||
// finished with upload
|
|
||||||
http.end();
|
|
||||||
saved_url.clear(); // prevent from downloading again
|
saved_url.clear(); // prevent from downloading again
|
||||||
LOG_INFO("Firmware uploaded successfully. Restarting...");
|
LOG_INFO("Firmware uploaded successfully. Restarting...");
|
||||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART);
|
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART);
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
#define EMSESP_APP_VERSION "3.9.0-dev.9"
|
#define EMSESP_APP_VERSION "3.9.0-dev.10"
|
||||||
|
|||||||
@@ -478,7 +478,7 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// show scheduler, with name, on/off
|
// show scheduler, with name, on/off, unless it's of type SCHEDULE_IMMEDIATE
|
||||||
if (EMSESP::webSchedulerService.count_entities(true)) {
|
if (EMSESP::webSchedulerService.count_entities(true)) {
|
||||||
JsonObject obj = nodes.add<JsonObject>();
|
JsonObject obj = nodes.add<JsonObject>();
|
||||||
obj["id"] = EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID; // it's unique id
|
obj["id"] = EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID; // it's unique id
|
||||||
@@ -488,8 +488,8 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) {
|
|||||||
|
|
||||||
EMSESP::webSchedulerService.read([&](const WebScheduler & webScheduler) {
|
EMSESP::webSchedulerService.read([&](const WebScheduler & webScheduler) {
|
||||||
for (const ScheduleItem & scheduleItem : webScheduler.scheduleItems) {
|
for (const ScheduleItem & scheduleItem : webScheduler.scheduleItems) {
|
||||||
// only add if we have a name - we don't need a u (UOM) for this
|
// only add if we have a name and it's not of type SCHEDULE_IMMEDIATE - we don't need a u (UOM) for this
|
||||||
if (scheduleItem.name[0] != '\0') {
|
if (scheduleItem.name[0] != '\0' && scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
|
||||||
JsonObject node = nodes.add<JsonObject>();
|
JsonObject node = nodes.add<JsonObject>();
|
||||||
node["id"] = (EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID * 100) + count++;
|
node["id"] = (EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID * 100) + count++;
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu
|
|||||||
EMSESP::webSchedulerService.ha_reset();
|
EMSESP::webSchedulerService.ha_reset();
|
||||||
|
|
||||||
// build up the list of schedule items
|
// build up the list of schedule items
|
||||||
auto scheduleItems = root["schedule"].as<JsonArray>();
|
auto scheduleItems = root["schedule"].as<JsonArray>();
|
||||||
for (const JsonObject schedule : scheduleItems) {
|
for (const JsonObject schedule : scheduleItems) {
|
||||||
// create each schedule item, overwriting any previous settings
|
// create each schedule item, overwriting any previous settings
|
||||||
// ignore the id (as this is only used in the web for table rendering)
|
// ignore the id (as this is only used in the web for table rendering)
|
||||||
@@ -137,6 +137,7 @@ bool WebSchedulerService::command_setvalue(const char * value, const int8_t id,
|
|||||||
if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
||||||
publish();
|
publish();
|
||||||
}
|
}
|
||||||
|
|
||||||
// save new state to nvs #2946
|
// save new state to nvs #2946
|
||||||
if (scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
|
if (scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
|
||||||
char key[sizeof(scheduleItem.name) + 2];
|
char key[sizeof(scheduleItem.name) + 2];
|
||||||
@@ -343,6 +344,7 @@ uint8_t WebSchedulerService::count_entities(bool cmd_only) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// execute scheduled command
|
// execute scheduled command
|
||||||
|
// return true if successful, false if not
|
||||||
bool WebSchedulerService::command(const char * name, const std::string & command, const std::string & data) {
|
bool WebSchedulerService::command(const char * name, const std::string & command, const std::string & data) {
|
||||||
std::string cmd = Helpers::toLower(command);
|
std::string cmd = Helpers::toLower(command);
|
||||||
|
|
||||||
@@ -365,130 +367,10 @@ bool WebSchedulerService::command(const char * name, const std::string & command
|
|||||||
std::string value = doc["value"] | data; // extract value if its in the command, or take the data
|
std::string value = doc["value"] | data; // extract value if its in the command, or take the data
|
||||||
std::string method = doc["method"] | "GET"; // default GET
|
std::string method = doc["method"] | "GET"; // default GET
|
||||||
commands(value, false);
|
commands(value, false);
|
||||||
if (value.length()) {
|
auto lower_url = Helpers::toLower(url.c_str());
|
||||||
method = "POST";
|
if (lower_url.starts_with("http://") || lower_url.starts_with("https://")) {
|
||||||
}
|
std::string result;
|
||||||
std::string result;
|
int httpResult = http_request(url, method, value, doc["header"].as<JsonObjectConst>(), result);
|
||||||
int httpResult = 0;
|
|
||||||
#ifndef NO_TLS_SUPPORT
|
|
||||||
if (Helpers::toLower(url.c_str()).starts_with("https://")) {
|
|
||||||
WiFiClient * basic_client = new WiFiClient;
|
|
||||||
ESP_SSLClient * ssl_client = new ESP_SSLClient;
|
|
||||||
ssl_client->setInsecure(); // with root CA we should set here: ssl_client->setCACert(rootCACert);
|
|
||||||
ssl_client->setBufferSizes(1024, 1024);
|
|
||||||
ssl_client->setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds)
|
|
||||||
url.replace(0, 8, "");
|
|
||||||
std::string host = url;
|
|
||||||
auto index = url.find_first_of('/');
|
|
||||||
if (index != std::string::npos) {
|
|
||||||
host = url.substr(0, index);
|
|
||||||
url.replace(0, index, "");
|
|
||||||
}
|
|
||||||
// EMSESP::logger().debug("Host: %s, URL: %s", host.c_str(), url.c_str());
|
|
||||||
ssl_client->setClient(basic_client);
|
|
||||||
if (ssl_client->connect(host.c_str(), 443)) {
|
|
||||||
if (value.length() || Helpers::toLower(method) == "post") {
|
|
||||||
// EMSESP::logger().debug("POST %s HTTP/1.1", url.c_str());
|
|
||||||
ssl_client->print("POST ");
|
|
||||||
ssl_client->print(url.c_str());
|
|
||||||
ssl_client->println(" HTTP/1.1");
|
|
||||||
ssl_client->print("Host: ");
|
|
||||||
ssl_client->println(host.c_str());
|
|
||||||
bool content_set = false;
|
|
||||||
for (JsonPair p : doc["header"].as<JsonObject>()) {
|
|
||||||
content_set |= (emsesp::Helpers::toLower(p.key().c_str()) == "content-type");
|
|
||||||
ssl_client->print(p.key().c_str());
|
|
||||||
ssl_client->print(": ");
|
|
||||||
ssl_client->println(p.value().as<std::string>().c_str());
|
|
||||||
}
|
|
||||||
if (!content_set) {
|
|
||||||
ssl_client->print("Content-Type: ");
|
|
||||||
if (value.starts_with('{')) {
|
|
||||||
ssl_client->println(asyncsrv::T_application_json);
|
|
||||||
} else {
|
|
||||||
ssl_client->println(asyncsrv::T_text_plain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ssl_client->print("Content-Length: ");
|
|
||||||
ssl_client->println(value.length());
|
|
||||||
ssl_client->println("Connection: close");
|
|
||||||
ssl_client->print("\r\n");
|
|
||||||
ssl_client->print(value.c_str());
|
|
||||||
} else {
|
|
||||||
// EMSESP::logger().debug("GET %s HTTP/1.1", url.c_str());
|
|
||||||
ssl_client->print("GET ");
|
|
||||||
ssl_client->print(url.c_str());
|
|
||||||
ssl_client->println(" HTTP/1.1");
|
|
||||||
ssl_client->print("Host: ");
|
|
||||||
ssl_client->println(host.c_str());
|
|
||||||
for (JsonPair p : doc["header"].as<JsonObject>()) {
|
|
||||||
ssl_client->print(p.key().c_str());
|
|
||||||
ssl_client->print(": ");
|
|
||||||
ssl_client->println(p.value().as<std::string>().c_str());
|
|
||||||
}
|
|
||||||
ssl_client->println("Connection: close");
|
|
||||||
}
|
|
||||||
auto ms = millis();
|
|
||||||
while (ssl_client->connected() && !ssl_client->available() && millis() - ms < 3000) {
|
|
||||||
delay(0);
|
|
||||||
}
|
|
||||||
while (ssl_client->available()) {
|
|
||||||
result += (char)ssl_client->read();
|
|
||||||
}
|
|
||||||
ssl_client->stop();
|
|
||||||
// EMSESP::logger().debug("HTTPS response: %s", result.c_str());
|
|
||||||
index = result.find_first_of(' ');
|
|
||||||
if (index != std::string::npos) {
|
|
||||||
httpResult = stoi(result.substr(index + 1, 3));
|
|
||||||
// EMSESP::logger().debug("HTTPS code: %i", httpResult);
|
|
||||||
}
|
|
||||||
index = result.find("\r\n\r\n");
|
|
||||||
if (index != std::string::npos) {
|
|
||||||
result.replace(0, index + 4, "");
|
|
||||||
// EMSESP::logger().debug("HTTPS response: %s", result.c_str());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
EMSESP::logger().warning("HTTPS connection failed");
|
|
||||||
}
|
|
||||||
delete ssl_client;
|
|
||||||
delete basic_client;
|
|
||||||
// check HTTP return code
|
|
||||||
if (httpResult != 200) {
|
|
||||||
EMSESP::logger().warning("Schedule '%s': URL command failed with http code %d", name, httpResult);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} else
|
|
||||||
#endif
|
|
||||||
if (Helpers::toLower(url.c_str()).starts_with("http://")) {
|
|
||||||
HTTPClient * http = new HTTPClient;
|
|
||||||
if (http->begin(url.c_str())) {
|
|
||||||
bool content_set = false;
|
|
||||||
for (JsonPair p : doc["header"].as<JsonObject>()) {
|
|
||||||
http->addHeader(p.key().c_str(), p.value().as<std::string>().c_str());
|
|
||||||
content_set |= p.key() == "content-type";
|
|
||||||
}
|
|
||||||
// if there is data, force a POST
|
|
||||||
if (Helpers::toLower(method) == "post") { // we have all lowercase
|
|
||||||
if (!content_set) {
|
|
||||||
// http->addHeader("Content-Type", value.find_first_of('{') != std::string::npos ? "application/json" : "text/plain");
|
|
||||||
if (value.starts_with('{')) {
|
|
||||||
http->addHeader(asyncsrv::T_Content_Type, asyncsrv::T_application_json, false); // auto-set to JSON
|
|
||||||
} else {
|
|
||||||
http->addHeader(asyncsrv::T_Content_Type, asyncsrv::T_text_plain, false); // auto-set to JSON
|
|
||||||
}
|
|
||||||
}
|
|
||||||
httpResult = http->POST(value.c_str());
|
|
||||||
} else {
|
|
||||||
httpResult = http->GET(); // normal GET
|
|
||||||
if (httpResult > 0) {
|
|
||||||
result = http->getString().c_str();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
http->end();
|
|
||||||
delete http;
|
|
||||||
// check HTTP return code
|
|
||||||
if (httpResult != 200) {
|
if (httpResult != 200) {
|
||||||
EMSESP::logger().warning("Schedule '%s': URL command failed with http code %d", name, httpResult);
|
EMSESP::logger().warning("Schedule '%s': URL command failed with http code %d", name, httpResult);
|
||||||
return false;
|
return false;
|
||||||
@@ -598,7 +480,7 @@ void WebSchedulerService::loop() {
|
|||||||
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||||
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
|
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
|
||||||
command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()));
|
command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()));
|
||||||
scheduleItem.active = false;
|
// scheduleItem.active = false;
|
||||||
publish_single(scheduleItem.name, false);
|
publish_single(scheduleItem.name, false);
|
||||||
if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
||||||
publish();
|
publish();
|
||||||
@@ -659,6 +541,18 @@ void WebSchedulerService::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// execute a schedule item immediately
|
||||||
|
bool WebSchedulerService::executeSchedule(const char * name) {
|
||||||
|
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||||
|
if (scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE && strcmp(scheduleItem.name, name) == 0) {
|
||||||
|
EMSESP::logger().info("Executing schedule '%s'", name);
|
||||||
|
return command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EMSESP::logger().warning("Schedule '%s' not found", name);
|
||||||
|
return false; // not found
|
||||||
|
}
|
||||||
|
|
||||||
// process schedules async
|
// process schedules async
|
||||||
void WebSchedulerService::scheduler_task(void * pvParameters) {
|
void WebSchedulerService::scheduler_task(void * pvParameters) {
|
||||||
while (1) {
|
while (1) {
|
||||||
@@ -681,11 +575,11 @@ void WebSchedulerService::load_test_data() {
|
|||||||
// test 1
|
// test 1
|
||||||
auto si = ScheduleItem();
|
auto si = ScheduleItem();
|
||||||
si.active = true;
|
si.active = true;
|
||||||
si.flags = 1;
|
si.flags = 1; // day schedule
|
||||||
si.time = "12:00";
|
si.time = "12:00";
|
||||||
si.cmd = "system/fetch";
|
si.cmd = "system/fetch";
|
||||||
si.value = "10";
|
si.value = "10";
|
||||||
strcpy(si.name, "test_scheduler");
|
strcpy(si.name, "test_scheduler1");
|
||||||
si.elapsed_min = 0;
|
si.elapsed_min = 0;
|
||||||
si.retry_cnt = 0xFF; // no startup retries
|
si.retry_cnt = 0xFF; // no startup retries
|
||||||
|
|
||||||
@@ -694,11 +588,11 @@ void WebSchedulerService::load_test_data() {
|
|||||||
// test 2
|
// test 2
|
||||||
si = ScheduleItem();
|
si = ScheduleItem();
|
||||||
si.active = false;
|
si.active = false;
|
||||||
si.flags = 1;
|
si.flags = SCHEDULEFLAG_SCHEDULE_IMMEDIATE; // immediate
|
||||||
si.time = "13:00";
|
si.time = "13:00";
|
||||||
si.cmd = "system/message";
|
si.cmd = "system/message";
|
||||||
si.value = "20";
|
si.value = "20";
|
||||||
strcpy(si.name, ""); // to make sure its excluded from Dashboard
|
strcpy(si.name, "test_scheduler2"); // to make sure its excluded from Dashboard
|
||||||
si.elapsed_min = 0;
|
si.elapsed_min = 0;
|
||||||
si.retry_cnt = 0xFF; // no startup retries
|
si.retry_cnt = 0xFF; // no startup retries
|
||||||
|
|
||||||
|
|||||||
@@ -38,14 +38,14 @@
|
|||||||
|
|
||||||
// bit flags for the schedule items. Matches those in interface/src/app/main/SchedulerDialog.tsx
|
// bit flags for the schedule items. Matches those in interface/src/app/main/SchedulerDialog.tsx
|
||||||
// 0-127 (0->0x7F) is day schedule
|
// 0-127 (0->0x7F) is day schedule
|
||||||
// 128/0x80 is timer
|
// 128 (0x80) is timer
|
||||||
// 129/0x81 is on change
|
// 129 (0x81) is on change
|
||||||
// 130/0x82 is on condition
|
// 130 (0x82) is on condition
|
||||||
// 132/0x84 is immediate
|
// 132 (0x84) is immediate
|
||||||
#define SCHEDULEFLAG_SCHEDULE_TIMER 0x80 // 7th bit for Timer
|
#define SCHEDULEFLAG_SCHEDULE_TIMER 0x80 // 7th bit for Timer
|
||||||
#define SCHEDULEFLAG_SCHEDULE_ONCHANGE 0x81 // 7th+1st bit for OnChange
|
#define SCHEDULEFLAG_SCHEDULE_ONCHANGE 0x81 // 7th+1st bit for OnChange
|
||||||
#define SCHEDULEFLAG_SCHEDULE_CONDITION 0x82 // 7th+2nd bit for Condition
|
#define SCHEDULEFLAG_SCHEDULE_CONDITION 0x82 // 7th+2nd bit for Condition
|
||||||
#define SCHEDULEFLAG_SCHEDULE_IMMEDIATE 0x84 // 7th+3rd bit for Condition
|
#define SCHEDULEFLAG_SCHEDULE_IMMEDIATE 0x84 // 7th+3rd bit for Immediate
|
||||||
|
|
||||||
#define MAX_STARTUP_RETRIES 3 // retry the start-up commands x times
|
#define MAX_STARTUP_RETRIES 3 // retry the start-up commands x times
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ namespace emsesp {
|
|||||||
class ScheduleItem {
|
class ScheduleItem {
|
||||||
public:
|
public:
|
||||||
boolean active;
|
boolean active;
|
||||||
uint8_t flags;
|
uint8_t flags; // bit flags, see SCHEDULEFLAG_* defines
|
||||||
uint16_t elapsed_min; // total mins from 00:00
|
uint16_t elapsed_min; // total mins from 00:00
|
||||||
stringPSRAM time; // HH:MM
|
stringPSRAM time; // HH:MM
|
||||||
stringPSRAM cmd;
|
stringPSRAM cmd;
|
||||||
@@ -88,6 +88,8 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
|
|||||||
uint8_t count_entities(bool cmd_only = false);
|
uint8_t count_entities(bool cmd_only = false);
|
||||||
bool onChange(const char * cmd);
|
bool onChange(const char * cmd);
|
||||||
|
|
||||||
|
bool executeSchedule(const char * name);
|
||||||
|
|
||||||
std::string get_metrics_prometheus();
|
std::string get_metrics_prometheus();
|
||||||
|
|
||||||
std::string raw_value;
|
std::string raw_value;
|
||||||
|
|||||||
@@ -83,19 +83,15 @@ void WebSettings::read(WebSettings & settings, JsonObject root) {
|
|||||||
root["modbus_max_clients"] = settings.modbus_max_clients;
|
root["modbus_max_clients"] = settings.modbus_max_clients;
|
||||||
root["modbus_timeout"] = settings.modbus_timeout;
|
root["modbus_timeout"] = settings.modbus_timeout;
|
||||||
root["developer_mode"] = settings.developer_mode;
|
root["developer_mode"] = settings.developer_mode;
|
||||||
#ifndef NO_TLS_SUPPORT
|
root["email_enabled"] = settings.email_enabled;
|
||||||
root["email_enabled"] = settings.email_enabled;
|
root["email_security"] = settings.email_security;
|
||||||
#else
|
root["email_server"] = settings.email_server;
|
||||||
root["email_enabled"] = false;
|
root["email_port"] = settings.email_port;
|
||||||
#endif
|
root["email_login"] = settings.email_login;
|
||||||
root["email_security"] = settings.email_security;
|
root["email_pass"] = settings.email_pass;
|
||||||
root["email_server"] = settings.email_server;
|
root["email_sender"] = settings.email_sender;
|
||||||
root["email_port"] = settings.email_port;
|
root["email_recp"] = settings.email_recp;
|
||||||
root["email_login"] = settings.email_login;
|
root["email_subject"] = settings.email_subject;
|
||||||
root["email_pass"] = settings.email_pass;
|
|
||||||
root["email_sender"] = settings.email_sender;
|
|
||||||
root["email_recp"] = settings.email_recp;
|
|
||||||
root["email_subject"] = settings.email_subject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// call on initialization and also when settings are updated/saved via web or console
|
// call on initialization and also when settings are updated/saved via web or console
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
|
|
||||||
#ifndef EMSESP_STANDALONE
|
#ifndef EMSESP_STANDALONE
|
||||||
#include <esp_ota_ops.h>
|
#include <esp_ota_ops.h>
|
||||||
#include <HTTPClient.h>
|
#include <WiFiClient.h>
|
||||||
|
#include <ESP_SSLClient.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace emsesp {
|
namespace emsesp {
|
||||||
@@ -227,6 +228,8 @@ void WebStatusService::action(AsyncWebServerRequest * request, JsonVariant json)
|
|||||||
EMSESP::mqtt_.reset_mqtt();
|
EMSESP::mqtt_.reset_mqtt();
|
||||||
} else if (action == "upgradeImportantMessages") {
|
} else if (action == "upgradeImportantMessages") {
|
||||||
root["upgradeImportantMessageType"] = upgradeImportantMessages(param);
|
root["upgradeImportantMessageType"] = upgradeImportantMessages(param);
|
||||||
|
} else if (action == "executeSchedule") {
|
||||||
|
ok = EMSESP::webSchedulerService.executeSchedule(param.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(EMSESP_STANDALONE) && !defined(EMSESP_UNITY)
|
#if defined(EMSESP_STANDALONE) && !defined(EMSESP_UNITY)
|
||||||
@@ -412,30 +415,91 @@ bool WebStatusService::refresh_versions_cache() {
|
|||||||
#ifdef EMSESP_STANDALONE
|
#ifdef EMSESP_STANDALONE
|
||||||
return false;
|
return false;
|
||||||
#else
|
#else
|
||||||
HTTPClient http;
|
// detect scheme from EMSESP_VERSIONS_URL (case-insensitive). One code path for HTTP and HTTPS,
|
||||||
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
// using ESP_SSLClient as a plain TCP passthrough when SSL is disabled.
|
||||||
http.setTimeout(5000);
|
String url = EMSESP_VERSIONS_URL;
|
||||||
http.useHTTP10(true);
|
String lower = url;
|
||||||
|
lower.toLowerCase();
|
||||||
if (!http.begin(EMSESP_VERSIONS_URL)) {
|
const bool is_https = lower.startsWith("https://");
|
||||||
|
if (!is_https && !lower.startsWith("http://")) {
|
||||||
#if defined(EMSESP_DEBUG)
|
#if defined(EMSESP_DEBUG)
|
||||||
EMSESP::logger().debug("versions.json: failed to start HTTPS request");
|
EMSESP::logger().debug("versions.json: unsupported scheme");
|
||||||
|
#endif
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const int scheme_len = is_https ? 8 : 7;
|
||||||
|
|
||||||
|
WiFiClient basic_client;
|
||||||
|
ESP_SSLClient ssl_client;
|
||||||
|
if (is_https) {
|
||||||
|
ssl_client.setInsecure();
|
||||||
|
ssl_client.setBufferSizes(16384, 1024); // versions.json fits easily but TLS records may be full-size
|
||||||
|
ssl_client.setSessionTimeout(120);
|
||||||
|
}
|
||||||
|
basic_client.setTimeout(5000);
|
||||||
|
ssl_client.setTimeout(5000);
|
||||||
|
ssl_client.setClient(&basic_client, is_https);
|
||||||
|
|
||||||
|
// split into host and path
|
||||||
|
String rest = url.substring(scheme_len);
|
||||||
|
String host;
|
||||||
|
String path;
|
||||||
|
int s = rest.indexOf('/');
|
||||||
|
if (s < 0) {
|
||||||
|
host = rest;
|
||||||
|
path = "/";
|
||||||
|
} else {
|
||||||
|
host = rest.substring(0, s);
|
||||||
|
path = rest.substring(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ssl_client.connect(host.c_str(), is_https ? 443 : 80)) {
|
||||||
|
#if defined(EMSESP_DEBUG)
|
||||||
|
EMSESP::logger().debug("versions.json: connection failed");
|
||||||
#endif
|
#endif
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int httpCode = http.GET();
|
// minimal HTTP/1.0 GET so we don't have to handle chunked encoding
|
||||||
if (httpCode != HTTP_CODE_OK) {
|
ssl_client.print("GET ");
|
||||||
|
ssl_client.print(path);
|
||||||
|
ssl_client.println(" HTTP/1.0");
|
||||||
|
ssl_client.print("Host: ");
|
||||||
|
ssl_client.println(host);
|
||||||
|
ssl_client.println("User-Agent: EMS-ESP");
|
||||||
|
ssl_client.println("Connection: close");
|
||||||
|
ssl_client.print("\r\n");
|
||||||
|
|
||||||
|
// wait for the first byte
|
||||||
|
uint32_t ms = millis();
|
||||||
|
while (ssl_client.connected() && !ssl_client.available() && millis() - ms < 5000) {
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse status line
|
||||||
|
String status_line = ssl_client.readStringUntil('\n');
|
||||||
|
int sp = status_line.indexOf(' ');
|
||||||
|
int http_code = (sp >= 0) ? status_line.substring(sp + 1, sp + 4).toInt() : 0;
|
||||||
|
if (http_code != 200) {
|
||||||
|
ssl_client.stop();
|
||||||
#if defined(EMSESP_DEBUG)
|
#if defined(EMSESP_DEBUG)
|
||||||
EMSESP::logger().debug("versions.json: HTTP error code %d", httpCode);
|
EMSESP::logger().debug("versions.json: HTTP error code %d", http_code);
|
||||||
#endif
|
#endif
|
||||||
http.end();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// skip headers
|
||||||
|
while (ssl_client.connected() || ssl_client.available()) {
|
||||||
|
String line = ssl_client.readStringUntil('\n');
|
||||||
|
line.trim();
|
||||||
|
if (line.isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JsonDocument doc(PSRAM_DOC);
|
JsonDocument doc(PSRAM_DOC);
|
||||||
DeserializationError err = deserializeJson(doc, http.getStream());
|
DeserializationError err = deserializeJson(doc, ssl_client);
|
||||||
http.end();
|
ssl_client.stop();
|
||||||
if (err) {
|
if (err) {
|
||||||
#if defined(EMSESP_DEBUG)
|
#if defined(EMSESP_DEBUG)
|
||||||
EMSESP::logger().debug("versions.json: parse error");
|
EMSESP::logger().debug("versions.json: parse error");
|
||||||
|
|||||||
Reference in New Issue
Block a user