mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-10 17:59:53 +03:00
7
.github/workflows/test_release.yml
vendored
7
.github/workflows/test_release.yml
vendored
@@ -13,12 +13,15 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: Enable Corepack
|
||||||
|
run: corepack enable
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
- uses: actions/setup-node@v4
|
- name: Use Node.js 20.x
|
||||||
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20.x'
|
||||||
|
|
||||||
- name: Get EMS-ESP source code and version
|
- name: Get EMS-ESP source code and version
|
||||||
id: build_info
|
id: build_info
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
- timeout for remote thermostat emulation [#1680](https://github.com/emsesp/EMS-ESP32/discussions/1680), [#1774](https://github.com/emsesp/EMS-ESP32/issues/1774)
|
- timeout for remote thermostat emulation [#1680](https://github.com/emsesp/EMS-ESP32/discussions/1680), [#1774](https://github.com/emsesp/EMS-ESP32/issues/1774)
|
||||||
- CR120 thermostat as own model() [#1779](https://github.com/emsesp/EMS-ESP32/discussions/1779)
|
- CR120 thermostat as own model() [#1779](https://github.com/emsesp/EMS-ESP32/discussions/1779)
|
||||||
- Modules - external linkable module library [#1778](https://github.com/emsesp/EMS-ESP32/issues/1778)
|
- Modules - external linkable module library [#1778](https://github.com/emsesp/EMS-ESP32/issues/1778)
|
||||||
|
- Scheduler onChange and Conditions [#1806](https://github.com/emsesp/EMS-ESP32/issues/1806)
|
||||||
- make remote control timeout editable [#1774](https://github.com/emsesp/EMS-ESP32/issues/1774)
|
- make remote control timeout editable [#1774](https://github.com/emsesp/EMS-ESP32/issues/1774)
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const Scheduler: FC = () => {
|
|||||||
|
|
||||||
const schedule_theme = useTheme({
|
const schedule_theme = useTheme({
|
||||||
Table: `
|
Table: `
|
||||||
--data-table-library_grid-template-columns: 36px 324px 50px 192px repeat(1, minmax(100px, 1fr)) 160px;
|
--data-table-library_grid-template-columns: 36px 210px 100px 192px repeat(1, minmax(100px, 1fr)) 160px;
|
||||||
`,
|
`,
|
||||||
BaseRow: `
|
BaseRow: `
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -198,7 +198,7 @@ const Scheduler: FC = () => {
|
|||||||
active: false,
|
active: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
flags: 0,
|
flags: 0,
|
||||||
time: '12:00',
|
time: '',
|
||||||
cmd: '',
|
cmd: '',
|
||||||
value: '',
|
value: '',
|
||||||
name: ''
|
name: ''
|
||||||
@@ -216,11 +216,21 @@ const Scheduler: FC = () => {
|
|||||||
<Box>
|
<Box>
|
||||||
<Typography
|
<Typography
|
||||||
sx={{ fontSize: 11 }}
|
sx={{ fontSize: 11 }}
|
||||||
color={(si.flags & flag) === flag ? 'primary' : 'grey'}
|
color={
|
||||||
|
si.flags >= ScheduleFlag.SCHEDULE_TIMER && si.flags !== flag
|
||||||
|
? 'gray'
|
||||||
|
: (si.flags & flag) === flag
|
||||||
|
? 'primary'
|
||||||
|
: 'grey'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{flag === ScheduleFlag.SCHEDULE_TIMER
|
{flag === ScheduleFlag.SCHEDULE_TIMER
|
||||||
? LL.TIMER(0)
|
? LL.TIMER(0)
|
||||||
: dow[Math.log(flag) / Math.log(2)]}
|
: flag === ScheduleFlag.SCHEDULE_ONCHANGE
|
||||||
|
? 'OnChange'
|
||||||
|
: flag === ScheduleFlag.SCHEDULE_CONDITION
|
||||||
|
? 'Condition'
|
||||||
|
: dow[Math.log(flag) / Math.log(2)]}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
@@ -245,7 +255,7 @@ const Scheduler: FC = () => {
|
|||||||
<HeaderRow>
|
<HeaderRow>
|
||||||
<HeaderCell />
|
<HeaderCell />
|
||||||
<HeaderCell stiff>{LL.SCHEDULE(0)}</HeaderCell>
|
<HeaderCell stiff>{LL.SCHEDULE(0)}</HeaderCell>
|
||||||
<HeaderCell stiff>{LL.TIME(0)}</HeaderCell>
|
<HeaderCell stiff>{LL.TIME(0)}/Cond.</HeaderCell>
|
||||||
<HeaderCell stiff>{LL.COMMAND(0)}</HeaderCell>
|
<HeaderCell stiff>{LL.COMMAND(0)}</HeaderCell>
|
||||||
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
|
<HeaderCell stiff>{LL.VALUE(0)}</HeaderCell>
|
||||||
<HeaderCell stiff>{LL.NAME(0)}</HeaderCell>
|
<HeaderCell stiff>{LL.NAME(0)}</HeaderCell>
|
||||||
@@ -268,16 +278,25 @@ const Scheduler: FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell stiff>
|
<Cell stiff>
|
||||||
<Stack spacing={1} direction="row">
|
<Stack spacing={0.5} direction="row">
|
||||||
<Divider orientation="vertical" flexItem />
|
<Divider orientation="vertical" flexItem />
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_MON)}
|
{si.flags < ScheduleFlag.SCHEDULE_TIMER ? (
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_TUE)}
|
<>
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_WED)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_MON)}
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_THU)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_TUE)}
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_FRI)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_WED)}
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_SAT)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_THU)}
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_SUN)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_FRI)}
|
||||||
{dayBox(si, ScheduleFlag.SCHEDULE_TIMER)}
|
{dayBox(si, ScheduleFlag.SCHEDULE_SAT)}
|
||||||
|
{dayBox(si, ScheduleFlag.SCHEDULE_SUN)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{dayBox(si, ScheduleFlag.SCHEDULE_TIMER)}
|
||||||
|
{dayBox(si, ScheduleFlag.SCHEDULE_ONCHANGE)}
|
||||||
|
{dayBox(si, ScheduleFlag.SCHEDULE_CONDITION)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell>{si.time}</Cell>
|
<Cell>{si.time}</Cell>
|
||||||
@@ -341,7 +360,7 @@ const Scheduler: FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
startIcon={<AddIcon />}
|
startIcon={<AddIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="primary"
|
||||||
onClick={addScheduleItem}
|
onClick={addScheduleItem}
|
||||||
>
|
>
|
||||||
{LL.ADD(0)}
|
{LL.ADD(0)}
|
||||||
|
|||||||
@@ -91,10 +91,10 @@ const SchedulerDialog = ({
|
|||||||
|
|
||||||
const getFlagString = (f: number) => {
|
const getFlagString = (f: number) => {
|
||||||
const new_flags: string[] = [];
|
const new_flags: string[] = [];
|
||||||
if ((f & 1) === 1) {
|
if ((f & 129) === 1) {
|
||||||
new_flags.push('1');
|
new_flags.push('1');
|
||||||
}
|
}
|
||||||
if ((f & 2) === 2) {
|
if ((f & 130) === 2) {
|
||||||
new_flags.push('2');
|
new_flags.push('2');
|
||||||
}
|
}
|
||||||
if ((f & 4) === 4) {
|
if ((f & 4) === 4) {
|
||||||
@@ -112,26 +112,45 @@ const SchedulerDialog = ({
|
|||||||
if ((f & 64) === 64) {
|
if ((f & 64) === 64) {
|
||||||
new_flags.push('64');
|
new_flags.push('64');
|
||||||
}
|
}
|
||||||
if ((f & 128) === 128) {
|
if ((f & 131) === 128) {
|
||||||
new_flags.push('128');
|
new_flags.push('128');
|
||||||
}
|
}
|
||||||
|
if ((f & 131) === 129) {
|
||||||
|
new_flags.push('129');
|
||||||
|
}
|
||||||
|
if ((f & 131) === 130) {
|
||||||
|
new_flags.push('130');
|
||||||
|
}
|
||||||
return new_flags;
|
return new_flags;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isTimer = editItem.flags === ScheduleFlag.SCHEDULE_TIMER;
|
||||||
|
const isCondition = editItem.flags === ScheduleFlag.SCHEDULE_CONDITION;
|
||||||
|
const isOnChange = editItem.flags === ScheduleFlag.SCHEDULE_ONCHANGE;
|
||||||
|
|
||||||
const showFlag = (si: ScheduleItem, flag: number) => (
|
const showFlag = (si: ScheduleItem, flag: number) => (
|
||||||
<Typography
|
<Typography
|
||||||
variant="button"
|
variant="button"
|
||||||
sx={{ fontSize: 10 }}
|
sx={{ fontSize: 10 }}
|
||||||
color={(si.flags & flag) === flag ? 'primary' : 'grey'}
|
color={
|
||||||
|
(isOnChange && flag !== ScheduleFlag.SCHEDULE_ONCHANGE) ||
|
||||||
|
(isCondition && flag !== ScheduleFlag.SCHEDULE_CONDITION)
|
||||||
|
? 'grey'
|
||||||
|
: (si.flags & flag) === flag
|
||||||
|
? 'primary'
|
||||||
|
: 'grey'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{flag === ScheduleFlag.SCHEDULE_TIMER
|
{flag === ScheduleFlag.SCHEDULE_TIMER
|
||||||
? LL.TIMER(0)
|
? LL.TIMER(0)
|
||||||
: dow[Math.log(flag) / Math.log(2)]}
|
: flag === ScheduleFlag.SCHEDULE_ONCHANGE
|
||||||
|
? 'On Change'
|
||||||
|
: flag === ScheduleFlag.SCHEDULE_CONDITION
|
||||||
|
? 'Condition'
|
||||||
|
: dow[Math.log(flag) / Math.log(2)]}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
|
|
||||||
const isTimer = editItem.flags === ScheduleFlag.SCHEDULE_TIMER;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
@@ -179,7 +198,7 @@ const SchedulerDialog = ({
|
|||||||
sx={{ bgcolor: '#334f65' }}
|
sx={{ bgcolor: '#334f65' }}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditItem({ ...editItem, flags: 0 });
|
setEditItem({ ...editItem, flags: ScheduleFlag.SCHEDULE_TIMER });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showFlag(editItem, ScheduleFlag.SCHEDULE_TIMER)}
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_TIMER)}
|
||||||
@@ -199,6 +218,66 @@ const SchedulerDialog = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box>
|
||||||
|
{isOnChange ? (
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
sx={{ bgcolor: '#334f65' }}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
setEditItem({
|
||||||
|
...editItem,
|
||||||
|
flags: ScheduleFlag.SCHEDULE_ONCHANGE
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_ONCHANGE)}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => {
|
||||||
|
setEditItem({
|
||||||
|
...editItem,
|
||||||
|
flags: ScheduleFlag.SCHEDULE_ONCHANGE
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_ONCHANGE)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
{isCondition ? (
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
sx={{ bgcolor: '#334f65' }}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
setEditItem({
|
||||||
|
...editItem,
|
||||||
|
flags: ScheduleFlag.SCHEDULE_CONDITION
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_CONDITION)}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => {
|
||||||
|
setEditItem({
|
||||||
|
...editItem,
|
||||||
|
flags: ScheduleFlag.SCHEDULE_CONDITION
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showFlag(editItem, ScheduleFlag.SCHEDULE_CONDITION)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
@@ -213,18 +292,31 @@ const SchedulerDialog = ({
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<TextField
|
{isCondition || isOnChange ? (
|
||||||
name="time"
|
<TextField
|
||||||
type="time"
|
name="time"
|
||||||
label={isTimer ? LL.TIMER(1) : LL.TIME(1)}
|
label={isCondition ? 'Condition' : 'On Change Value'}
|
||||||
value={editItem.time}
|
fullWidth
|
||||||
margin="normal"
|
value={editItem.time}
|
||||||
onChange={updateFormValue}
|
margin="normal"
|
||||||
/>
|
onChange={updateFormValue}
|
||||||
{isTimer && (
|
/>
|
||||||
<Box color="warning.main" ml={2} mt={4}>
|
) : (
|
||||||
<Typography variant="body2">{LL.SCHEDULER_HELP_2()}</Typography>
|
<>
|
||||||
</Box>
|
<TextField
|
||||||
|
name="time"
|
||||||
|
type="time"
|
||||||
|
label={isTimer ? LL.TIMER(1) : LL.TIME(1)}
|
||||||
|
value={editItem.time}
|
||||||
|
margin="normal"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
/>
|
||||||
|
{isTimer && (
|
||||||
|
<Box color="warning.main" ml={2} mt={4}>
|
||||||
|
<Typography variant="body2">{LL.SCHEDULER_HELP_2()}</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
|
|||||||
@@ -337,7 +337,9 @@ export enum ScheduleFlag {
|
|||||||
SCHEDULE_THU = 16,
|
SCHEDULE_THU = 16,
|
||||||
SCHEDULE_FRI = 32,
|
SCHEDULE_FRI = 32,
|
||||||
SCHEDULE_SAT = 64,
|
SCHEDULE_SAT = 64,
|
||||||
SCHEDULE_TIMER = 128
|
SCHEDULE_TIMER = 128,
|
||||||
|
SCHEDULE_ONCHANGE = 129,
|
||||||
|
SCHEDULE_CONDITION = 130
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityItem {
|
export interface EntityItem {
|
||||||
|
|||||||
@@ -409,6 +409,9 @@ void AnalogSensor::publish_sensor(const Sensor & sensor) const {
|
|||||||
char payload[10];
|
char payload[10];
|
||||||
Mqtt::queue_publish(topic, Helpers::render_value(payload, sensor.value(), 2)); // always publish as doubles
|
Mqtt::queue_publish(topic, Helpers::render_value(payload, sensor.value(), 2)); // always publish as doubles
|
||||||
}
|
}
|
||||||
|
char cmd[COMMAND_MAX_LENGTH];
|
||||||
|
snprintf(cmd, sizeof(cmd), "%s/%s", F_(analogsensor), sensor.name().c_str());
|
||||||
|
EMSESP::webSchedulerService.onChange(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// send empty config topic to remove the entry from HA
|
// send empty config topic to remove the entry from HA
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ uint8_t Command::call(const uint8_t device_type, const char * cmd, const char *
|
|||||||
if (single_command) {
|
if (single_command) {
|
||||||
// exception 1: anything that is from System
|
// exception 1: anything that is from System
|
||||||
// exception 2: boiler coldshot command
|
// exception 2: boiler coldshot command
|
||||||
bool get_attributes = (!cf || !cf->cmdfunction_json_) && (device_type > EMSdevice::DeviceType::SYSTEM) && (strcmp(cmd, F_(coldshot)) != 0);
|
bool get_attributes = (!cf || !cf->cmdfunction_json_) && (strcmp(cmd, F_(coldshot)) != 0);
|
||||||
|
|
||||||
if (get_attributes) {
|
if (get_attributes) {
|
||||||
LOG_DEBUG("Calling %s command '%s' to retrieve attributes", dname, cmd);
|
LOG_DEBUG("Calling %s command '%s' to retrieve attributes", dname, cmd);
|
||||||
@@ -437,6 +437,19 @@ Command::CmdFunction * Command::find_command(const uint8_t device_type, const ui
|
|||||||
return nullptr; // command not found
|
return nullptr; // command not found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Command::erase_device_commands(const uint8_t device_type) {
|
||||||
|
if (cmdfunctions_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto it = cmdfunctions_.end();
|
||||||
|
do {
|
||||||
|
int i = it - cmdfunctions_.begin();
|
||||||
|
if (cmdfunctions_[i].device_type_==device_type) {
|
||||||
|
cmdfunctions_.erase(it);
|
||||||
|
}
|
||||||
|
} while (it-- > cmdfunctions_.begin());
|
||||||
|
}
|
||||||
|
|
||||||
void Command::erase_command(const uint8_t device_type, const char * cmd, uint8_t flag) {
|
void Command::erase_command(const uint8_t device_type, const char * cmd, uint8_t flag) {
|
||||||
if ((cmd == nullptr) || (strlen(cmd) == 0) || (cmdfunctions_.empty())) {
|
if ((cmd == nullptr) || (strlen(cmd) == 0) || (cmdfunctions_.empty())) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ class Command {
|
|||||||
static Command::CmdFunction * find_command(const uint8_t device_type, const uint8_t device_id, const char * cmd, const uint8_t flag);
|
static Command::CmdFunction * find_command(const uint8_t device_type, const uint8_t device_id, const char * cmd, const uint8_t flag);
|
||||||
static std::string tagged_cmd(const std::string & cmd, const uint8_t flag);
|
static std::string tagged_cmd(const std::string & cmd, const uint8_t flag);
|
||||||
|
|
||||||
|
static void erase_device_commands(const uint8_t device_type);
|
||||||
static void erase_command(const uint8_t device_type, const char * cmd, uint8_t flag = CommandFlag::CMD_FLAG_DEFAULT);
|
static void erase_command(const uint8_t device_type, const char * cmd, uint8_t flag = CommandFlag::CMD_FLAG_DEFAULT);
|
||||||
static void show(uuid::console::Shell & shell, uint8_t device_type, bool verbose);
|
static void show(uuid::console::Shell & shell, uint8_t device_type, bool verbose);
|
||||||
static void show_devices(uuid::console::Shell & shell);
|
static void show_devices(uuid::console::Shell & shell);
|
||||||
|
|||||||
@@ -105,6 +105,10 @@ const char * EMSdevice::device_type_2_device_name(const uint8_t device_type) {
|
|||||||
switch (device_type) {
|
switch (device_type) {
|
||||||
case DeviceType::SYSTEM:
|
case DeviceType::SYSTEM:
|
||||||
return F_(system);
|
return F_(system);
|
||||||
|
case DeviceType::TEMPERATURESENSOR:
|
||||||
|
return F_(temperaturesensor);
|
||||||
|
case DeviceType::ANALOGSENSOR:
|
||||||
|
return F_(analogsensor);
|
||||||
case DeviceType::SCHEDULER:
|
case DeviceType::SCHEDULER:
|
||||||
return F_(scheduler);
|
return F_(scheduler);
|
||||||
case DeviceType::CUSTOM:
|
case DeviceType::CUSTOM:
|
||||||
@@ -113,28 +117,26 @@ const char * EMSdevice::device_type_2_device_name(const uint8_t device_type) {
|
|||||||
return F_(boiler);
|
return F_(boiler);
|
||||||
case DeviceType::THERMOSTAT:
|
case DeviceType::THERMOSTAT:
|
||||||
return F_(thermostat);
|
return F_(thermostat);
|
||||||
case DeviceType::HEATPUMP:
|
|
||||||
return F_(heatpump);
|
|
||||||
case DeviceType::SOLAR:
|
|
||||||
return F_(solar);
|
|
||||||
case DeviceType::CONNECT:
|
|
||||||
return F_(connect);
|
|
||||||
case DeviceType::MIXER:
|
case DeviceType::MIXER:
|
||||||
return F_(mixer);
|
return F_(mixer);
|
||||||
case DeviceType::TEMPERATURESENSOR:
|
case DeviceType::SOLAR:
|
||||||
return F_(temperaturesensor);
|
return F_(solar);
|
||||||
case DeviceType::ANALOGSENSOR:
|
case DeviceType::HEATPUMP:
|
||||||
return F_(analogsensor);
|
return F_(heatpump);
|
||||||
case DeviceType::CONTROLLER:
|
|
||||||
return F_(controller);
|
|
||||||
case DeviceType::SWITCH:
|
|
||||||
return F_(switch);
|
|
||||||
case DeviceType::GATEWAY:
|
case DeviceType::GATEWAY:
|
||||||
return F_(gateway);
|
return F_(gateway);
|
||||||
|
case DeviceType::SWITCH:
|
||||||
|
return F_(switch);
|
||||||
|
case DeviceType::CONTROLLER:
|
||||||
|
return F_(controller);
|
||||||
|
case DeviceType::CONNECT:
|
||||||
|
return F_(connect);
|
||||||
case DeviceType::ALERT:
|
case DeviceType::ALERT:
|
||||||
return F_(alert);
|
return F_(alert);
|
||||||
case DeviceType::EXTENSION:
|
case DeviceType::EXTENSION:
|
||||||
return F_(extension);
|
return F_(extension);
|
||||||
|
case DeviceType::GENERIC:
|
||||||
|
return F_(generic);
|
||||||
case DeviceType::HEATSOURCE:
|
case DeviceType::HEATSOURCE:
|
||||||
return F_(heatsource);
|
return F_(heatsource);
|
||||||
case DeviceType::VENTILATION:
|
case DeviceType::VENTILATION:
|
||||||
@@ -752,7 +754,8 @@ void EMSdevice::set_minmax(const void * value_p, int16_t min, uint32_t max) {
|
|||||||
|
|
||||||
// publish a single value on change
|
// publish a single value on change
|
||||||
void EMSdevice::publish_value(void * value_p) const {
|
void EMSdevice::publish_value(void * value_p) const {
|
||||||
if (!Mqtt::publish_single() || value_p == nullptr) {
|
// if (!Mqtt::publish_single() || value_p == nullptr) {
|
||||||
|
if (value_p == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,9 +823,17 @@ void EMSdevice::publish_value(void * value_p) const {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload[0] != '\0') {
|
if (Mqtt::publish_single() && payload[0] != '\0') {
|
||||||
Mqtt::queue_publish(topic, payload);
|
Mqtt::queue_publish(topic, payload);
|
||||||
}
|
}
|
||||||
|
// check scheduler for on change
|
||||||
|
char cmd[COMMAND_MAX_LENGTH];
|
||||||
|
if (dv.tag >= DeviceValueTAG::TAG_HC1) {
|
||||||
|
snprintf(cmd, sizeof(cmd), "%s/%s/%s", device_type_2_device_name(device_type_), tag_to_mqtt(dv.tag), dv.short_name);
|
||||||
|
} else {
|
||||||
|
snprintf(cmd, sizeof(cmd), "%s/%s", device_type_2_device_name(device_type_), (dv.short_name));
|
||||||
|
}
|
||||||
|
EMSESP::webSchedulerService.onChange(cmd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -771,6 +771,10 @@ bool EMSESP::get_device_value_info(JsonObject root, const char * cmd, const int8
|
|||||||
return webCustomEntityService.get_value_info(root, cmd);
|
return webCustomEntityService.get_value_info(root, cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (devicetype == DeviceType::SYSTEM) {
|
||||||
|
return system_.get_value_info(root, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
char error[100];
|
char error[100];
|
||||||
snprintf(error, sizeof(error), "cannot find values for entity '%s'", cmd);
|
snprintf(error, sizeof(error), "cannot find values for entity '%s'", cmd);
|
||||||
root["message"] = error;
|
root["message"] = error;
|
||||||
|
|||||||
@@ -124,6 +124,10 @@ bool System::command_allvalues(const char * value, const int8_t id, JsonObject o
|
|||||||
device_output = output["Custom Entities"].to<JsonObject>();
|
device_output = output["Custom Entities"].to<JsonObject>();
|
||||||
EMSESP::webCustomEntityService.get_value_info(device_output, "");
|
EMSESP::webCustomEntityService.get_value_info(device_output, "");
|
||||||
|
|
||||||
|
// Scheduler
|
||||||
|
device_output = output["Scheduler"].to<JsonObject>();
|
||||||
|
EMSESP::webSchedulerService.get_value_info(device_output, "");
|
||||||
|
|
||||||
// Sensors
|
// Sensors
|
||||||
device_output = output["Analog Sensors"].to<JsonObject>();
|
device_output = output["Analog Sensors"].to<JsonObject>();
|
||||||
EMSESP::analogsensor_.get_value_info(device_output, "values");
|
EMSESP::analogsensor_.get_value_info(device_output, "values");
|
||||||
@@ -1268,6 +1272,44 @@ bool System::saveSettings(const char * filename, const char * section, JsonObjec
|
|||||||
return false; // not found
|
return false; // not found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool System::get_value_info(JsonObject root, const char * command) {
|
||||||
|
if (command == nullptr || strlen(command) == 0) {
|
||||||
|
LOG_ERROR("empty system command");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char cmd[COMMAND_MAX_LENGTH];
|
||||||
|
strlcpy(cmd, command, sizeof(cmd));
|
||||||
|
char * val = strstr(cmd, "/value");
|
||||||
|
if (val) {
|
||||||
|
val[0] = '\0';
|
||||||
|
}
|
||||||
|
char * dash = strchr(cmd, '/');
|
||||||
|
if (dash) {
|
||||||
|
*dash = '\0';
|
||||||
|
dash++;
|
||||||
|
}
|
||||||
|
if (command_info("", 0, root)) {
|
||||||
|
std::string s;
|
||||||
|
if (dash && root[cmd].containsKey(dash)) {
|
||||||
|
s = root[cmd][dash].as<std::string>();
|
||||||
|
} else if (root.containsKey(cmd)) {
|
||||||
|
s = root[cmd].as<std::string>();
|
||||||
|
}
|
||||||
|
if (!s.empty()) {
|
||||||
|
root.clear();
|
||||||
|
if (val) {
|
||||||
|
root["api_data"] = s;
|
||||||
|
} else {
|
||||||
|
root["value"] = s;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root.clear();
|
||||||
|
LOG_ERROR("system command not found: %s from %s", cmd, command);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// export status information including the device information
|
// export status information including the device information
|
||||||
// http://ems-esp/api/system/info
|
// http://ems-esp/api/system/info
|
||||||
bool System::command_info(const char * value, const int8_t id, JsonObject output) {
|
bool System::command_info(const char * value, const int8_t id, JsonObject output) {
|
||||||
@@ -1448,13 +1490,13 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output
|
|||||||
// Settings
|
// Settings
|
||||||
node = output["Settings"].to<JsonObject>();
|
node = output["Settings"].to<JsonObject>();
|
||||||
EMSESP::webSettingsService.read([&](WebSettings & settings) {
|
EMSESP::webSettingsService.read([&](WebSettings & settings) {
|
||||||
node["board profile"] = settings.board_profile;
|
node["board profile"] = settings.board_profile;
|
||||||
node["locale"] = settings.locale;
|
node["locale"] = settings.locale;
|
||||||
node["tx mode"] = settings.tx_mode;
|
node["tx mode"] = settings.tx_mode;
|
||||||
node["ems bus id"] = settings.ems_bus_id;
|
node["ems bus id"] = settings.ems_bus_id;
|
||||||
node["shower timer"] = settings.shower_timer;
|
node["shower timer"] = settings.shower_timer;
|
||||||
node["shower alert"] = settings.shower_alert;
|
node["shower min duration"] = settings.shower_min_duration; // seconds
|
||||||
node["shpwe_min_duration"] = settings.shower_min_duration; // seconds
|
node["shower alert"] = settings.shower_alert;
|
||||||
if (settings.shower_alert) {
|
if (settings.shower_alert) {
|
||||||
node["shower alert coldshot"] = settings.shower_alert_coldshot; // seconds
|
node["shower alert coldshot"] = settings.shower_alert_coldshot; // seconds
|
||||||
node["shower alert trigger"] = settings.shower_alert_trigger; // minutes
|
node["shower alert trigger"] = settings.shower_alert_trigger; // minutes
|
||||||
@@ -1499,7 +1541,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output
|
|||||||
obj["product id"] = emsdevice->product_id();
|
obj["product id"] = emsdevice->product_id();
|
||||||
obj["version"] = emsdevice->version();
|
obj["version"] = emsdevice->version();
|
||||||
obj["entities"] = emsdevice->count_entities();
|
obj["entities"] = emsdevice->count_entities();
|
||||||
char result[300];
|
char result[500];
|
||||||
(void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::RECEIVED);
|
(void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::RECEIVED);
|
||||||
if (result[0] != '\0') {
|
if (result[0] != '\0') {
|
||||||
obj["handlers received"] = result; // don't show handlers if there aren't any
|
obj["handlers received"] = result; // don't show handlers if there aren't any
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class System {
|
|||||||
static bool command_commands(const char * value, const int8_t id, JsonObject output);
|
static bool command_commands(const char * value, const int8_t id, JsonObject output);
|
||||||
static bool command_response(const char * value, const int8_t id, JsonObject output);
|
static bool command_response(const char * value, const int8_t id, JsonObject output);
|
||||||
static bool command_allvalues(const char * value, const int8_t id, JsonObject output);
|
static bool command_allvalues(const char * value, const int8_t id, JsonObject output);
|
||||||
|
static bool get_value_info(JsonObject root, const char *cmd);
|
||||||
|
|
||||||
#if defined(EMSESP_TEST)
|
#if defined(EMSESP_TEST)
|
||||||
static bool command_test(const char * value, const int8_t id);
|
static bool command_test(const char * value, const int8_t id);
|
||||||
|
|||||||
@@ -441,6 +441,9 @@ void TemperatureSensor::publish_sensor(const Sensor & sensor) {
|
|||||||
char payload[10];
|
char payload[10];
|
||||||
Mqtt::queue_publish(topic, Helpers::render_value(payload, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0));
|
Mqtt::queue_publish(topic, Helpers::render_value(payload, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0));
|
||||||
}
|
}
|
||||||
|
char cmd[COMMAND_MAX_LENGTH];
|
||||||
|
snprintf(cmd, sizeof(cmd), "%s/%s", F_(temperaturesensor), sensor.name().c_str());
|
||||||
|
EMSESP::webSchedulerService.onChange(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// send empty config topic to remove the entry from HA
|
// send empty config topic to remove the entry from HA
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
#define EMSESP_APP_VERSION "3.7.0-dev.16"
|
#define EMSESP_APP_VERSION "3.7.0-dev.17"
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ WebCustomEntityService::WebCustomEntityService(AsyncWebServer * server, FS * fs,
|
|||||||
// load the settings when the service starts
|
// load the settings when the service starts
|
||||||
void WebCustomEntityService::begin() {
|
void WebCustomEntityService::begin() {
|
||||||
_fsPersistence.readFromFS();
|
_fsPersistence.readFromFS();
|
||||||
|
// save a local pointer to the item list
|
||||||
|
EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems_ = &webEntity.customEntityItems; });
|
||||||
EMSESP::logger().info("Starting Custom Entity service");
|
EMSESP::logger().info("Starting Custom Entity service");
|
||||||
Mqtt::subscribe(EMSdevice::DeviceType::CUSTOM, "custom/#", nullptr); // use empty function callback
|
Mqtt::subscribe(EMSdevice::DeviceType::CUSTOM, "custom/#", nullptr); // use empty function callback
|
||||||
}
|
}
|
||||||
@@ -63,9 +65,7 @@ void WebCustomEntity::read(WebCustomEntity & webEntity, JsonObject root) {
|
|||||||
// this loads the data into the internal class
|
// this loads the data into the internal class
|
||||||
StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & webCustomEntity) {
|
StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & webCustomEntity) {
|
||||||
// reset everything to start fresh
|
// reset everything to start fresh
|
||||||
for (CustomEntityItem & entityItem : webCustomEntity.customEntityItems) {
|
Command::erase_device_commands(EMSdevice::DeviceType::CUSTOM);
|
||||||
Command::erase_command(EMSdevice::DeviceType::CUSTOM, entityItem.name.c_str());
|
|
||||||
}
|
|
||||||
webCustomEntity.customEntityItems.clear();
|
webCustomEntity.customEntityItems.clear();
|
||||||
EMSESP::webCustomEntityService.ha_reset();
|
EMSESP::webCustomEntityService.ha_reset();
|
||||||
|
|
||||||
@@ -112,12 +112,12 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web
|
|||||||
|
|
||||||
webCustomEntity.customEntityItems.push_back(entityItem); // add to list
|
webCustomEntity.customEntityItems.push_back(entityItem); // add to list
|
||||||
|
|
||||||
if (entityItem.writeable) {
|
if (webCustomEntity.customEntityItems.back().writeable && !webCustomEntity.customEntityItems.back().name.empty()) {
|
||||||
Command::add(
|
Command::add(
|
||||||
EMSdevice::DeviceType::CUSTOM,
|
EMSdevice::DeviceType::CUSTOM,
|
||||||
webCustomEntity.customEntityItems.back().name.c_str(),
|
webCustomEntity.customEntityItems.back().name.c_str(),
|
||||||
[webCustomEntity](const char * value, const int8_t id) {
|
[webCustomEntity](const char * value, const int8_t id) {
|
||||||
return EMSESP::webCustomEntityService.command_setvalue(value, webCustomEntity.customEntityItems.back().name);
|
return EMSESP::webCustomEntityService.command_setvalue(value, id, webCustomEntity.customEntityItems.back().name.c_str());
|
||||||
},
|
},
|
||||||
FL_(entity_cmd),
|
FL_(entity_cmd),
|
||||||
CommandFlag::ADMIN_ONLY);
|
CommandFlag::ADMIN_ONLY);
|
||||||
@@ -128,9 +128,8 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set value by api command
|
// set value by api command
|
||||||
bool WebCustomEntityService::command_setvalue(const char * value, const std::string name) {
|
bool WebCustomEntityService::command_setvalue(const char * value, const int8_t id, const char * name) {
|
||||||
EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; });
|
for (CustomEntityItem & entityItem : *customEntityItems_) {
|
||||||
for (CustomEntityItem & entityItem : *customEntityItems) {
|
|
||||||
if (Helpers::toLower(entityItem.name) == Helpers::toLower(name)) {
|
if (Helpers::toLower(entityItem.name) == Helpers::toLower(name)) {
|
||||||
if (entityItem.ram == 1) {
|
if (entityItem.ram == 1) {
|
||||||
entityItem.data = value;
|
entityItem.data = value;
|
||||||
@@ -175,6 +174,9 @@ bool WebCustomEntityService::command_setvalue(const char * value, const std::str
|
|||||||
if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
||||||
publish();
|
publish();
|
||||||
}
|
}
|
||||||
|
char cmd[COMMAND_MAX_LENGTH];
|
||||||
|
snprintf(cmd, sizeof(cmd_function_p), "custom/%s", entityItem.name.c_str());
|
||||||
|
EMSESP::webSchedulerService.onChange(cmd);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,20 +248,18 @@ void WebCustomEntityService::render_value(JsonObject output, CustomEntityItem en
|
|||||||
// display all custom entities
|
// display all custom entities
|
||||||
// adding each one, with UOM to a json object string
|
// adding each one, with UOM to a json object string
|
||||||
void WebCustomEntityService::show_values(JsonObject output) {
|
void WebCustomEntityService::show_values(JsonObject output) {
|
||||||
for (const CustomEntityItem & entity : *customEntityItems) {
|
for (const CustomEntityItem & entity : *customEntityItems_) {
|
||||||
render_value(output, entity, false, false, true); // with add_uom
|
render_value(output, entity, false, false, true); // with add_uom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// process json output for info/commands and value_info
|
// process json output for info/commands and value_info
|
||||||
bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) {
|
bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) {
|
||||||
EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; });
|
|
||||||
|
|
||||||
// if it's commands...
|
// if it's commands...
|
||||||
if (Helpers::toLower(cmd) == F_(commands)) {
|
if (Helpers::toLower(cmd) == F_(commands)) {
|
||||||
output[F_(info)] = Helpers::translated_word(FL_(info_cmd));
|
output[F_(info)] = Helpers::translated_word(FL_(info_cmd));
|
||||||
output[F_(commands)] = Helpers::translated_word(FL_(commands_cmd));
|
output[F_(commands)] = Helpers::translated_word(FL_(commands_cmd));
|
||||||
for (const auto & entity : *customEntityItems) {
|
for (const auto & entity : *customEntityItems_) {
|
||||||
output[entity.name] = "custom entity";
|
output[entity.name] = "custom entity";
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -267,14 +267,14 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd)
|
|||||||
|
|
||||||
// if no entries, return empty json
|
// if no entries, return empty json
|
||||||
// https://github.com/emsesp/EMS-ESP32/issues/1297
|
// https://github.com/emsesp/EMS-ESP32/issues/1297
|
||||||
if (customEntityItems->size() == 0) {
|
if (customEntityItems_->size() == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it's info or values...
|
// if it's info or values...
|
||||||
if (strlen(cmd) == 0 || Helpers::toLower(cmd) == F_(values) || Helpers::toLower(cmd) == F_(info)) {
|
if (strlen(cmd) == 0 || Helpers::toLower(cmd) == F_(values) || Helpers::toLower(cmd) == F_(info)) {
|
||||||
// list all names
|
// list all names
|
||||||
for (const CustomEntityItem & entity : *customEntityItems) {
|
for (const CustomEntityItem & entity : *customEntityItems_) {
|
||||||
render_value(output, entity);
|
render_value(output, entity);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -290,7 +290,7 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd)
|
|||||||
attribute_s = breakp + 1;
|
attribute_s = breakp + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto & entity : *customEntityItems) {
|
for (const auto & entity : *customEntityItems_) {
|
||||||
if (Helpers::toLower(entity.name) == command_s) {
|
if (Helpers::toLower(entity.name) == command_s) {
|
||||||
output["name"] = entity.name;
|
output["name"] = entity.name;
|
||||||
output["ram"] = entity.ram;
|
output["ram"] = entity.ram;
|
||||||
@@ -367,12 +367,11 @@ void WebCustomEntityService::publish(const bool force) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; });
|
if (customEntityItems_->size() == 0) {
|
||||||
if (customEntityItems->size() == 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Mqtt::publish_single() && force) {
|
if (Mqtt::publish_single() && force) {
|
||||||
for (const CustomEntityItem & entityItem : *customEntityItems) {
|
for (const CustomEntityItem & entityItem : *customEntityItems_) {
|
||||||
publish_single(entityItem);
|
publish_single(entityItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,7 +380,7 @@ void WebCustomEntityService::publish(const bool force) {
|
|||||||
JsonObject output = doc.to<JsonObject>();
|
JsonObject output = doc.to<JsonObject>();
|
||||||
bool ha_created = ha_registered_;
|
bool ha_created = ha_registered_;
|
||||||
|
|
||||||
for (const CustomEntityItem & entityItem : *customEntityItems) {
|
for (const CustomEntityItem & entityItem : *customEntityItems_) {
|
||||||
render_value(output, entityItem);
|
render_value(output, entityItem);
|
||||||
// create HA config
|
// create HA config
|
||||||
if (Mqtt::ha_enabled() && !ha_registered_) {
|
if (Mqtt::ha_enabled() && !ha_registered_) {
|
||||||
@@ -458,15 +457,14 @@ void WebCustomEntityService::publish(const bool force) {
|
|||||||
|
|
||||||
// count only entities with valid value or command to show in dashboard
|
// count only entities with valid value or command to show in dashboard
|
||||||
uint8_t WebCustomEntityService::count_entities() {
|
uint8_t WebCustomEntityService::count_entities() {
|
||||||
EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; });
|
if (customEntityItems_->size() == 0) {
|
||||||
if (customEntityItems->size() == 0) {
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
JsonObject output = doc.to<JsonObject>();
|
JsonObject output = doc.to<JsonObject>();
|
||||||
uint8_t count = 0;
|
uint8_t count = 0;
|
||||||
for (const CustomEntityItem & entity : *customEntityItems) {
|
for (const CustomEntityItem & entity : *customEntityItems_) {
|
||||||
render_value(output, entity);
|
render_value(output, entity);
|
||||||
count += (output.containsKey(entity.name) || entity.writeable) ? 1 : 0;
|
count += (output.containsKey(entity.name) || entity.writeable) ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -475,9 +473,8 @@ uint8_t WebCustomEntityService::count_entities() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint8_t WebCustomEntityService::has_commands() {
|
uint8_t WebCustomEntityService::has_commands() {
|
||||||
EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; });
|
|
||||||
uint8_t count = 0;
|
uint8_t count = 0;
|
||||||
for (const CustomEntityItem & entity : *customEntityItems) {
|
for (const CustomEntityItem & entity : *customEntityItems_) {
|
||||||
count += entity.writeable ? 1 : 0;
|
count += entity.writeable ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,12 +483,10 @@ uint8_t WebCustomEntityService::has_commands() {
|
|||||||
|
|
||||||
// send to dashboard, msgpack don't like serialized, use number
|
// send to dashboard, msgpack don't like serialized, use number
|
||||||
void WebCustomEntityService::generate_value_web(JsonObject output) {
|
void WebCustomEntityService::generate_value_web(JsonObject output) {
|
||||||
EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; });
|
|
||||||
|
|
||||||
output["label"] = (std::string) "Custom Entities";
|
output["label"] = (std::string) "Custom Entities";
|
||||||
JsonArray data = output["data"].to<JsonArray>();
|
JsonArray data = output["data"].to<JsonArray>();
|
||||||
uint8_t index = 0;
|
uint8_t index = 0;
|
||||||
for (const CustomEntityItem & entity : *customEntityItems) {
|
for (const CustomEntityItem & entity : *customEntityItems_) {
|
||||||
JsonObject obj = data.add<JsonObject>(); // create the object, we know there is a value
|
JsonObject obj = data.add<JsonObject>(); // create the object, we know there is a value
|
||||||
obj["id"] = "00" + entity.name;
|
obj["id"] = "00" + entity.name;
|
||||||
obj["u"] = entity.uom;
|
obj["u"] = entity.uom;
|
||||||
@@ -559,10 +554,9 @@ void WebCustomEntityService::generate_value_web(JsonObject output) {
|
|||||||
|
|
||||||
// fetch telegram, called from emsesp::fetch
|
// fetch telegram, called from emsesp::fetch
|
||||||
void WebCustomEntityService::fetch() {
|
void WebCustomEntityService::fetch() {
|
||||||
EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; });
|
|
||||||
const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3, 4};
|
const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3, 4};
|
||||||
|
|
||||||
for (auto & entity : *customEntityItems) {
|
for (auto & entity : *customEntityItems_) {
|
||||||
if (entity.device_id > 0 && entity.type_id > 0) { // ths excludes also RAM type
|
if (entity.device_id > 0 && entity.type_id > 0) { // ths excludes also RAM type
|
||||||
bool needFetch = true;
|
bool needFetch = true;
|
||||||
uint8_t fetchblock = entity.type_id > 0x0FF ? 25 : 27;
|
uint8_t fetchblock = entity.type_id > 0x0FF ? 25 : 27;
|
||||||
@@ -590,10 +584,9 @@ void WebCustomEntityService::fetch() {
|
|||||||
// called on process telegram, read from telegram
|
// called on process telegram, read from telegram
|
||||||
bool WebCustomEntityService::get_value(std::shared_ptr<const Telegram> telegram) {
|
bool WebCustomEntityService::get_value(std::shared_ptr<const Telegram> telegram) {
|
||||||
bool has_change = false;
|
bool has_change = false;
|
||||||
EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; });
|
// read-length of BOOL, INT8, UINT8, INT16, UINT16, UINT24, TIME, UINT32
|
||||||
// read-length of BOOL, INT, UINT, SHORT, USHORT, ULONG, TIME
|
|
||||||
const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3, 4};
|
const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3, 4};
|
||||||
for (auto & entity : *customEntityItems) {
|
for (auto & entity : *customEntityItems_) {
|
||||||
if (entity.value_type == DeviceValueType::STRING && telegram->type_id == entity.type_id && telegram->src == entity.device_id
|
if (entity.value_type == DeviceValueType::STRING && telegram->type_id == entity.type_id && telegram->src == entity.device_id
|
||||||
&& telegram->offset <= entity.offset && (telegram->offset + telegram->message_length) >= (entity.offset + (uint8_t)entity.factor)) {
|
&& telegram->offset <= entity.offset && (telegram->offset + telegram->message_length) >= (entity.offset + (uint8_t)entity.factor)) {
|
||||||
auto data = Helpers::data_to_hex(telegram->message_data, (uint8_t)entity.factor);
|
auto data = Helpers::data_to_hex(telegram->message_data, (uint8_t)entity.factor);
|
||||||
@@ -604,6 +597,9 @@ bool WebCustomEntityService::get_value(std::shared_ptr<const Telegram> telegram)
|
|||||||
} else if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
} else if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
||||||
has_change = true;
|
has_change = true;
|
||||||
}
|
}
|
||||||
|
char cmd[COMMAND_MAX_LENGTH];
|
||||||
|
snprintf(cmd, sizeof(cmd_function_p), "custom/%s", entity.name.c_str());
|
||||||
|
EMSESP::webSchedulerService.onChange(cmd);
|
||||||
}
|
}
|
||||||
} else if (entity.value_type != DeviceValueType::STRING && telegram->type_id == entity.type_id && telegram->src == entity.device_id
|
} else if (entity.value_type != DeviceValueType::STRING && telegram->type_id == entity.type_id && telegram->src == entity.device_id
|
||||||
&& telegram->offset <= entity.offset && (telegram->offset + telegram->message_length) >= (entity.offset + len[entity.value_type])) {
|
&& telegram->offset <= entity.offset && (telegram->offset + telegram->message_length) >= (entity.offset + len[entity.value_type])) {
|
||||||
@@ -618,6 +614,9 @@ bool WebCustomEntityService::get_value(std::shared_ptr<const Telegram> telegram)
|
|||||||
} else if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
} else if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
||||||
has_change = true;
|
has_change = true;
|
||||||
}
|
}
|
||||||
|
char cmd[COMMAND_MAX_LENGTH];
|
||||||
|
snprintf(cmd, sizeof(cmd_function_p), "%s/%s", "custom", entity.name.c_str());
|
||||||
|
EMSESP::webSchedulerService.onChange(cmd);
|
||||||
}
|
}
|
||||||
// EMSESP::logger().debug("custom entity %s received with value %d", entity.name.c_str(), (int)entity.val);
|
// EMSESP::logger().debug("custom entity %s received with value %d", entity.name.c_str(), (int)entity.val);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,8 +43,7 @@ class CustomEntityItem {
|
|||||||
|
|
||||||
class WebCustomEntity {
|
class WebCustomEntity {
|
||||||
public:
|
public:
|
||||||
std::vector<CustomEntityItem> customEntityItems;
|
std::list<CustomEntityItem> customEntityItems;
|
||||||
// std::list<CustomEntityItem> customEntityItems;
|
|
||||||
|
|
||||||
static void read(WebCustomEntity & webEntity, JsonObject root);
|
static void read(WebCustomEntity & webEntity, JsonObject root);
|
||||||
static StateUpdateResult update(JsonObject root, WebCustomEntity & webEntity);
|
static StateUpdateResult update(JsonObject root, WebCustomEntity & webEntity);
|
||||||
@@ -57,7 +56,7 @@ class WebCustomEntityService : public StatefulService<WebCustomEntity> {
|
|||||||
void begin();
|
void begin();
|
||||||
void publish_single(const CustomEntityItem & entity);
|
void publish_single(const CustomEntityItem & entity);
|
||||||
void publish(const bool force = false);
|
void publish(const bool force = false);
|
||||||
bool command_setvalue(const char * value, const std::string name);
|
bool command_setvalue(const char * value, const int8_t id, const char * name);
|
||||||
bool get_value_info(JsonObject output, const char * cmd);
|
bool get_value_info(JsonObject output, const char * cmd);
|
||||||
bool get_value(std::shared_ptr<const Telegram> telegram);
|
bool get_value(std::shared_ptr<const Telegram> telegram);
|
||||||
void fetch();
|
void fetch();
|
||||||
@@ -79,8 +78,7 @@ class WebCustomEntityService : public StatefulService<WebCustomEntity> {
|
|||||||
HttpEndpoint<WebCustomEntity> _httpEndpoint;
|
HttpEndpoint<WebCustomEntity> _httpEndpoint;
|
||||||
FSPersistence<WebCustomEntity> _fsPersistence;
|
FSPersistence<WebCustomEntity> _fsPersistence;
|
||||||
|
|
||||||
std::vector<CustomEntityItem> * customEntityItems; // pointer to the list of entity items
|
std::list<CustomEntityItem> * customEntityItems_; // pointer to the list of entity items
|
||||||
// std::list<CustomEntityItem> * customEntityItems; // pointer to the list of entity items
|
|
||||||
|
|
||||||
bool ha_registered_ = false;
|
bool ha_registered_ = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,11 +57,8 @@ void WebScheduler::read(WebScheduler & webScheduler, JsonObject root) {
|
|||||||
// call on initialization and also when the Schedule web page is saved
|
// call on initialization and also when the Schedule web page is saved
|
||||||
// this loads the data into the internal class
|
// this loads the data into the internal class
|
||||||
StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webScheduler) {
|
StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webScheduler) {
|
||||||
for (ScheduleItem & scheduleItem : webScheduler.scheduleItems) {
|
|
||||||
Command::erase_command(EMSdevice::DeviceType::SCHEDULER, scheduleItem.name.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset the list
|
// reset the list
|
||||||
|
Command::erase_device_commands(EMSdevice::DeviceType::SCHEDULER);
|
||||||
webScheduler.scheduleItems.clear();
|
webScheduler.scheduleItems.clear();
|
||||||
EMSESP::webSchedulerService.ha_reset();
|
EMSESP::webSchedulerService.ha_reset();
|
||||||
|
|
||||||
@@ -88,7 +85,7 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu
|
|||||||
EMSdevice::DeviceType::SCHEDULER,
|
EMSdevice::DeviceType::SCHEDULER,
|
||||||
webScheduler.scheduleItems.back().name.c_str(),
|
webScheduler.scheduleItems.back().name.c_str(),
|
||||||
[webScheduler](const char * value, const int8_t id) {
|
[webScheduler](const char * value, const int8_t id) {
|
||||||
return EMSESP::webSchedulerService.command_setvalue(value, webScheduler.scheduleItems.back().name);
|
return EMSESP::webSchedulerService.command_setvalue(value, id, webScheduler.scheduleItems.back().name.c_str());
|
||||||
},
|
},
|
||||||
FL_(schedule_cmd),
|
FL_(schedule_cmd),
|
||||||
CommandFlag::ADMIN_ONLY);
|
CommandFlag::ADMIN_ONLY);
|
||||||
@@ -102,20 +99,20 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set active by api command
|
// set active by api command
|
||||||
bool WebSchedulerService::command_setvalue(const char * value, const std::string name) {
|
bool WebSchedulerService::command_setvalue(const char * value, const int8_t id, const char * name) {
|
||||||
bool v;
|
bool v;
|
||||||
if (!Helpers::value2bool(value, v)) {
|
if (!Helpers::value2bool(value, v)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||||
if (scheduleItem.name == name) {
|
if (Helpers::toLower(scheduleItem.name) == Helpers::toLower(name)) {
|
||||||
if (scheduleItem.active == v) {
|
if (scheduleItem.active == v) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleItem.active = v;
|
scheduleItem.active = v;
|
||||||
publish_single(name.c_str(), v);
|
publish_single(name, v);
|
||||||
|
|
||||||
if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
||||||
publish();
|
publish();
|
||||||
@@ -212,7 +209,7 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) {
|
|||||||
|
|
||||||
// publish single value
|
// publish single value
|
||||||
void WebSchedulerService::publish_single(const char * name, const bool state) {
|
void WebSchedulerService::publish_single(const char * name, const bool state) {
|
||||||
if (!Mqtt::publish_single() || name == nullptr || name[0] == '\0') {
|
if (!Mqtt::enabled() || !Mqtt::publish_single() || name == nullptr || name[0] == '\0') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,26 +364,66 @@ bool WebSchedulerService::command(const char * cmd, const char * data) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include "shuntingYard.hpp"
|
||||||
|
|
||||||
|
bool WebSchedulerService::onChange(const char * cmd) {
|
||||||
|
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||||
|
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_ONCHANGE && Helpers::toLower(scheduleItem.time) == Helpers::toLower(cmd)) {
|
||||||
|
#ifdef EMESESP_DEBUG
|
||||||
|
// emsesp::EMSESP::logger().debug(scheduleItem.cmd.c_str());
|
||||||
|
#endif
|
||||||
|
return command(scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSchedulerService::condition() {
|
||||||
|
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||||
|
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_CONDITION) {
|
||||||
|
auto match = compute(scheduleItem.time.c_str());
|
||||||
|
#ifdef EMESESP_DEBUG
|
||||||
|
// emsesp::EMSESP::logger().debug("condition match: %s", match.c_str());
|
||||||
|
#endif
|
||||||
|
if (!match.empty() && match[0] == '1') {
|
||||||
|
if (scheduleItem.retry_cnt == 0xFF) { // default unswitched
|
||||||
|
scheduleItem.retry_cnt = command(scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()).c_str()) ? 1 : 0xFF;
|
||||||
|
}
|
||||||
|
} else if (scheduleItem.retry_cnt == 1) {
|
||||||
|
scheduleItem.retry_cnt = 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// process any scheduled jobs
|
// process any scheduled jobs
|
||||||
// checks on the minute and at startup
|
// checks on the minute and at startup
|
||||||
void WebSchedulerService::loop() {
|
void WebSchedulerService::loop() {
|
||||||
// initialize static value on startup
|
// initialize static value on startup
|
||||||
static int8_t last_tm_min = -1; // invalid value also used for startup commands
|
static int8_t last_tm_min = -2; // invalid value also used for startup commands
|
||||||
static uint32_t last_uptime_min = 0;
|
static uint32_t last_uptime_min = 0;
|
||||||
|
static uint32_t last_uptime_sec = 0;
|
||||||
|
|
||||||
// get list of scheduler events and exit if it's empty
|
// get list of scheduler events and exit if it's empty
|
||||||
if (scheduleItems_->size() == 0) {
|
if (scheduleItems_->size() == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check conditions every 10 seconds
|
||||||
|
uint32_t uptime_sec = uuid::get_uptime_sec() / 10;
|
||||||
|
if (last_uptime_sec != uptime_sec) {
|
||||||
|
condition();
|
||||||
|
last_uptime_sec = uptime_sec;
|
||||||
|
}
|
||||||
|
|
||||||
// check startup commands
|
// check startup commands
|
||||||
if (last_tm_min == -1) {
|
if (last_tm_min == -2) {
|
||||||
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||||
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min == 0) {
|
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min == 0) {
|
||||||
scheduleItem.retry_cnt = command(scheduleItem.cmd.c_str(), scheduleItem.value.c_str()) ? 0xFF : 0;
|
scheduleItem.retry_cnt = command(scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()).c_str()) ? 0xFF : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
last_tm_min = 0; // startup done, now use for RTC
|
last_tm_min = -1; // startup done, now use for RTC
|
||||||
}
|
}
|
||||||
|
|
||||||
// check timer every minute, sync to EMS-ESP clock
|
// check timer every minute, sync to EMS-ESP clock
|
||||||
@@ -401,7 +438,7 @@ void WebSchedulerService::loop() {
|
|||||||
// scheduled timer commands
|
// scheduled timer commands
|
||||||
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min > 0
|
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min > 0
|
||||||
&& (uptime_min % scheduleItem.elapsed_min == 0)) {
|
&& (uptime_min % scheduleItem.elapsed_min == 0)) {
|
||||||
command(scheduleItem.cmd.c_str(), scheduleItem.value.c_str());
|
command(scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()).c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
last_uptime_min = uptime_min;
|
last_uptime_min = uptime_min;
|
||||||
@@ -416,8 +453,9 @@ void WebSchedulerService::loop() {
|
|||||||
uint16_t real_min = tm->tm_hour * 60 + tm->tm_min;
|
uint16_t real_min = tm->tm_hour * 60 + tm->tm_min;
|
||||||
|
|
||||||
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
|
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||||
if (scheduleItem.active && (real_dow & scheduleItem.flags) && real_min == scheduleItem.elapsed_min) {
|
uint8_t dow = scheduleItem.flags & SCHEDULEFLAG_SCHEDULE_TIMER ? 0 : scheduleItem.flags;
|
||||||
command(scheduleItem.cmd.c_str(), scheduleItem.value.c_str());
|
if (scheduleItem.active && (real_dow & dow) && real_min == scheduleItem.elapsed_min) {
|
||||||
|
command(scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()).c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
last_tm_min = tm->tm_min;
|
last_tm_min = tm->tm_min;
|
||||||
|
|||||||
@@ -22,8 +22,10 @@
|
|||||||
#define EMSESP_SCHEDULER_FILE "/config/emsespScheduler.json"
|
#define EMSESP_SCHEDULER_FILE "/config/emsespScheduler.json"
|
||||||
#define EMSESP_SCHEDULER_SERVICE_PATH "/rest/schedule" // GET and POST
|
#define EMSESP_SCHEDULER_SERVICE_PATH "/rest/schedule" // GET and POST
|
||||||
|
|
||||||
#define SCHEDULEFLAG_SCHEDULE_TIMER 0x80 // 7th bit for Timer
|
#define SCHEDULEFLAG_SCHEDULE_TIMER 0x80 // 7th bit for Timer
|
||||||
#define MAX_STARTUP_RETRIES 3 // retry the start-up commands x times
|
#define SCHEDULEFLAG_SCHEDULE_ONCHANGE 0x81 // 7th+1st bit for OnChange
|
||||||
|
#define SCHEDULEFLAG_SCHEDULE_CONDITION 0x82 // 7th+2nd bit for Condition
|
||||||
|
#define MAX_STARTUP_RETRIES 3 // retry the start-up commands x times
|
||||||
|
|
||||||
namespace emsesp {
|
namespace emsesp {
|
||||||
|
|
||||||
@@ -56,11 +58,12 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
|
|||||||
void publish_single(const char * name, const bool state);
|
void publish_single(const char * name, const bool state);
|
||||||
void publish(const bool force = false);
|
void publish(const bool force = false);
|
||||||
bool has_commands();
|
bool has_commands();
|
||||||
bool command_setvalue(const char * value, const std::string name);
|
bool command_setvalue(const char * value, const int8_t id, const char * name);
|
||||||
bool get_value_info(JsonObject output, const char * cmd);
|
bool get_value_info(JsonObject output, const char * cmd);
|
||||||
void ha_reset() {
|
void ha_reset() {
|
||||||
ha_registered_ = false;
|
ha_registered_ = false;
|
||||||
}
|
}
|
||||||
|
bool onChange(const char * cmd);
|
||||||
|
|
||||||
#if defined(EMSESP_TEST)
|
#if defined(EMSESP_TEST)
|
||||||
void test();
|
void test();
|
||||||
@@ -71,6 +74,7 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
|
|||||||
private:
|
private:
|
||||||
#endif
|
#endif
|
||||||
bool command(const char * cmd, const char * data);
|
bool command(const char * cmd, const char * data);
|
||||||
|
void condition();
|
||||||
|
|
||||||
HttpEndpoint<WebScheduler> _httpEndpoint;
|
HttpEndpoint<WebScheduler> _httpEndpoint;
|
||||||
FSPersistence<WebScheduler> _fsPersistence;
|
FSPersistence<WebScheduler> _fsPersistence;
|
||||||
|
|||||||
541
src/web/shuntingYard.hpp
Normal file
541
src/web/shuntingYard.hpp
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
// Shunting-yard Algorithm
|
||||||
|
// https://en.wikipedia.org/wiki/Shunting-yard_algorithm
|
||||||
|
//
|
||||||
|
// Implementation notes for unary operators by Austin Taylor
|
||||||
|
// https://stackoverflow.com/a/5240912
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// https://ideone.com/VocUTq
|
||||||
|
//
|
||||||
|
// License:
|
||||||
|
// If you use this code in binary / compiled / un-commented (removing all text comments) form,
|
||||||
|
// you can use it under CC0 license.
|
||||||
|
//
|
||||||
|
// But if you use this code as source code / readable text, since main content of this code is
|
||||||
|
// their notes, I recommend you to indicate notices which conform CC-BY-SA. For example,
|
||||||
|
//
|
||||||
|
// --- ---
|
||||||
|
// YOUR-CONTENT uses the following materials.
|
||||||
|
// (1) Wikipedia article [Shunting-yard algorithm](https://en.wikipedia.org/wiki/Shunting-yard_algorithm),
|
||||||
|
// which is released under the [Creative Commons Attribution-Share-Alike License 3.0](https://creativecommons.org/licenses/by-sa/3.0/).
|
||||||
|
// (2) [Implementation notes for unary operators in Shunting-Yard algorithm](https://stackoverflow.com/a/5240912) by Austin Taylor
|
||||||
|
// which is released under the [Creative Commons Attribution-Share-Alike License 2.5](https://creativecommons.org/licenses/by-sa/2.5/).
|
||||||
|
// --- ---
|
||||||
|
// copy from https://gist.github.com/t-mat/b9f681b7591cdae712f6
|
||||||
|
// modified MDvP, 06.2024
|
||||||
|
//
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <deque>
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
class Token {
|
||||||
|
public:
|
||||||
|
enum class Type {
|
||||||
|
Unknown,
|
||||||
|
Number,
|
||||||
|
String,
|
||||||
|
Operator,
|
||||||
|
Compare,
|
||||||
|
Logic,
|
||||||
|
Unary,
|
||||||
|
LeftParen,
|
||||||
|
RightParen,
|
||||||
|
};
|
||||||
|
|
||||||
|
Token(Type type, const std::string & s, int8_t precedence = -1, bool rightAssociative = false)
|
||||||
|
: type{type}
|
||||||
|
, str(s)
|
||||||
|
, precedence{precedence}
|
||||||
|
, rightAssociative{rightAssociative} {
|
||||||
|
}
|
||||||
|
|
||||||
|
const Type type;
|
||||||
|
const std::string str;
|
||||||
|
const int8_t precedence;
|
||||||
|
const bool rightAssociative;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::deque<Token> exprToTokens(const std::string & expr) {
|
||||||
|
std::deque<Token> tokens;
|
||||||
|
|
||||||
|
for (const auto * p = expr.c_str(); *p; ++p) {
|
||||||
|
if (isblank(*p)) {
|
||||||
|
// do nothing
|
||||||
|
} else if (*p >= 'a' && *p <= 'z') {
|
||||||
|
const auto * b = p;
|
||||||
|
while ((*p >= 'a' && *p <= 'z') || (*p == '_')) {
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
const auto s = std::string(b, p);
|
||||||
|
tokens.push_back(Token{Token::Type::String, s, -2});
|
||||||
|
--p;
|
||||||
|
} else if (*p == '"') {
|
||||||
|
++p;
|
||||||
|
const auto * b = p;
|
||||||
|
while (*p && *p != '"') {
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
const auto s = std::string(b, p);
|
||||||
|
tokens.push_back(Token{Token::Type::String, s, -3});
|
||||||
|
if (*p == '\0') {
|
||||||
|
--p;
|
||||||
|
}
|
||||||
|
} else if (*p == '\'') {
|
||||||
|
++p;
|
||||||
|
const auto * b = p;
|
||||||
|
while (*p && *p != '\'') {
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
const auto s = std::string(b, p);
|
||||||
|
tokens.push_back(Token{Token::Type::String, s, -3});
|
||||||
|
if (*p == '\0') {
|
||||||
|
--p;
|
||||||
|
}
|
||||||
|
} else if (isdigit(*p)) {
|
||||||
|
const auto * b = p;
|
||||||
|
while (isdigit(*p) || *p == '.') {
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
const auto s = std::string(b, p);
|
||||||
|
tokens.push_back(Token{Token::Type::Number, s, -4});
|
||||||
|
--p;
|
||||||
|
} else {
|
||||||
|
Token::Type token = Token::Type::Operator;
|
||||||
|
int8_t precedence = -1;
|
||||||
|
bool rightAssociative = false;
|
||||||
|
char c = *p;
|
||||||
|
switch (c) {
|
||||||
|
default:
|
||||||
|
token = Token::Type::Unknown;
|
||||||
|
break;
|
||||||
|
case '(':
|
||||||
|
token = Token::Type::LeftParen;
|
||||||
|
break;
|
||||||
|
case ')':
|
||||||
|
token = Token::Type::RightParen;
|
||||||
|
break;
|
||||||
|
case '^':
|
||||||
|
precedence = 4;
|
||||||
|
rightAssociative = true;
|
||||||
|
break;
|
||||||
|
case '*':
|
||||||
|
precedence = 3;
|
||||||
|
break;
|
||||||
|
case '/':
|
||||||
|
precedence = 3;
|
||||||
|
break;
|
||||||
|
case '%':
|
||||||
|
precedence = 3;
|
||||||
|
break;
|
||||||
|
case '+':
|
||||||
|
precedence = 2;
|
||||||
|
break;
|
||||||
|
case '-':
|
||||||
|
// If current token is '-'
|
||||||
|
// and if it is the first token, or preceded by another operator, or left-paren,
|
||||||
|
if (tokens.empty() || tokens.back().type == Token::Type::Operator || tokens.back().type == Token::Type::Compare
|
||||||
|
|| tokens.back().type == Token::Type::Logic || tokens.back().type == Token::Type::Unary || tokens.back().type == Token::Type::LeftParen) {
|
||||||
|
// it's unary '-'
|
||||||
|
// note#1 : 'm' is a special operator name for unary '-'
|
||||||
|
// note#2 : It has highest precedence than any of the infix operators
|
||||||
|
if (!tokens.empty() && tokens.back().str[0] == 'm') { // double unary minus
|
||||||
|
tokens.pop_back();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
token = Token::Type::Unary;
|
||||||
|
c = 'm';
|
||||||
|
precedence = 5;
|
||||||
|
} else {
|
||||||
|
// otherwise, it's binary '-'
|
||||||
|
precedence = 2;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '&':
|
||||||
|
if (p[1] == '&')
|
||||||
|
++p;
|
||||||
|
precedence = 0;
|
||||||
|
token = Token::Type::Logic;
|
||||||
|
break;
|
||||||
|
case '|':
|
||||||
|
if (p[1] == '|')
|
||||||
|
++p;
|
||||||
|
precedence = 0;
|
||||||
|
token = Token::Type::Logic;
|
||||||
|
break;
|
||||||
|
case '!':
|
||||||
|
if (p[1] == '=') {
|
||||||
|
++p;
|
||||||
|
precedence = 1;
|
||||||
|
token = Token::Type::Compare;
|
||||||
|
} else {
|
||||||
|
precedence = 1;
|
||||||
|
token = Token::Type::Unary;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '<':
|
||||||
|
if (p[1] == '=') {
|
||||||
|
++p;
|
||||||
|
c = '{';
|
||||||
|
}
|
||||||
|
precedence = 1;
|
||||||
|
token = Token::Type::Compare;
|
||||||
|
break;
|
||||||
|
case '>':
|
||||||
|
if (p[1] == '=') {
|
||||||
|
++p;
|
||||||
|
c = '}';
|
||||||
|
}
|
||||||
|
precedence = 1;
|
||||||
|
token = Token::Type::Compare;
|
||||||
|
break;
|
||||||
|
case '=':
|
||||||
|
if (p[1] == '=')
|
||||||
|
++p;
|
||||||
|
precedence = 1;
|
||||||
|
token = Token::Type::Compare;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto s = std::string(1, c);
|
||||||
|
tokens.push_back(Token{token, s, precedence, rightAssociative});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::deque<Token> shuntingYard(const std::deque<Token> & tokens) {
|
||||||
|
std::deque<Token> queue;
|
||||||
|
std::vector<Token> stack;
|
||||||
|
|
||||||
|
// While there are tokens to be read:
|
||||||
|
for (auto token : tokens) {
|
||||||
|
// Read a token
|
||||||
|
switch (token.type) {
|
||||||
|
case Token::Type::Number:
|
||||||
|
case Token::Type::String:
|
||||||
|
// If the token is a number, then add it to the output queue
|
||||||
|
queue.push_back(token);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Token::Type::Unary:
|
||||||
|
case Token::Type::Compare:
|
||||||
|
case Token::Type::Logic:
|
||||||
|
case Token::Type::Operator: {
|
||||||
|
// If the token is operator, o1, then:
|
||||||
|
const auto o1 = token;
|
||||||
|
|
||||||
|
// while there is an operator token,
|
||||||
|
while (!stack.empty()) {
|
||||||
|
// o2, at the top of stack, and
|
||||||
|
const auto o2 = stack.back();
|
||||||
|
|
||||||
|
// either o1 is left-associative and its precedence is
|
||||||
|
// *less than or equal* to that of o2,
|
||||||
|
// or o1 if right associative, and has precedence
|
||||||
|
// *less than* that of o2,
|
||||||
|
if ((!o1.rightAssociative && o1.precedence <= o2.precedence) || (o1.rightAssociative && o1.precedence < o2.precedence)) {
|
||||||
|
// then pop o2 off the stack,
|
||||||
|
stack.pop_back();
|
||||||
|
// onto the output queue;
|
||||||
|
queue.push_back(o2);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @@ otherwise, exit.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// push o1 onto the stack.
|
||||||
|
stack.push_back(o1);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case Token::Type::LeftParen:
|
||||||
|
// If token is left parenthesis, then push it onto the stack
|
||||||
|
stack.push_back(token);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Token::Type::RightParen: {
|
||||||
|
// If token is right parenthesis:
|
||||||
|
bool match = false;
|
||||||
|
|
||||||
|
// Until the token at the top of the stack
|
||||||
|
// is a left parenthesis,
|
||||||
|
while (!stack.empty() && stack.back().type != Token::Type::LeftParen) {
|
||||||
|
// pop operators off the stack
|
||||||
|
// onto the output queue.
|
||||||
|
queue.push_back(stack.back());
|
||||||
|
stack.pop_back();
|
||||||
|
match = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!match && stack.empty()) {
|
||||||
|
// If the stack runs out without finding a left parenthesis,
|
||||||
|
// then there are mismatched parentheses.
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop the left parenthesis from the stack,
|
||||||
|
// but not onto the output queue.
|
||||||
|
stack.pop_back();
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When there are no more tokens to read:
|
||||||
|
// While there are still operator tokens in the stack:
|
||||||
|
while (!stack.empty()) {
|
||||||
|
// If the operator token on the top of the stack is a parenthesis,
|
||||||
|
// then there are mismatched parentheses.
|
||||||
|
if (stack.back().type == Token::Type::LeftParen) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop the operator onto the output queue.
|
||||||
|
queue.push_back(std::move(stack.back()));
|
||||||
|
stack.pop_back();
|
||||||
|
}
|
||||||
|
return queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isnum(const std::string & s) {
|
||||||
|
if (s.find_first_not_of("0123456789.") == std::string::npos || (s[0] == '-' && s.find_first_not_of("0123456789.", 1) == std::string::npos)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// replace commands like "<device>/<hc>/<cmd>" with its value"
|
||||||
|
std::string commands(std::string & expr) {
|
||||||
|
for (uint8_t device = 0; device < emsesp::EMSdevice::DeviceType::UNKNOWN; device++) {
|
||||||
|
const char * d = emsesp::EMSdevice::device_type_2_device_name(device);
|
||||||
|
auto f = expr.find(d);
|
||||||
|
while (f != std::string::npos) {
|
||||||
|
auto e = expr.find_first_of(")=<>|&+-*!", f);
|
||||||
|
if (e == std::string::npos) {
|
||||||
|
e = expr.length();
|
||||||
|
}
|
||||||
|
while (e > 0 && expr[e - 1] == ' ') { // remove blanks from end
|
||||||
|
e--;
|
||||||
|
}
|
||||||
|
char cmd[COMMAND_MAX_LENGTH];
|
||||||
|
size_t l = e - f;
|
||||||
|
if (l >= sizeof(cmd) - 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
expr.copy(cmd, l, f);
|
||||||
|
cmd[l] = '\0';
|
||||||
|
if (strstr(cmd, "/value") == nullptr) {
|
||||||
|
strlcat(cmd, "/value", sizeof(cmd) - 6);
|
||||||
|
}
|
||||||
|
JsonDocument doc_out, doc_in;
|
||||||
|
JsonObject output = doc_out.to<JsonObject>();
|
||||||
|
JsonObject input = doc_in.to<JsonObject>();
|
||||||
|
std::string cmd_s = "api/" + std::string(cmd);
|
||||||
|
emsesp::Command::process(cmd_s.c_str(), true, input, output);
|
||||||
|
if (output.containsKey("api_data")) {
|
||||||
|
std::string data = output["api_data"].as<std::string>();
|
||||||
|
if (!isnum(data)) {
|
||||||
|
data.insert(data.begin(), '"');
|
||||||
|
data.insert(data.end(), '"');
|
||||||
|
}
|
||||||
|
expr.replace(f, l, data);
|
||||||
|
e = f + data.length();
|
||||||
|
} else {
|
||||||
|
return expr = "";
|
||||||
|
}
|
||||||
|
f = expr.find(d, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int to_logic(const std::string & s) {
|
||||||
|
if (s[0] == '1' || s == "on" || s == "ON" || s == "true") {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (s[0] == '0' || s == "off" || s == "OFF" || s == "false") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string to_string(double d) {
|
||||||
|
if (d == static_cast<int>(d)) {
|
||||||
|
return std::to_string(static_cast<int>(d));
|
||||||
|
}
|
||||||
|
return std::to_string(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string compute(const std::string & expr) {
|
||||||
|
auto expr_new = expr; //emsesp::Helpers::toLower(expr);
|
||||||
|
// emsesp::EMSESP::logger().info("calculate: %s", expr_new.c_str());
|
||||||
|
commands(expr_new);
|
||||||
|
// emsesp::EMSESP::logger().info("calculate: %s", expr_new.c_str());
|
||||||
|
const auto tokens = exprToTokens(expr_new);
|
||||||
|
if (tokens.empty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
auto queue = shuntingYard(tokens);
|
||||||
|
if (queue.empty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
std::vector<std::string> stack;
|
||||||
|
|
||||||
|
while (!queue.empty()) {
|
||||||
|
const auto token = queue.front();
|
||||||
|
queue.pop_front();
|
||||||
|
switch (token.type) {
|
||||||
|
case Token::Type::Number:
|
||||||
|
case Token::Type::String:
|
||||||
|
stack.push_back(token.str);
|
||||||
|
break;
|
||||||
|
case Token::Type::Unary: {
|
||||||
|
if (stack.empty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const auto rhs = stack.back();
|
||||||
|
stack.pop_back();
|
||||||
|
switch (token.str[0]) {
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
break;
|
||||||
|
case 'm': // Special operator name for unary '-'
|
||||||
|
if (!isnum(rhs)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
stack.push_back(to_string(-1 * std::stod(rhs)));
|
||||||
|
break;
|
||||||
|
case '!':
|
||||||
|
stack.push_back(to_logic(rhs) == 0 ? "1" : "0");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
} break;
|
||||||
|
case Token::Type::Compare: {
|
||||||
|
if (stack.size() < 2) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const auto rhs = stack.back();
|
||||||
|
stack.pop_back();
|
||||||
|
const auto lhs = stack.back();
|
||||||
|
stack.pop_back();
|
||||||
|
switch (token.str[0]) {
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
break;
|
||||||
|
case '<':
|
||||||
|
if (isnum(rhs) && isnum(lhs)) {
|
||||||
|
stack.push_back((std::stod(lhs) < std::stod(rhs)) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
stack.push_back((lhs < rhs) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
case '{':
|
||||||
|
if (isnum(rhs) && isnum(lhs)) {
|
||||||
|
stack.push_back((std::stod(lhs) <= std::stod(rhs)) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
stack.push_back((lhs <= rhs) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
case '>':
|
||||||
|
if (isnum(rhs) && isnum(lhs)) {
|
||||||
|
stack.push_back((std::stod(lhs) > std::stod(rhs)) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
stack.push_back((lhs > rhs) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
case '}':
|
||||||
|
if (isnum(rhs) && isnum(lhs)) {
|
||||||
|
stack.push_back((std::stod(lhs) >= std::stod(rhs)) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
stack.push_back((lhs >= rhs) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
case '=':
|
||||||
|
if (isnum(rhs) && isnum(lhs)) {
|
||||||
|
stack.push_back((std::stod(lhs) == std::stod(rhs)) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
stack.push_back((lhs == rhs) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
case '!':
|
||||||
|
if (isnum(rhs) && isnum(lhs)) {
|
||||||
|
stack.push_back((std::stod(lhs) != std::stod(rhs)) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
stack.push_back((lhs != rhs) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case Token::Type::Logic: {
|
||||||
|
// binary operators
|
||||||
|
if (stack.size() < 2) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const auto rhs = to_logic(stack.back());
|
||||||
|
stack.pop_back();
|
||||||
|
const auto lhs = to_logic(stack.back());
|
||||||
|
stack.pop_back();
|
||||||
|
switch (token.str[0]) {
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
break;
|
||||||
|
case '&':
|
||||||
|
stack.push_back((lhs && rhs) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
case '|':
|
||||||
|
stack.push_back((lhs || rhs) ? "1" : "0");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case Token::Type::Operator: {
|
||||||
|
// binary operators
|
||||||
|
if (stack.empty() || !isnum(stack.back())) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const auto rhs = std::stod(stack.back());
|
||||||
|
stack.pop_back();
|
||||||
|
if (stack.empty() || !isnum(stack.back())) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const auto lhs = std::stod(stack.back());
|
||||||
|
stack.pop_back();
|
||||||
|
|
||||||
|
switch (token.str[0]) {
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
break;
|
||||||
|
case '^':
|
||||||
|
stack.push_back(to_string(pow(lhs, rhs)));
|
||||||
|
break;
|
||||||
|
case '*':
|
||||||
|
stack.push_back(to_string(lhs * rhs));
|
||||||
|
break;
|
||||||
|
case '/':
|
||||||
|
stack.push_back(to_string(lhs / rhs));
|
||||||
|
break;
|
||||||
|
case '%':
|
||||||
|
stack.push_back(std::to_string(static_cast<int>(lhs) % static_cast<int>(rhs)));
|
||||||
|
break;
|
||||||
|
case '+':
|
||||||
|
stack.push_back(to_string(lhs + rhs));
|
||||||
|
break;
|
||||||
|
case '-':
|
||||||
|
stack.push_back(to_string(lhs - rhs));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stack.back();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user