Merge pull request #1899 from MichaelDvP/dev

scheduler single and fixes
This commit is contained in:
Proddy
2024-07-26 21:47:39 +02:00
committed by GitHub
22 changed files with 276 additions and 139 deletions

View File

@@ -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)}

View File

@@ -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>
);

View File

@@ -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
)}

View File

@@ -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 {

View File

@@ -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' },

View File

@@ -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;
}

View File

@@ -73,3 +73,7 @@ the LICENSE file.
#define EMC_SIZE_POOL_ELEMENTS 128
#endif
#endif
#ifndef TASMOTA_SDK
#define EMC_CLIENT_SECURE
#endif

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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");

View File

@@ -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

View File

@@ -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();

View File

@@ -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
}
}

View File

@@ -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);
}

View File

@@ -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')

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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>()) {

View File

@@ -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();
}