mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 16:29:51 +03:00
Merge pull request #1899 from MichaelDvP/dev
scheduler single and fixes
This commit is contained in:
@@ -160,6 +160,9 @@ const Scheduler: FC = () => {
|
||||
setCreating(false);
|
||||
setSelectedScheduleItem(si);
|
||||
setDialogOpen(true);
|
||||
if (si.o_name === undefined) {
|
||||
si.o_name = si.name;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onDialogClose = () => {
|
||||
@@ -280,7 +283,9 @@ const Scheduler: FC = () => {
|
||||
<Cell stiff>
|
||||
<Stack spacing={0.5} direction="row">
|
||||
<Divider orientation="vertical" flexItem />
|
||||
{si.flags < ScheduleFlag.SCHEDULE_TIMER ? (
|
||||
{si.flags === 0 ? (
|
||||
<></>
|
||||
) : si.flags < ScheduleFlag.SCHEDULE_TIMER ? (
|
||||
<>
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_MON)}
|
||||
{dayBox(si, ScheduleFlag.SCHEDULE_TUE)}
|
||||
|
||||
@@ -72,6 +72,17 @@ const SchedulerDialog = ({
|
||||
}
|
||||
};
|
||||
|
||||
const saveandactivate = async () => {
|
||||
editItem.active = true;
|
||||
try {
|
||||
setFieldErrors(undefined);
|
||||
await validate(validator, editItem);
|
||||
onSave(editItem);
|
||||
} catch (error) {
|
||||
setFieldErrors(error as ValidateFieldsError);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
editItem.deleted = true;
|
||||
onSave(editItem);
|
||||
@@ -326,36 +337,36 @@ const SchedulerDialog = ({
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="cmd"
|
||||
label={LL.COMMAND(0)}
|
||||
multiline
|
||||
fullWidth
|
||||
value={editItem.cmd}
|
||||
margin="normal"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
<TextField
|
||||
name="value"
|
||||
label={LL.VALUE(0)}
|
||||
multiline
|
||||
margin="normal"
|
||||
fullWidth
|
||||
value={editItem.value}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="name"
|
||||
label={LL.NAME(0) + ' (' + LL.OPTIONAL() + ')'}
|
||||
value={editItem.name}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="cmd"
|
||||
label={LL.COMMAND(0)}
|
||||
multiline
|
||||
fullWidth
|
||||
value={editItem.cmd}
|
||||
margin="normal"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
<TextField
|
||||
name="value"
|
||||
label={LL.VALUE(0)}
|
||||
multiline
|
||||
margin="normal"
|
||||
fullWidth
|
||||
value={editItem.value}
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
<ValidatedTextField
|
||||
fieldErrors={fieldErrors}
|
||||
name="name"
|
||||
label={LL.NAME(0) + ' (' + LL.OPTIONAL() + ')'}
|
||||
value={editItem.name}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
onChange={updateFormValue}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{!creating && (
|
||||
@@ -386,6 +397,16 @@ const SchedulerDialog = ({
|
||||
>
|
||||
{creating ? LL.ADD(0) : LL.UPDATE()}
|
||||
</Button>
|
||||
{editItem.flags === 0 && editItem.cmd !== '' && (
|
||||
<Button
|
||||
startIcon={<DoneIcon />}
|
||||
variant="outlined"
|
||||
onClick={saveandactivate}
|
||||
color="warning"
|
||||
>
|
||||
{LL.EXECUTE()}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -255,6 +255,7 @@ const Sensors: FC = () => {
|
||||
|
||||
const updateTemperatureSensor = (ts: TemperatureSensor) => {
|
||||
if (me.admin) {
|
||||
ts.o_n = ts.n;
|
||||
setSelectedTemperatureSensor(ts);
|
||||
setTemperatureDialogOpen(true);
|
||||
}
|
||||
@@ -282,6 +283,7 @@ const Sensors: FC = () => {
|
||||
const updateAnalogSensor = (as: AnalogSensor) => {
|
||||
if (me.admin) {
|
||||
setCreating(false);
|
||||
as.o_n = as.n;
|
||||
setSelectedAnalogSensor(as);
|
||||
setAnalogDialogOpen(true);
|
||||
}
|
||||
@@ -302,7 +304,8 @@ const Sensors: FC = () => {
|
||||
o: 0,
|
||||
t: 0,
|
||||
f: 1,
|
||||
d: false
|
||||
d: false,
|
||||
o_n: ''
|
||||
});
|
||||
setAnalogDialogOpen(true);
|
||||
};
|
||||
@@ -465,7 +468,10 @@ const Sensors: FC = () => {
|
||||
onClose={onTemperatureDialogClose}
|
||||
onSave={onTemperatureDialogSave}
|
||||
selectedItem={selectedTemperatureSensor}
|
||||
validator={temperatureSensorItemValidation(sensorData.ts)}
|
||||
validator={temperatureSensorItemValidation(
|
||||
sensorData.ts,
|
||||
selectedTemperatureSensor
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{sensorData?.analog_enabled === true && (
|
||||
@@ -483,6 +489,7 @@ const Sensors: FC = () => {
|
||||
selectedItem={selectedAnalogSensor}
|
||||
validator={analogSensorItemValidation(
|
||||
sensorData.as,
|
||||
selectedAnalogSensor,
|
||||
creating,
|
||||
sensorData.platform
|
||||
)}
|
||||
|
||||
@@ -79,6 +79,7 @@ export interface TemperatureSensor {
|
||||
t?: number; // temp, optional
|
||||
o: number; // offset
|
||||
u: number; // uom
|
||||
o_n?: string;
|
||||
}
|
||||
|
||||
export interface AnalogSensor {
|
||||
@@ -91,6 +92,7 @@ export interface AnalogSensor {
|
||||
f: number;
|
||||
t: number;
|
||||
d: boolean; // deleted flag
|
||||
o_n?: string;
|
||||
}
|
||||
|
||||
export interface WriteTemperatureSensor {
|
||||
|
||||
@@ -241,6 +241,25 @@ export const createSettingsValidator = (settings: Settings) =>
|
||||
{ type: 'number', min: 0, max: 10, message: 'Must be between 0 and 10' }
|
||||
]
|
||||
}),
|
||||
...(settings.modbus_enabled && {
|
||||
modbus_max_clients: [
|
||||
{ required: true, message: 'Max clients is required' },
|
||||
{ type: 'number', min: 0, max: 50, message: 'Invalid number' }
|
||||
],
|
||||
modbus_port: [
|
||||
{ required: true, message: 'Port is required' },
|
||||
{ type: 'number', min: 0, max: 65535, message: 'Invalid Port' }
|
||||
],
|
||||
modbus_timeout: [
|
||||
{ required: true, message: 'Timeout is required' },
|
||||
{
|
||||
type: 'number',
|
||||
min: 100,
|
||||
max: 20000,
|
||||
message: 'Must be between 100 and 20000'
|
||||
}
|
||||
]
|
||||
}),
|
||||
...(settings.shower_timer && {
|
||||
shower_min_duration: [
|
||||
{
|
||||
@@ -388,9 +407,16 @@ export const entityItemValidation = (entity: EntityItem[], entityItem: EntityIte
|
||||
]
|
||||
});
|
||||
|
||||
export const uniqueTemperatureNameValidator = (sensors: TemperatureSensor[]) => ({
|
||||
export const uniqueTemperatureNameValidator = (
|
||||
sensors: TemperatureSensor[],
|
||||
o_name?: string
|
||||
) => ({
|
||||
validator(rule: InternalRuleItem, n: string, callback: (error?: string) => void) {
|
||||
if (n !== '' && sensors.find((ts) => ts.n.toLowerCase() === n.toLowerCase())) {
|
||||
if (
|
||||
(o_name === undefined || o_name.toLowerCase() !== n.toLowerCase()) &&
|
||||
n !== '' &&
|
||||
sensors.find((ts) => ts.n.toLowerCase() === n.toLowerCase())
|
||||
) {
|
||||
callback('Name already in use');
|
||||
} else {
|
||||
callback();
|
||||
@@ -398,7 +424,10 @@ export const uniqueTemperatureNameValidator = (sensors: TemperatureSensor[]) =>
|
||||
}
|
||||
});
|
||||
|
||||
export const temperatureSensorItemValidation = (sensors: TemperatureSensor[]) =>
|
||||
export const temperatureSensorItemValidation = (
|
||||
sensors: TemperatureSensor[],
|
||||
sensor: TemperatureSensor
|
||||
) =>
|
||||
new Schema({
|
||||
n: [
|
||||
{
|
||||
@@ -406,7 +435,7 @@ export const temperatureSensorItemValidation = (sensors: TemperatureSensor[]) =>
|
||||
pattern: /^[a-zA-Z0-9_]{0,19}$/,
|
||||
message: "Must be <20 characters: alphanumeric or '_'"
|
||||
},
|
||||
...[uniqueTemperatureNameValidator(sensors)]
|
||||
...[uniqueTemperatureNameValidator(sensors, sensor.o_n)]
|
||||
]
|
||||
});
|
||||
|
||||
@@ -424,9 +453,16 @@ export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({
|
||||
}
|
||||
});
|
||||
|
||||
export const uniqueAnalogNameValidator = (sensors: AnalogSensor[]) => ({
|
||||
export const uniqueAnalogNameValidator = (
|
||||
sensors: AnalogSensor[],
|
||||
o_name?: string
|
||||
) => ({
|
||||
validator(rule: InternalRuleItem, n: string, callback: (error?: string) => void) {
|
||||
if (n !== '' && sensors.find((as) => as.n.toLowerCase() === n.toLowerCase())) {
|
||||
if (
|
||||
(o_name === undefined || o_name.toLowerCase() !== n.toLowerCase()) &&
|
||||
n !== '' &&
|
||||
sensors.find((as) => as.n.toLowerCase() === n.toLowerCase())
|
||||
) {
|
||||
callback('Name already in use');
|
||||
} else {
|
||||
callback();
|
||||
@@ -436,6 +472,7 @@ export const uniqueAnalogNameValidator = (sensors: AnalogSensor[]) => ({
|
||||
|
||||
export const analogSensorItemValidation = (
|
||||
sensors: AnalogSensor[],
|
||||
sensor: AnalogSensor,
|
||||
creating: boolean,
|
||||
platform: string
|
||||
) =>
|
||||
@@ -446,7 +483,7 @@ export const analogSensorItemValidation = (
|
||||
pattern: /^[a-zA-Z0-9_]{0,19}$/,
|
||||
message: "Must be <20 characters: alphanumeric or '_'"
|
||||
},
|
||||
...[uniqueAnalogNameValidator(sensors)]
|
||||
...[uniqueAnalogNameValidator(sensors, sensor.o_n)]
|
||||
],
|
||||
g: [
|
||||
{ required: true, message: 'GPIO is required' },
|
||||
|
||||
@@ -67,13 +67,14 @@ const dnsServers = ({ dns_ip_1, dns_ip_2 }: NetworkStatusType) => {
|
||||
if (!dns_ip_1) {
|
||||
return 'none';
|
||||
}
|
||||
return dns_ip_1 + (!dns_ip_2 || dns_ip_2 === '0.0.0.0' ? '' : ',' + dns_ip_2);
|
||||
return dns_ip_1 + (!dns_ip_2 || dns_ip_2 === '0.0.0.0' ? '' : ', ' + dns_ip_2);
|
||||
};
|
||||
|
||||
const IPs = (status: NetworkStatusType) => {
|
||||
if (
|
||||
!status.local_ipv6 ||
|
||||
status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000'
|
||||
status.local_ipv6 === '0000:0000:0000:0000:0000:0000:0000:0000' ||
|
||||
status.local_ipv6 === '::'
|
||||
) {
|
||||
return status.local_ip;
|
||||
}
|
||||
|
||||
@@ -73,3 +73,7 @@ the LICENSE file.
|
||||
#define EMC_SIZE_POOL_ELEMENTS 128
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef TASMOTA_SDK
|
||||
#define EMC_CLIENT_SECURE
|
||||
#endif
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "IPUtils.h"
|
||||
#include "SecurityManager.h"
|
||||
|
||||
#define MAX_NETWORK_STATUS_SIZE 1024
|
||||
#define NETWORK_STATUS_SERVICE_PATH "/rest/networkStatus"
|
||||
|
||||
class NetworkStatus {
|
||||
|
||||
@@ -339,7 +339,7 @@ void AnalogSensor::loop() {
|
||||
|
||||
// update analog information name and offset
|
||||
// a type value of -1 is used to delete the sensor
|
||||
bool AnalogSensor::update(uint8_t gpio, const std::string & name, double offset, double factor, uint8_t uom, int8_t type, bool deleted) {
|
||||
bool AnalogSensor::update(uint8_t gpio, std::string & name, double offset, double factor, uint8_t uom, int8_t type, bool deleted) {
|
||||
// first see if we can find the sensor in our customization list
|
||||
bool found_sensor = false;
|
||||
EMSESP::webCustomizationService.update([&](WebCustomization & settings) {
|
||||
@@ -347,6 +347,11 @@ bool AnalogSensor::update(uint8_t gpio, const std::string & name, double offset,
|
||||
if (AnalogCustomization.type == AnalogType::COUNTER || AnalogCustomization.type >= AnalogType::DIGITAL_OUT) {
|
||||
Command::erase_command(EMSdevice::DeviceType::ANALOGSENSOR, AnalogCustomization.name.c_str());
|
||||
}
|
||||
if (name.empty()) {
|
||||
char n[20];
|
||||
snprintf(n, sizeof(n), "%s_%02d", FL_(AnalogTypeName)[type], gpio);
|
||||
name = n;
|
||||
}
|
||||
if (AnalogCustomization.gpio == gpio) {
|
||||
found_sensor = true; // found the record
|
||||
// see if it's marked for deletion
|
||||
@@ -756,16 +761,6 @@ AnalogSensor::Sensor::Sensor(const uint8_t gpio, const std::string & name, const
|
||||
value_ = 0; // init value to 0 always
|
||||
}
|
||||
|
||||
// returns name of the analog sensor or creates one if its empty
|
||||
std::string AnalogSensor::Sensor::name() const {
|
||||
if (name_.empty()) {
|
||||
char name[20];
|
||||
snprintf(name, sizeof(name), "%s_%02d", FL_(AnalogTypeName)[type_], gpio_);
|
||||
return name;
|
||||
}
|
||||
return name_;
|
||||
}
|
||||
|
||||
// set the dig_out/counter/DAC/PWM value, id is gpio-no
|
||||
bool AnalogSensor::command_setvalue(const char * value, const int8_t gpio) {
|
||||
float val;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
namespace emsesp {
|
||||
|
||||
// names, same order as AnalogType
|
||||
MAKE_ENUM_FIXED(AnalogTypeName, "disabled", "dig_in", "counter", "adc", "timer", "rate", "pwm0", "pwm1", "pwm2")
|
||||
MAKE_ENUM_FIXED(AnalogTypeName, "disabled", "dig_in", "counter", "adc", "timer", "rate", "dig_out", "pwm0", "pwm1", "pwm2")
|
||||
|
||||
class AnalogSensor {
|
||||
public:
|
||||
@@ -41,7 +41,9 @@ class AnalogSensor {
|
||||
offset_ = offset;
|
||||
}
|
||||
|
||||
std::string name() const;
|
||||
std::string name() const {
|
||||
return name_;
|
||||
}
|
||||
|
||||
void set_name(const std::string & name) {
|
||||
name_ = name;
|
||||
@@ -155,7 +157,7 @@ class AnalogSensor {
|
||||
return sensors_.size();
|
||||
}
|
||||
|
||||
bool update(uint8_t gpio, const std::string & name, double offset, double factor, uint8_t uom, int8_t type, bool deleted = false);
|
||||
bool update(uint8_t gpio, std::string & name, double offset, double factor, uint8_t uom, int8_t type, bool deleted = false);
|
||||
bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1);
|
||||
void store_counters();
|
||||
|
||||
|
||||
@@ -98,7 +98,9 @@ uint8_t Command::process(const char * path, const bool is_admin, const JsonObjec
|
||||
|
||||
// some commands may be prefixed with hc. dhw. or hc/ or dhw/ so extract these if they exist
|
||||
// parse_command_string returns the extracted command
|
||||
command_p = parse_command_string(command_p, id_n);
|
||||
if (device_type > EMSdevice::DeviceType::BOILER) {
|
||||
command_p = parse_command_string(command_p, id_n);
|
||||
}
|
||||
if (command_p == nullptr) {
|
||||
// handle dead endpoints like api/system or api/boiler
|
||||
// default to 'info' for SYSTEM, the other devices to 'values' for shortname version
|
||||
@@ -148,7 +150,10 @@ uint8_t Command::process(const char * path, const bool is_admin, const JsonObjec
|
||||
strlcpy(device_s, d, device_end - d + 1);
|
||||
data_p = device_end + 1;
|
||||
int8_t id_d = -1;
|
||||
data_p = parse_command_string(data_p, id_d);
|
||||
uint8_t device_type = EMSdevice::device_name_2_device_type(device_p);
|
||||
if (device_type > EMSdevice::DeviceType::BOILER) {
|
||||
data_p = parse_command_string(data_p, id_d);
|
||||
}
|
||||
if (data_p == nullptr) {
|
||||
return CommandRet::INVALID;
|
||||
}
|
||||
@@ -159,7 +164,6 @@ uint8_t Command::process(const char * path, const bool is_admin, const JsonObjec
|
||||
strcat(data_s, "/value");
|
||||
}
|
||||
|
||||
uint8_t device_type = EMSdevice::device_name_2_device_type(device_p);
|
||||
if (Command::call(device_type, data_s, "", true, id_d, output) != CommandRet::OK) {
|
||||
return CommandRet::INVALID;
|
||||
}
|
||||
|
||||
@@ -428,10 +428,9 @@ static void setup_commands(std::shared_ptr<Commands> & commands) {
|
||||
to_app(shell).send_read_request(type_id, device_id, offset, length, true);
|
||||
} else if (arguments.size() == 3) {
|
||||
uint16_t offset = Helpers::hextoint(arguments.back().c_str());
|
||||
to_app(shell).send_read_request(type_id, device_id, offset, EMS_MAX_TELEGRAM_LENGTH, true);
|
||||
to_app(shell).send_read_request(type_id, device_id, offset, 0, true);
|
||||
} else {
|
||||
// send with length to send immediately and trigger publish read_id
|
||||
to_app(shell).send_read_request(type_id, device_id, 0, EMS_MAX_TELEGRAM_LENGTH, true);
|
||||
to_app(shell).send_read_request(type_id, device_id, 0, 0, true);
|
||||
}
|
||||
to_app(shell).set_read_id(type_id);
|
||||
});
|
||||
@@ -533,10 +532,14 @@ static void setup_commands(std::shared_ptr<Commands> & commands) {
|
||||
|
||||
JsonDocument doc;
|
||||
int8_t id = -1;
|
||||
const char * cmd = Command::parse_command_string(arguments[1].c_str(), id);
|
||||
const char * cmd = Helpers::toLower(arguments[1]).c_str();
|
||||
uint8_t return_code = CommandRet::OK;
|
||||
JsonObject json = doc.to<JsonObject>();
|
||||
bool has_data = false;
|
||||
|
||||
if (device_type >= EMSdevice::DeviceType::BOILER) {
|
||||
cmd = Command::parse_command_string(cmd, id); // extract hc or dhw
|
||||
}
|
||||
if (cmd == nullptr) {
|
||||
cmd = device_type == EMSdevice::DeviceType::SYSTEM ? F_(info) : F_(values);
|
||||
}
|
||||
@@ -551,6 +554,7 @@ static void setup_commands(std::shared_ptr<Commands> & commands) {
|
||||
} else if (arguments[2] == "?") {
|
||||
return_code = Command::call(device_type, cmd, nullptr, true, id, json);
|
||||
} else {
|
||||
has_data = true;
|
||||
// has a value but no id so use -1
|
||||
return_code = Command::call(device_type, cmd, arguments.back().c_str(), true, id, json);
|
||||
}
|
||||
@@ -559,6 +563,7 @@ static void setup_commands(std::shared_ptr<Commands> & commands) {
|
||||
if (arguments[2] == "?") {
|
||||
return_code = Command::call(device_type, cmd, nullptr, true, atoi(arguments[3].c_str()), json);
|
||||
} else {
|
||||
has_data = true;
|
||||
return_code = Command::call(device_type, cmd, arguments[2].c_str(), true, atoi(arguments[3].c_str()), json);
|
||||
}
|
||||
}
|
||||
@@ -573,10 +578,12 @@ static void setup_commands(std::shared_ptr<Commands> & commands) {
|
||||
serializeJsonPretty(doc, shell);
|
||||
shell.println();
|
||||
return;
|
||||
} else {
|
||||
} else if (!has_data) {
|
||||
// show message if no data returned (e.g. for analogsensor, temperaturesensor, custom)
|
||||
shell.println("Command executed");
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else if (return_code == CommandRet::NOT_FOUND) {
|
||||
shell.println("Unknown command");
|
||||
|
||||
@@ -2030,7 +2030,7 @@ int EMSdevice::get_modbus_value(uint8_t tag, const std::string & shortname, std:
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool EMSdevice::modbus_value_to_json(uint8_t tag, const std::string & shortname, const std::vector<uint8_t> & modbus_data, JsonObject & jsonValue) {
|
||||
bool EMSdevice::modbus_value_to_json(uint8_t tag, const std::string & shortname, const std::vector<uint8_t> & modbus_data, JsonObject jsonValue) {
|
||||
//Serial.printf("modbus_value_to_json(%d,%s,[%d bytes])\n", tag, shortname.c_str(), modbus_data.size());
|
||||
|
||||
// find device value by shortname
|
||||
|
||||
@@ -203,7 +203,7 @@ class EMSdevice {
|
||||
}
|
||||
|
||||
int get_modbus_value(uint8_t tag, const std::string & shortname, std::vector<uint16_t> & result);
|
||||
bool modbus_value_to_json(uint8_t tag, const std::string & shortname, const std::vector<uint8_t> & modbus_data, JsonObject & jsonValue);
|
||||
bool modbus_value_to_json(uint8_t tag, const std::string & shortname, const std::vector<uint8_t> & modbus_data, JsonObject jsonValue);
|
||||
|
||||
const char * brand_to_char();
|
||||
const std::string to_string();
|
||||
|
||||
@@ -710,17 +710,33 @@ void EMSESP::publish_sensor_values(const bool time, const bool force) {
|
||||
|
||||
// MQTT publish a telegram as raw data to the topic 'response'
|
||||
void EMSESP::publish_response(std::shared_ptr<const Telegram> telegram) {
|
||||
static char * buffer = nullptr;
|
||||
static uint8_t offset;
|
||||
if (buffer == nullptr) {
|
||||
offset = telegram->offset; // store offset from first part
|
||||
buffer = new char[768]; // max 256 hex-codes, 255 spaces, 1 termination
|
||||
buffer[0] = '\0';
|
||||
static char * buffer = nullptr;
|
||||
static uint8_t offset = 0;
|
||||
static uint16_t type = 0;
|
||||
// restart on mismatch while collecting telegram
|
||||
if (buffer && (telegram->offset < offset || telegram->type_id != type)) {
|
||||
delete[] buffer;
|
||||
buffer = nullptr;
|
||||
}
|
||||
if (buffer == nullptr) {
|
||||
offset = telegram->offset; // store offset from first part
|
||||
type = telegram->type_id;
|
||||
buffer = new char[768]; // max 256 hex-codes, 255 spaces, 1 termination
|
||||
for (uint16_t i = 0; i < 256; i++) {
|
||||
buffer[i * 3] = '0';
|
||||
buffer[i * 3 + 1] = '0';
|
||||
buffer[i * 3 + 2] = ' ';
|
||||
}
|
||||
buffer[267] = '\0';
|
||||
}
|
||||
if (telegram->message_length) {
|
||||
strlcpy(&buffer[(telegram->offset - offset) * 3], Helpers::data_to_hex(telegram->message_data, telegram->message_length).c_str(), 768);
|
||||
} else {
|
||||
strlcpy(&buffer[(telegram->offset - offset) * 3], "", 768);
|
||||
}
|
||||
strlcat(buffer, Helpers::data_to_hex(telegram->message_data, telegram->message_length).c_str(), 768);
|
||||
if (response_id_ != 0) {
|
||||
strlcat(buffer, " ", 768);
|
||||
return; // do not delete buffer
|
||||
buffer[strlen(buffer)] = ' '; // overwrite termination \0
|
||||
return; // do not delete buffer
|
||||
}
|
||||
JsonDocument doc;
|
||||
char s[10];
|
||||
@@ -730,12 +746,9 @@ void EMSESP::publish_response(std::shared_ptr<const Telegram> telegram) {
|
||||
doc["offset"] = Helpers::hextoa(s, offset);
|
||||
doc["data"] = buffer;
|
||||
|
||||
if (telegram->message_length <= 4 && strlen(buffer) <= 11) {
|
||||
uint32_t value = 0;
|
||||
for (uint8_t i = 0; i < telegram->message_length; i++) {
|
||||
value = (value << 8) + telegram->message_data[i];
|
||||
}
|
||||
doc["value"] = value;
|
||||
if (strlen(buffer) && strlen(buffer) <= 11) {
|
||||
uint32_t value = Helpers::hextoint(buffer);
|
||||
doc["value"] = value;
|
||||
}
|
||||
Mqtt::queue_publish("response", doc.as<JsonObject>());
|
||||
delete[] buffer;
|
||||
@@ -1179,31 +1192,16 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const
|
||||
}
|
||||
|
||||
// first check to see if we already have it, if so update the record
|
||||
auto it = emsdevices.begin();
|
||||
for (const auto & emsdevice : emsdevices) {
|
||||
if (emsdevice && emsdevice->is_device_id(device_id)) {
|
||||
if (product_id == 0 || emsdevice->product_id() != 0) { // update only with valid product_id
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_DEBUG("Updating details for already active deviceID 0x%02X", device_id);
|
||||
emsdevice->product_id(product_id);
|
||||
emsdevice->version(version);
|
||||
|
||||
// only set brand if it doesn't already exist
|
||||
if (emsdevice->brand() == EMSdevice::Brand::NO_BRAND) {
|
||||
emsdevice->brand(brand);
|
||||
}
|
||||
|
||||
// find the name and flags in our device library database
|
||||
for (const auto & device : device_library_) {
|
||||
if (device.product_id == product_id && device.device_type == emsdevice->device_type()) {
|
||||
emsdevice->default_name(device.default_name); // custom name is set later
|
||||
emsdevice->add_flags(device.flags);
|
||||
}
|
||||
}
|
||||
|
||||
return true; // finish up
|
||||
emsdevices.erase(it); // erase the old device without product_id and re detect
|
||||
break;
|
||||
}
|
||||
it++;
|
||||
}
|
||||
|
||||
// look up the rest of the details using the product_id and create the new device object
|
||||
@@ -1488,6 +1486,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
|
||||
read_next_ = true;
|
||||
txservice_.send();
|
||||
} else {
|
||||
read_next_ = false;
|
||||
txservice_.send_poll(); // close the bus
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +178,10 @@ class EMSESP {
|
||||
response_id_ = id;
|
||||
}
|
||||
|
||||
static uint16_t response_id() {
|
||||
return response_id_;
|
||||
}
|
||||
|
||||
static bool wait_validate() {
|
||||
return (wait_validate_ != 0);
|
||||
}
|
||||
|
||||
@@ -411,6 +411,7 @@ uint32_t Helpers::hextoint(const char * hex) {
|
||||
// get current character then increment
|
||||
char byte = *hex++;
|
||||
// transform hex character to the 4bit equivalent number, using the ascii table indexes
|
||||
if (byte == ' ') byte = *hex++; // skip spaces
|
||||
if (byte >= '0' && byte <= '9')
|
||||
byte = byte - '0';
|
||||
else if (byte >= 'a' && byte <= 'f')
|
||||
|
||||
@@ -208,6 +208,10 @@ class Mqtt {
|
||||
return lastresponse_;
|
||||
}
|
||||
|
||||
static void clear_response() {
|
||||
lastresponse_.clear();
|
||||
}
|
||||
|
||||
void set_qos(uint8_t mqtt_qos) const {
|
||||
mqtt_qos_ = mqtt_qos;
|
||||
}
|
||||
|
||||
@@ -1305,7 +1305,7 @@ bool System::get_value_info(JsonObject root, const char * command) {
|
||||
(void)command_info("", 0, root);
|
||||
|
||||
// check for hardcoded "info"
|
||||
if (!strcmp(cmd, F_(info))) {
|
||||
if (!strcmp(cmd, F_(info)) || !strcmp(cmd, F_(value))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -336,7 +336,7 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) {
|
||||
telegram_raw[3] = telegram->offset;
|
||||
|
||||
// EMS+ has different format for read and write
|
||||
if (telegram->operation != Telegram::Operation::TX_READ) {
|
||||
if (telegram->operation != Telegram::Operation::TX_READ && telegram->operation != Telegram::Operation::TX_RAW) {
|
||||
// WRITE/NONE
|
||||
telegram_raw[4] = (telegram->type_id >> 8) - 1; // type, 1st byte, high-byte, subtract 0x100
|
||||
telegram_raw[5] = telegram->type_id & 0xFF; // type, 2nd byte, low-byte
|
||||
@@ -404,6 +404,17 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (telegram->operation == Telegram::Operation::TX_RAW) {
|
||||
tx_state(Telegram::Operation::TX_READ);
|
||||
if (EMSESP::response_id() == 0) {
|
||||
Mqtt::clear_response();
|
||||
EMSESP::set_response_id(telegram->type_id);
|
||||
if (telegram->message_data[0] >= (telegram->type_id > 0xFF ? 25 : 27)) {
|
||||
EMSESP::set_read_id(telegram->type_id);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
tx_state(telegram->operation); // tx now in a wait state
|
||||
}
|
||||
|
||||
@@ -512,13 +523,8 @@ void TxService::add(uint8_t operation, const uint8_t * data, const uint8_t lengt
|
||||
if (src != ems_bus_id() || dest == 0) {
|
||||
operation = Telegram::Operation::NONE; // do not check reply/ack for other ids and broadcasts
|
||||
} else if (dest & 0x80) {
|
||||
operation = Telegram::Operation::TX_READ;
|
||||
EMSESP::set_response_id(type_id);
|
||||
// trigger read of all parts of telegram if requested length is more than 32
|
||||
// compatibility to earlier versions
|
||||
if (message_data[0] >= 32) {
|
||||
EMSESP::set_read_id(type_id);
|
||||
}
|
||||
// keep operation RAW to set the response when sending
|
||||
// Mqtt::clear_response(); // set here when receiving command or when sending?
|
||||
} else {
|
||||
operation = Telegram::Operation::TX_WRITE;
|
||||
validate_id = type_id;
|
||||
@@ -540,7 +546,7 @@ void TxService::add(uint8_t operation, const uint8_t * data, const uint8_t lengt
|
||||
|
||||
LOG_DEBUG("New Tx [#%d] telegram, length %d", tx_telegram_id_, message_length);
|
||||
|
||||
if (front) {
|
||||
if (front && (operation != Telegram::Operation::TX_RAW || EMSESP::response_id() == 0)) {
|
||||
// tx_telegrams_.push_front(qtxt); // add to front of queue
|
||||
tx_telegrams_.emplace_front(tx_telegram_id_++, std::move(telegram), false, validate_id); // add to front of queue
|
||||
} else {
|
||||
@@ -556,7 +562,7 @@ void TxService::add(uint8_t operation, const uint8_t * data, const uint8_t lengt
|
||||
void TxService::read_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset, const uint8_t length, const bool front) {
|
||||
LOG_DEBUG("Tx read request to deviceID 0x%02X for typeID 0x%02X", dest, type_id);
|
||||
|
||||
uint8_t message_data = (type_id > 0xFF) ? (EMS_MAX_TELEGRAM_MESSAGE_LENGTH - 2) : EMS_MAX_TELEGRAM_MESSAGE_LENGTH;
|
||||
uint8_t message_data = 0xFF;
|
||||
if (length > 0 && length < message_data) {
|
||||
message_data = length;
|
||||
}
|
||||
@@ -570,11 +576,11 @@ bool TxService::send_raw(const char * telegram_data) {
|
||||
}
|
||||
|
||||
// since the telegram data is a const, make a copy. add 1 to grab the \0 EOS
|
||||
char telegram[EMS_MAX_TELEGRAM_LENGTH * 3];
|
||||
char telegram[strlen(telegram_data) + 1];
|
||||
strlcpy(telegram, telegram_data, sizeof(telegram));
|
||||
|
||||
uint8_t count = 0;
|
||||
uint8_t data[EMS_MAX_TELEGRAM_LENGTH];
|
||||
uint8_t data[2 + strlen(telegram) / 3];
|
||||
|
||||
// get values
|
||||
char * p = strtok(telegram, " ,"); // delimiter
|
||||
@@ -602,6 +608,10 @@ void TxService::retry_tx(const uint8_t operation, const uint8_t * data, const ui
|
||||
if (operation == Telegram::Operation::TX_READ) {
|
||||
if (telegram_last_->offset > 0) { // ignore errors for higher offsets
|
||||
LOG_DEBUG("Last Tx Read operation failed after %d retries. Ignoring request: %s", MAXIMUM_TX_RETRIES, telegram_last_->to_string().c_str());
|
||||
if (EMSESP::response_id()) {
|
||||
EMSESP::set_response_id(0);
|
||||
EMSESP::set_read_id(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
increment_telegram_read_fail_count(); // another Tx fail
|
||||
@@ -628,7 +638,10 @@ void TxService::retry_tx(const uint8_t operation, const uint8_t * data, const ui
|
||||
|
||||
// add to the top of the queue
|
||||
if (tx_telegrams_.size() >= MAX_TX_TELEGRAMS) {
|
||||
tx_telegrams_.pop_back();
|
||||
LOG_WARNING("Tx queue overflow, skip retry");
|
||||
reset_retry_count(); // give up
|
||||
EMSESP::wait_validate(0); // do not wait for validation
|
||||
return;
|
||||
}
|
||||
|
||||
tx_telegrams_.emplace_front(tx_telegram_id_++, std::move(telegram_last_), true, get_post_send_query());
|
||||
@@ -636,15 +649,25 @@ void TxService::retry_tx(const uint8_t operation, const uint8_t * data, const ui
|
||||
|
||||
// send a request to read the next block of data from longer telegrams
|
||||
uint16_t TxService::read_next_tx(const uint8_t offset, const uint8_t length) {
|
||||
uint8_t old_length = telegram_last_->type_id > 0xFF ? length - 7 : length - 5;
|
||||
uint8_t next_length = telegram_last_->type_id > 0xFF ? EMS_MAX_TELEGRAM_MESSAGE_LENGTH - 2 : EMS_MAX_TELEGRAM_MESSAGE_LENGTH;
|
||||
uint8_t next_offset = offset + old_length;
|
||||
uint8_t message_data = (UINT8_MAX - next_offset) >= next_length ? next_length : UINT8_MAX - next_offset;
|
||||
// check telegram, offset and overflow
|
||||
// some telegrams only reply with one byte less, but have higher offsets (0x10)
|
||||
// some reply with higher offset than requestes and have not_set values intermediate (0xEA)
|
||||
if (old_length >= (next_length - 1) || telegram_last_->offset < offset) {
|
||||
add(Telegram::Operation::TX_READ, telegram_last_->dest, telegram_last_->type_id, next_offset, &message_data, 1, 0, true);
|
||||
uint8_t old_length = telegram_last_->type_id > 0xFF ? length - 7 : length - 5;
|
||||
uint8_t max_length = telegram_last_->type_id > 0xFF ? EMS_MAX_TELEGRAM_MESSAGE_LENGTH - 2 : EMS_MAX_TELEGRAM_MESSAGE_LENGTH;
|
||||
uint8_t next_length = telegram_last_->message_data[0] > old_length ? telegram_last_->message_data[0] - old_length - offset + telegram_last_->offset : 0;
|
||||
uint8_t next_offset = offset + old_length;
|
||||
|
||||
// check telegram, length, offset and overflow
|
||||
// some telegrams only reply with one byte less, but have higher offsets (boiler 0x10)
|
||||
// some reply with higher offset than requestes and have not_set values intermediate (boiler 0xEA)
|
||||
|
||||
// We have th last byte received
|
||||
if (offset + old_length >= telegram_last_->offset + telegram_last_->message_data[0]) {
|
||||
return 0;
|
||||
}
|
||||
// we request all and get a short telegram with requested offset
|
||||
if ((next_length + next_offset) == 0xFF && old_length < max_length - 1 && offset <= telegram_last_->offset) {
|
||||
return 0;
|
||||
}
|
||||
if (offset >= telegram_last_->offset && old_length > 0 && next_length > 0) {
|
||||
add(Telegram::Operation::TX_READ, telegram_last_->dest, telegram_last_->type_id, next_offset, &next_length, 1, 0, true);
|
||||
return telegram_last_->type_id;
|
||||
}
|
||||
return 0;
|
||||
@@ -666,11 +689,10 @@ uint16_t TxService::post_send_query() {
|
||||
|
||||
if (post_typeid) {
|
||||
uint8_t dest = (this->telegram_last_->dest & 0x7F);
|
||||
// when set a value with large offset before and validate on same type, we have to add offset 0, 26, 52, ...
|
||||
uint8_t offset = (this->telegram_last_->type_id == post_typeid) ? ((this->telegram_last_->offset / 26) * 26) : 0;
|
||||
uint8_t message_data =
|
||||
(this->telegram_last_->type_id > 0xFF) ? (EMS_MAX_TELEGRAM_MESSAGE_LENGTH - 2) : EMS_MAX_TELEGRAM_MESSAGE_LENGTH; // request all data, 32 bytes
|
||||
this->add(Telegram::Operation::TX_READ, dest, post_typeid, offset, &message_data, 1, 0, true); // add to top/front of queue
|
||||
// when set a value with large offset before and validate on same type and offset, or complete telegram
|
||||
uint8_t length = (this->telegram_last_->type_id == post_typeid) ? this->telegram_last_->message_length : 0xFF;
|
||||
uint8_t offset = (this->telegram_last_->type_id == post_typeid) ? this->telegram_last_->offset : 0;
|
||||
this->add(Telegram::Operation::TX_READ, dest, post_typeid, offset, &length, 1, 0, true); // add to top/front of queue
|
||||
// read_request(telegram_last_post_send_query_, dest, 0); // no offset
|
||||
LOG_DEBUG("Sending post validate read, type ID 0x%02X to dest 0x%02X", post_typeid, dest);
|
||||
set_post_send_query(0); // reset
|
||||
|
||||
@@ -221,10 +221,6 @@ void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVar
|
||||
// using the unique ID from the web find the real device type
|
||||
for (const auto & emsdevice : EMSESP::emsdevices) {
|
||||
if (emsdevice->unique_id() == unique_id) {
|
||||
// parse the command as it could have a hc or dhw prefixed, e.g. hc2/seltemp
|
||||
int8_t id = -1; // default
|
||||
cmd = Command::parse_command_string(cmd, id); // extract hc or dhw
|
||||
|
||||
// create JSON for output
|
||||
auto * response = new AsyncJsonResponse(false);
|
||||
JsonObject output = response->getRoot();
|
||||
@@ -233,6 +229,11 @@ void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVar
|
||||
// authenticated is always true
|
||||
uint8_t return_code = CommandRet::NOT_FOUND;
|
||||
uint8_t device_type = emsdevice->device_type();
|
||||
// parse the command as it could have a hc or dhw prefixed, e.g. hc2/seltemp
|
||||
int8_t id = -1; // default
|
||||
if (device_type >= EMSdevice::DeviceType::BOILER) {
|
||||
cmd = Command::parse_command_string(cmd, id); // extract hc or dhw
|
||||
}
|
||||
if (data.is<const char *>()) {
|
||||
return_code = Command::call(device_type, cmd, data.as<const char *>(), true, id, output);
|
||||
} else if (data.is<int>()) {
|
||||
@@ -263,13 +264,15 @@ void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVar
|
||||
|
||||
// special check for custom entities (which have a unique id of 99)
|
||||
if (unique_id == 99) {
|
||||
// parse the command as it could have a hc or dhw prefixed, e.g. hc2/seltemp
|
||||
int8_t id = -1;
|
||||
cmd = Command::parse_command_string(cmd, id);
|
||||
auto * response = new AsyncJsonResponse(false);
|
||||
JsonObject output = response->getRoot();
|
||||
uint8_t return_code = CommandRet::NOT_FOUND;
|
||||
uint8_t device_type = EMSdevice::DeviceType::CUSTOM;
|
||||
// parse the command as it could have a hc or dhw prefixed, e.g. hc2/seltemp
|
||||
int8_t id = -1;
|
||||
if (device_type >= EMSdevice::DeviceType::BOILER) {
|
||||
cmd = Command::parse_command_string(cmd, id); // extract hc or dhw
|
||||
}
|
||||
if (data.is<const char *>()) {
|
||||
return_code = Command::call(device_type, cmd, data.as<const char *>(), true, id, output);
|
||||
} else if (data.is<int>()) {
|
||||
|
||||
@@ -39,7 +39,7 @@ void WebSchedulerService::begin() {
|
||||
snprintf(topic, sizeof(topic), "%s/#", F_(scheduler));
|
||||
Mqtt::subscribe(EMSdevice::DeviceType::SCHEDULER, topic, nullptr); // use empty function callback
|
||||
#ifndef EMSESP_STANDALONE
|
||||
xTaskCreate((TaskFunction_t)scheduler_task, "scheduler_task", 4096, NULL, 3, NULL);
|
||||
xTaskCreate((TaskFunction_t)scheduler_task, "scheduler_task", 4096, NULL, 1, NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ void WebScheduler::read(WebScheduler & webScheduler, JsonObject root) {
|
||||
for (const ScheduleItem & scheduleItem : webScheduler.scheduleItems) {
|
||||
JsonObject si = schedule.add<JsonObject>();
|
||||
si["id"] = counter++; // id is only used to render the table and must be unique
|
||||
si["active"] = scheduleItem.active;
|
||||
si["active"] = scheduleItem.flags ? scheduleItem.active : false;
|
||||
si["flags"] = scheduleItem.flags;
|
||||
si["time"] = scheduleItem.time;
|
||||
si["cmd"] = scheduleItem.cmd;
|
||||
@@ -192,6 +192,15 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) {
|
||||
char result[12];
|
||||
output["value"] = Helpers::render_boolean(result, scheduleItem.active);
|
||||
}
|
||||
if (scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_CONDITION) {
|
||||
output["condition"] = scheduleItem.time;
|
||||
} else if (scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_ONCHANGE) {
|
||||
output["onchange"] = scheduleItem.time;
|
||||
} else if (scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER) {
|
||||
output["timer"] = scheduleItem.time;
|
||||
} else if (scheduleItem.flags != 0){
|
||||
output["time"] = scheduleItem.time;
|
||||
}
|
||||
output["command"] = scheduleItem.cmd;
|
||||
output["cmd_data"] = scheduleItem.value;
|
||||
output["readable"] = true;
|
||||
@@ -426,8 +435,8 @@ bool WebSchedulerService::command(const char * name, const std::string & command
|
||||
return false;
|
||||
}
|
||||
|
||||
#include "shuntingYard.hpp"
|
||||
|
||||
// called from emsesp.cpp on every entity-change
|
||||
// queue schedules to be handled executed in scheduler-loop
|
||||
bool WebSchedulerService::onChange(const char * cmd) {
|
||||
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_ONCHANGE
|
||||
@@ -439,6 +448,9 @@ bool WebSchedulerService::onChange(const char * cmd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#include "shuntingYard.hpp"
|
||||
|
||||
// handle condition schedules, parse string stored in schedule.time field
|
||||
void WebSchedulerService::condition() {
|
||||
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_CONDITION) {
|
||||
@@ -476,6 +488,13 @@ void WebSchedulerService::loop() {
|
||||
cmd_changed_.pop_front();
|
||||
}
|
||||
|
||||
for (ScheduleItem & scheduleItem : *scheduleItems_) {
|
||||
if (scheduleItem.active && scheduleItem.flags == 0) {
|
||||
command(scheduleItem.name.c_str(), scheduleItem.cmd, compute(scheduleItem.value));
|
||||
scheduleItem.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
// check conditions every 10 seconds
|
||||
uint32_t uptime_sec = uuid::get_uptime_sec() / 10;
|
||||
if (last_uptime_sec != uptime_sec) {
|
||||
@@ -529,9 +548,10 @@ void WebSchedulerService::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
// process schedules async
|
||||
void WebSchedulerService::scheduler_task(void * pvParameters) {
|
||||
while (1) {
|
||||
delay(100); // no need to hurry
|
||||
delay(10); // no need to hurry
|
||||
if (!EMSESP::system_.upload_status()) {
|
||||
EMSESP::webSchedulerService.loop();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user