mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
scheduler: onChange and conditions
This commit is contained in:
@@ -87,7 +87,7 @@ const Scheduler: FC = () => {
|
||||
|
||||
const schedule_theme = useTheme({
|
||||
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: `
|
||||
font-size: 14px;
|
||||
@@ -198,7 +198,7 @@ const Scheduler: FC = () => {
|
||||
active: false,
|
||||
deleted: false,
|
||||
flags: 0,
|
||||
time: '12:00',
|
||||
time: '',
|
||||
cmd: '',
|
||||
value: '',
|
||||
name: ''
|
||||
@@ -216,11 +216,21 @@ const Scheduler: FC = () => {
|
||||
<Box>
|
||||
<Typography
|
||||
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
|
||||
? 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>
|
||||
</Box>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
@@ -245,7 +255,7 @@ const Scheduler: FC = () => {
|
||||
<HeaderRow>
|
||||
<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.VALUE(0)}</HeaderCell>
|
||||
<HeaderCell stiff>{LL.NAME(0)}</HeaderCell>
|
||||
@@ -268,16 +278,25 @@ const Scheduler: FC = () => {
|
||||
)}
|
||||
</Cell>
|
||||
<Cell stiff>
|
||||
<Stack spacing={1} direction="row">
|
||||
<Stack spacing={0.5} direction="row">
|
||||
<Divider orientation="vertical" flexItem />
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_MON)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_TUE)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_WED)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_THU)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_FRI)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_SAT)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_SUN)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_TIMER)}
|
||||
{si.flags < ScheduleFlag.SCHEDULE_TIMER ? (
|
||||
<>
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_MON)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_TUE)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_WED)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_THU)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_FRI)}
|
||||
{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>
|
||||
</Cell>
|
||||
<Cell>{si.time}</Cell>
|
||||
@@ -341,7 +360,7 @@ const Scheduler: FC = () => {
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
color="primary"
|
||||
onClick={addScheduleItem}
|
||||
>
|
||||
{LL.ADD(0)}
|
||||
|
||||
@@ -91,10 +91,10 @@ const SchedulerDialog = ({
|
||||
|
||||
const getFlagString = (f: number) => {
|
||||
const new_flags: string[] = [];
|
||||
if ((f & 1) === 1) {
|
||||
if ((f & 129) === 1) {
|
||||
new_flags.push('1');
|
||||
}
|
||||
if ((f & 2) === 2) {
|
||||
if ((f & 130) === 2) {
|
||||
new_flags.push('2');
|
||||
}
|
||||
if ((f & 4) === 4) {
|
||||
@@ -112,26 +112,45 @@ const SchedulerDialog = ({
|
||||
if ((f & 64) === 64) {
|
||||
new_flags.push('64');
|
||||
}
|
||||
if ((f & 128) === 128) {
|
||||
if ((f & 131) === 128) {
|
||||
new_flags.push('128');
|
||||
}
|
||||
if ((f & 131) === 129) {
|
||||
new_flags.push('129');
|
||||
}
|
||||
if ((f & 131) === 130) {
|
||||
new_flags.push('130');
|
||||
}
|
||||
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) => (
|
||||
<Typography
|
||||
variant="button"
|
||||
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
|
||||
? 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>
|
||||
);
|
||||
|
||||
const isTimer = editItem.flags === ScheduleFlag.SCHEDULE_TIMER;
|
||||
|
||||
return (
|
||||
<Dialog sx={dialogStyle} open={open} onClose={close}>
|
||||
<DialogTitle>
|
||||
@@ -179,7 +198,7 @@ const SchedulerDialog = ({
|
||||
sx={{ bgcolor: '#334f65' }}
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setEditItem({ ...editItem, flags: 0 });
|
||||
setEditItem({ ...editItem, flags: ScheduleFlag.SCHEDULE_TIMER });
|
||||
}}
|
||||
>
|
||||
{showFlag(editItem, ScheduleFlag.SCHEDULE_TIMER)}
|
||||
@@ -199,6 +218,66 @@ const SchedulerDialog = ({
|
||||
</Button>
|
||||
)}
|
||||
</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>
|
||||
<Grid container>
|
||||
<BlockFormControlLabel
|
||||
@@ -213,18 +292,31 @@ const SchedulerDialog = ({
|
||||
/>
|
||||
</Grid>
|
||||
<Grid container>
|
||||
<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>
|
||||
{isCondition || isOnChange ? (
|
||||
<TextField
|
||||
name="time"
|
||||
label={isCondition ? 'Condition' : 'On Change Value'}
|
||||
fullWidth
|
||||
value={editItem.time}
|
||||
margin="normal"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
<ValidatedTextField
|
||||
|
||||
@@ -334,7 +334,9 @@ export enum ScheduleFlag {
|
||||
SCHEDULE_THU = 16,
|
||||
SCHEDULE_FRI = 32,
|
||||
SCHEDULE_SAT = 64,
|
||||
SCHEDULE_TIMER = 128
|
||||
SCHEDULE_TIMER = 128,
|
||||
SCHEDULE_ONCHANGE = 129,
|
||||
SCHEDULE_CONDITION = 130
|
||||
}
|
||||
|
||||
export interface EntityItem {
|
||||
|
||||
@@ -409,6 +409,9 @@ void AnalogSensor::publish_sensor(const Sensor & sensor) const {
|
||||
char payload[10];
|
||||
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
|
||||
|
||||
@@ -105,6 +105,10 @@ const char * EMSdevice::device_type_2_device_name(const uint8_t device_type) {
|
||||
switch (device_type) {
|
||||
case DeviceType::SYSTEM:
|
||||
return F_(system);
|
||||
case DeviceType::TEMPERATURESENSOR:
|
||||
return F_(temperaturesensor);
|
||||
case DeviceType::ANALOGSENSOR:
|
||||
return F_(analogsensor);
|
||||
case DeviceType::SCHEDULER:
|
||||
return F_(scheduler);
|
||||
case DeviceType::CUSTOM:
|
||||
@@ -113,28 +117,26 @@ const char * EMSdevice::device_type_2_device_name(const uint8_t device_type) {
|
||||
return F_(boiler);
|
||||
case DeviceType::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:
|
||||
return F_(mixer);
|
||||
case DeviceType::TEMPERATURESENSOR:
|
||||
return F_(temperaturesensor);
|
||||
case DeviceType::ANALOGSENSOR:
|
||||
return F_(analogsensor);
|
||||
case DeviceType::CONTROLLER:
|
||||
return F_(controller);
|
||||
case DeviceType::SWITCH:
|
||||
return F_(switch);
|
||||
case DeviceType::SOLAR:
|
||||
return F_(solar);
|
||||
case DeviceType::HEATPUMP:
|
||||
return F_(heatpump);
|
||||
case DeviceType::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:
|
||||
return F_(alert);
|
||||
case DeviceType::EXTENSION:
|
||||
return F_(extension);
|
||||
case DeviceType::GENERIC:
|
||||
return F_(generic);
|
||||
case DeviceType::HEATSOURCE:
|
||||
return F_(heatsource);
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -820,9 +823,17 @@ void EMSdevice::publish_value(void * value_p) const {
|
||||
break;
|
||||
}
|
||||
|
||||
if (payload[0] != '\0') {
|
||||
if (Mqtt::publish_single() && payload[0] != '\0') {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,6 +441,9 @@ void TemperatureSensor::publish_sensor(const Sensor & sensor) {
|
||||
char payload[10];
|
||||
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
|
||||
|
||||
@@ -175,6 +175,9 @@ bool WebCustomEntityService::command_setvalue(const char * value, const std::str
|
||||
if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
||||
publish();
|
||||
}
|
||||
char cmd[COMMAND_MAX_LENGTH];
|
||||
snprintf(cmd, sizeof(cmd_function_p), "custom/%s", entityItem.name.c_str());
|
||||
EMSESP::webSchedulerService.onChange(cmd);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -591,7 +594,7 @@ void WebCustomEntityService::fetch() {
|
||||
bool WebCustomEntityService::get_value(std::shared_ptr<const Telegram> telegram) {
|
||||
bool has_change = false;
|
||||
EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; });
|
||||
// read-length of BOOL, INT, UINT, SHORT, USHORT, ULONG, TIME
|
||||
// read-length of BOOL, INT8, UINT8, INT16, UINT16, UINT24, TIME, UINT32
|
||||
const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3, 4};
|
||||
for (auto & entity : *customEntityItems) {
|
||||
if (entity.value_type == DeviceValueType::STRING && telegram->type_id == entity.type_id && telegram->src == entity.device_id
|
||||
@@ -604,6 +607,9 @@ bool WebCustomEntityService::get_value(std::shared_ptr<const Telegram> telegram)
|
||||
} else if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
||||
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
|
||||
&& telegram->offset <= entity.offset && (telegram->offset + telegram->message_length) >= (entity.offset + len[entity.value_type])) {
|
||||
@@ -618,6 +624,9 @@ bool WebCustomEntityService::get_value(std::shared_ptr<const Telegram> telegram)
|
||||
} else if (EMSESP::mqtt_.get_publish_onchange(0)) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -367,26 +367,64 @@ bool WebSchedulerService::command(const char * cmd, const char * data) {
|
||||
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)) {
|
||||
// emsesp::EMSESP::logger().debug(scheduleItem.cmd.c_str());
|
||||
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.c_str()[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
|
||||
// checks on the minute and at startup
|
||||
void WebSchedulerService::loop() {
|
||||
// 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_sec = 0;
|
||||
|
||||
// get list of scheduler events and exit if it's empty
|
||||
if (scheduleItems_->size() == 0) {
|
||||
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
|
||||
if (last_tm_min == -1) {
|
||||
if (last_tm_min == -2) {
|
||||
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||
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
|
||||
@@ -401,7 +439,7 @@ void WebSchedulerService::loop() {
|
||||
// scheduled timer commands
|
||||
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && 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;
|
||||
@@ -416,8 +454,9 @@ void WebSchedulerService::loop() {
|
||||
uint16_t real_min = tm->tm_hour * 60 + tm->tm_min;
|
||||
|
||||
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||
if (scheduleItem.active && (real_dow & scheduleItem.flags) && real_min == scheduleItem.elapsed_min) {
|
||||
command(scheduleItem.cmd.c_str(), scheduleItem.value.c_str());
|
||||
uint8_t dow = scheduleItem.flags & SCHEDULEFLAG_SCHEDULE_TIMER ? 0 : scheduleItem.flags;
|
||||
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;
|
||||
|
||||
@@ -22,8 +22,10 @@
|
||||
#define EMSESP_SCHEDULER_FILE "/config/emsespScheduler.json"
|
||||
#define EMSESP_SCHEDULER_SERVICE_PATH "/rest/schedule" // GET and POST
|
||||
|
||||
#define SCHEDULEFLAG_SCHEDULE_TIMER 0x80 // 7th bit for Timer
|
||||
#define MAX_STARTUP_RETRIES 3 // retry the start-up commands x times
|
||||
#define SCHEDULEFLAG_SCHEDULE_TIMER 0x80 // 7th bit for Timer
|
||||
#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 {
|
||||
|
||||
@@ -61,6 +63,7 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
|
||||
void ha_reset() {
|
||||
ha_registered_ = false;
|
||||
}
|
||||
bool onChange(const char * cmd);
|
||||
|
||||
#if defined(EMSESP_TEST)
|
||||
void test();
|
||||
@@ -71,6 +74,7 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
|
||||
private:
|
||||
#endif
|
||||
bool command(const char * cmd, const char * data);
|
||||
void condition();
|
||||
|
||||
HttpEndpoint<WebScheduler> _httpEndpoint;
|
||||
FSPersistence<WebScheduler> _fsPersistence;
|
||||
|
||||
404
src/web/shuntingYard.hpp
Normal file
404
src/web/shuntingYard.hpp
Normal file
@@ -0,0 +1,404 @@
|
||||
// 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,
|
||||
Operator,
|
||||
LeftParen,
|
||||
RightParen,
|
||||
};
|
||||
|
||||
Token(Type type, const std::string & s, int8_t precedence = -1, bool rightAssociative = false, bool unary = false)
|
||||
: type{type}
|
||||
, str(s)
|
||||
, precedence{precedence}
|
||||
, rightAssociative{rightAssociative}
|
||||
, unary{unary} {
|
||||
}
|
||||
|
||||
const Type type;
|
||||
const std::string str;
|
||||
const int8_t precedence;
|
||||
const bool rightAssociative;
|
||||
const bool unary;
|
||||
};
|
||||
|
||||
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')) {
|
||||
tokens.clear();
|
||||
return tokens;
|
||||
} 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});
|
||||
--p;
|
||||
} else {
|
||||
Token::Type token = Token::Type::Operator;
|
||||
int8_t precedence = -1;
|
||||
bool rightAssociative = false;
|
||||
bool unary = 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::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
|
||||
unary = true;
|
||||
c = 'm';
|
||||
precedence = 5;
|
||||
} else {
|
||||
// otherwise, it's binary '-'
|
||||
precedence = 2;
|
||||
}
|
||||
break;
|
||||
case '&':
|
||||
if (p[1] == '&')
|
||||
++p;
|
||||
precedence = 0;
|
||||
break;
|
||||
case '|':
|
||||
if (p[1] == '|')
|
||||
++p;
|
||||
precedence = 0;
|
||||
break;
|
||||
case '!':
|
||||
unary = true;
|
||||
precedence = 1;
|
||||
break;
|
||||
case '<':
|
||||
if (p[1] == '=') {
|
||||
++p;
|
||||
c = '{';
|
||||
}
|
||||
precedence = 1;
|
||||
break;
|
||||
case '>':
|
||||
if (p[1] == '=') {
|
||||
++p;
|
||||
c = '}';
|
||||
}
|
||||
precedence = 1;
|
||||
break;
|
||||
case '=':
|
||||
if (p[1] == '=')
|
||||
++p;
|
||||
precedence = 1;
|
||||
break;
|
||||
}
|
||||
const auto s = std::string(1, c);
|
||||
tokens.push_back(Token{token, s, precedence, rightAssociative, unary});
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
// If the token is a number, then add it to the output queue
|
||||
queue.push_back(token);
|
||||
break;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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(" )=<>|&+-*\0", f);
|
||||
if (e == std::string::npos) {
|
||||
e = expr.length();
|
||||
}
|
||||
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 (data == "true" || data == "ON" || data == "on") {
|
||||
data = "1";
|
||||
}
|
||||
if (data == "false" || data == "OFF" || data == "off") {
|
||||
data = "0";
|
||||
}
|
||||
expr.replace(f, l, data);
|
||||
e = f + data.length();
|
||||
}
|
||||
f = expr.find(d, e);
|
||||
}
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
std::string compute(const std::string & expr) {
|
||||
auto expr_new = emsesp::Helpers::toLower(expr);
|
||||
#ifdef EMESESP_DEBUG
|
||||
emsesp::EMSESP::logger().debug("calculate: %s", expr_new.c_str());
|
||||
#endif
|
||||
commands(expr_new);
|
||||
#ifdef EMESESP_DEBUG
|
||||
emsesp::EMSESP::logger().debug("calculate: %s", expr_new.c_str());
|
||||
#endif
|
||||
const auto tokens = exprToTokens(expr_new);
|
||||
if (tokens.empty()) {
|
||||
return "Error: no tokens";
|
||||
}
|
||||
auto queue = shuntingYard(tokens);
|
||||
std::vector<double> stack;
|
||||
|
||||
while (!queue.empty()) {
|
||||
const auto token = queue.front();
|
||||
queue.pop_front();
|
||||
switch (token.type) {
|
||||
case Token::Type::Number:
|
||||
stack.push_back(std::stod(token.str));
|
||||
break;
|
||||
|
||||
case Token::Type::Operator: {
|
||||
if (token.unary) {
|
||||
// unray operators
|
||||
const auto rhs = stack.back();
|
||||
stack.pop_back();
|
||||
switch (token.str[0]) {
|
||||
default:
|
||||
return "";
|
||||
break;
|
||||
case 'm': // Special operator name for unary '-'
|
||||
stack.push_back(-rhs);
|
||||
break;
|
||||
case '!':
|
||||
stack.push_back(!(int)rhs);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// binary operators
|
||||
const auto rhs = stack.back();
|
||||
stack.pop_back();
|
||||
const auto lhs = stack.back();
|
||||
stack.pop_back();
|
||||
|
||||
switch (token.str[0]) {
|
||||
default:
|
||||
return "";
|
||||
break;
|
||||
case '^':
|
||||
stack.push_back(static_cast<int>(pow(lhs, rhs)));
|
||||
break;
|
||||
case '*':
|
||||
stack.push_back(lhs * rhs);
|
||||
break;
|
||||
case '/':
|
||||
stack.push_back(lhs / rhs);
|
||||
break;
|
||||
case '%':
|
||||
stack.push_back((int)lhs % (int)rhs);
|
||||
break;
|
||||
case '+':
|
||||
stack.push_back(lhs + rhs);
|
||||
break;
|
||||
case '-':
|
||||
stack.push_back(lhs - rhs);
|
||||
break;
|
||||
case '&':
|
||||
stack.push_back(((int)lhs && (int)rhs) ? 1 : 0);
|
||||
break;
|
||||
case '|':
|
||||
stack.push_back(((int)lhs || (int)rhs) ? 1 : 0);
|
||||
break;
|
||||
case '<':
|
||||
stack.push_back(((int)lhs < (int)rhs) ? 1 : 0);
|
||||
break;
|
||||
case '{':
|
||||
stack.push_back(((int)lhs <= (int)rhs) ? 1 : 0);
|
||||
break;
|
||||
case '>':
|
||||
stack.push_back(((int)lhs > (int)rhs) ? 1 : 0);
|
||||
break;
|
||||
case '}':
|
||||
stack.push_back(((int)lhs >= (int)rhs) ? 1 : 0);
|
||||
break;
|
||||
case '=':
|
||||
stack.push_back(((int)lhs == (int)rhs) ? 1 : 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
if (stack.back() == (int)stack.back()) {
|
||||
return (std::to_string((int)stack.back()));
|
||||
}
|
||||
return std::to_string(stack.back());
|
||||
}
|
||||
Reference in New Issue
Block a user