diff --git a/interface/src/app/main/Scheduler.tsx b/interface/src/app/main/Scheduler.tsx index 827ae75ee..e91364988 100644 --- a/interface/src/app/main/Scheduler.tsx +++ b/interface/src/app/main/Scheduler.tsx @@ -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 = () => { - {si.flags < ScheduleFlag.SCHEDULE_TIMER ? ( + {si.flags === 0 ? ( + <> + ) : si.flags < ScheduleFlag.SCHEDULE_TIMER ? ( <> {dayBox(si, ScheduleFlag.SCHEDULE_MON)} {dayBox(si, ScheduleFlag.SCHEDULE_TUE)} diff --git a/interface/src/app/main/SchedulerDialog.tsx b/interface/src/app/main/SchedulerDialog.tsx index 2fdb95e22..529c00f7a 100644 --- a/interface/src/app/main/SchedulerDialog.tsx +++ b/interface/src/app/main/SchedulerDialog.tsx @@ -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 = ({ )} - - - )} + + + {!creating && ( @@ -386,6 +397,16 @@ const SchedulerDialog = ({ > {creating ? LL.ADD(0) : LL.UPDATE()} + {editItem.flags === 0 && editItem.cmd !== '' && ( + + )} ); diff --git a/interface/src/app/main/Sensors.tsx b/interface/src/app/main/Sensors.tsx index 7f4a91116..039ba37bb 100644 --- a/interface/src/app/main/Sensors.tsx +++ b/interface/src/app/main/Sensors.tsx @@ -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 )} diff --git a/interface/src/app/main/types.ts b/interface/src/app/main/types.ts index 05907fcfb..f7a6edbf5 100644 --- a/interface/src/app/main/types.ts +++ b/interface/src/app/main/types.ts @@ -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 { diff --git a/interface/src/app/main/validators.ts b/interface/src/app/main/validators.ts index b5bb94c9b..92bb00d15 100644 --- a/interface/src/app/main/validators.ts +++ b/interface/src/app/main/validators.ts @@ -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' }, diff --git a/interface/src/app/status/NetworkStatus.tsx b/interface/src/app/status/NetworkStatus.tsx index 683ed0727..3ef8e6a63 100644 --- a/interface/src/app/status/NetworkStatus.tsx +++ b/interface/src/app/status/NetworkStatus.tsx @@ -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; } diff --git a/lib/espMqttClient/src/Config.h b/lib/espMqttClient/src/Config.h index 935f7e1f5..f6c02eada 100644 --- a/lib/espMqttClient/src/Config.h +++ b/lib/espMqttClient/src/Config.h @@ -73,3 +73,7 @@ the LICENSE file. #define EMC_SIZE_POOL_ELEMENTS 128 #endif #endif + +#ifndef TASMOTA_SDK +#define EMC_CLIENT_SECURE +#endif diff --git a/lib/framework/NetworkStatus.h b/lib/framework/NetworkStatus.h index 6c5524169..978b59e08 100644 --- a/lib/framework/NetworkStatus.h +++ b/lib/framework/NetworkStatus.h @@ -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 { diff --git a/src/analogsensor.cpp b/src/analogsensor.cpp index f36231f73..c15d66e29 100644 --- a/src/analogsensor.cpp +++ b/src/analogsensor.cpp @@ -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; diff --git a/src/analogsensor.h b/src/analogsensor.h index 5636dc779..31c65b6c7 100644 --- a/src/analogsensor.h +++ b/src/analogsensor.h @@ -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(); diff --git a/src/command.cpp b/src/command.cpp index 7cc3e0574..239a6a97c 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -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; } diff --git a/src/console.cpp b/src/console.cpp index 25dea0dd0..33113a2a1 100644 --- a/src/console.cpp +++ b/src/console.cpp @@ -428,10 +428,9 @@ static void setup_commands(std::shared_ptr & 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) { 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(); + 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) { } 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) { 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) { 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"); diff --git a/src/emsdevice.cpp b/src/emsdevice.cpp index ac8902250..1f96767ed 100644 --- a/src/emsdevice.cpp +++ b/src/emsdevice.cpp @@ -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 & modbus_data, JsonObject & jsonValue) { +bool EMSdevice::modbus_value_to_json(uint8_t tag, const std::string & shortname, const std::vector & 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 diff --git a/src/emsdevice.h b/src/emsdevice.h index 64d819ccb..cb07683fd 100644 --- a/src/emsdevice.h +++ b/src/emsdevice.h @@ -203,7 +203,7 @@ class EMSdevice { } int get_modbus_value(uint8_t tag, const std::string & shortname, std::vector & result); - bool modbus_value_to_json(uint8_t tag, const std::string & shortname, const std::vector & modbus_data, JsonObject & jsonValue); + bool modbus_value_to_json(uint8_t tag, const std::string & shortname, const std::vector & modbus_data, JsonObject jsonValue); const char * brand_to_char(); const std::string to_string(); diff --git a/src/emsesp.cpp b/src/emsesp.cpp index c49316807..17c9d3e2c 100644 --- a/src/emsesp.cpp +++ b/src/emsesp.cpp @@ -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 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 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()); 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 } } diff --git a/src/emsesp.h b/src/emsesp.h index 77c0246ce..ac6592765 100644 --- a/src/emsesp.h +++ b/src/emsesp.h @@ -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); } diff --git a/src/helpers.cpp b/src/helpers.cpp index a86dd54f9..991a6e2a1 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -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') diff --git a/src/mqtt.h b/src/mqtt.h index 054a39aef..c4acca9de 100644 --- a/src/mqtt.h +++ b/src/mqtt.h @@ -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; } diff --git a/src/system.cpp b/src/system.cpp index 60e0b774e..01d60ba81 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -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; } diff --git a/src/telegram.cpp b/src/telegram.cpp index 07c5f406c..c3deedfec 100644 --- a/src/telegram.cpp +++ b/src/telegram.cpp @@ -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 diff --git a/src/web/WebDataService.cpp b/src/web/WebDataService.cpp index a423e5d0b..af89ab1ce 100644 --- a/src/web/WebDataService.cpp +++ b/src/web/WebDataService.cpp @@ -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()) { return_code = Command::call(device_type, cmd, data.as(), true, id, output); } else if (data.is()) { @@ -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()) { return_code = Command::call(device_type, cmd, data.as(), true, id, output); } else if (data.is()) { diff --git a/src/web/WebSchedulerService.cpp b/src/web/WebSchedulerService.cpp index ad3fc6cb7..37657776e 100644 --- a/src/web/WebSchedulerService.cpp +++ b/src/web/WebSchedulerService.cpp @@ -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(); 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(); }