Merge branch 'dev' into dev2

This commit is contained in:
MichaelDvP
2024-01-30 07:31:00 +01:00
46 changed files with 1565 additions and 430 deletions

View File

@@ -33,29 +33,13 @@ void AnalogSensor::start() {
LOG_INFO("Starting Analog sensor service");
// Add API call for /info
Command::add(
EMSdevice::DeviceType::ANALOGSENSOR,
F_(info),
[&](const char * value, const int8_t id, JsonObject output) { return command_info(value, id, output); },
FL_(info_cmd));
Command::add(
EMSdevice::DeviceType::ANALOGSENSOR,
F_(values),
[&](const char * value, const int8_t id, JsonObject output) { return command_info(value, 0, output); },
nullptr,
CommandFlag::HIDDEN); // this command is hidden
// Add API calls
Command::add(
EMSdevice::DeviceType::ANALOGSENSOR,
F_(setvalue),
[&](const char * value, const int8_t id) { return command_setvalue(value, id); },
FL_(setiovalue_cmd),
CommandFlag::ADMIN_ONLY);
Command::add(
EMSdevice::DeviceType::ANALOGSENSOR,
F_(commands),
[&](const char * value, const int8_t id, JsonObject output) { return command_commands(value, id, output); },
FL_(commands_cmd));
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf(topic, sizeof(topic), "%s/#", F_(analogsensor));
@@ -333,10 +317,10 @@ void AnalogSensor::loop() {
}
// update analog information name and offset
// a type of -1 is used to delete the sensor
// 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) {
boolean found_sensor = false; // see if we can find the sensor in our customization list
// first see if we can find the sensor in our customization list
bool found_sensor = false;
EMSESP::webCustomizationService.update(
[&](WebCustomization & settings) {
for (auto & AnalogCustomization : settings.analogCustomizations) {
@@ -374,7 +358,7 @@ bool AnalogSensor::update(uint8_t gpio, const std::string & name, double offset,
remove_ha_topic(type, gpio); // the GPIO
}
// we didn't find it, it's new, so create and store it
// we didn't find it, it's new, so create and store it in the customization list
if (!found_sensor) {
EMSESP::webCustomizationService.update(
[&](WebCustomization & settings) {
@@ -636,46 +620,64 @@ void AnalogSensor::publish_values(const bool force) {
Mqtt::queue_publish(topic, doc.as<JsonObject>());
}
// called from emsesp.cpp, similar to the emsdevice->get_value_info
// searches by name
bool AnalogSensor::get_value_info(JsonObject output, const char * cmd, const int8_t id) const {
// called from emsesp.cpp for commands
// searches sensor by name
bool AnalogSensor::get_value_info(JsonObject output, const char * cmd, const int8_t id) {
if (sensors_.empty()) {
return true; // no sensors, return true
}
uint8_t show_all = 0;
if (Helpers::hasValue(cmd)) {
show_all = (strncmp(cmd, F_(info), 4) == 0) ? 1 : (strncmp(cmd, F_(values), 6) == 0) ? 2 : 0;
}
// see if we're showing all sensors
if (show_all) {
for (const auto & sensor : sensors_) {
if (show_all == 1) {
// info
JsonObject dataSensor = output[sensor.name()].to<JsonObject>();
addSensorJson(dataSensor, sensor);
} else {
// values, shortname version. Also used in 'system allvalues'
output[sensor.name()] = sensor.value();
}
}
return true;
}
// make a copy of the string command for parsing
char command_s[30];
strlcpy(command_s, cmd, sizeof(command_s));
char * attribute_s = nullptr;
// check of it a 'commmands' command
if (Helpers::toLower(cmd) == F_(commands)) {
return Command::list(EMSdevice::DeviceType::TEMPERATURESENSOR, output);
}
// this is for a specific sensor
// make a copy of the string command for parsing, and lowercase it
char sensor_name[30] = {'\0'};
char * attribute_s = nullptr;
strlcpy(sensor_name, cmd, sizeof(sensor_name));
auto sensor_lowercase = Helpers::toLower(sensor_name);
// check specific attribute to fetch instead of the complete record
char * breakp = strchr(command_s, '/');
char * breakp = strchr(sensor_name, '/');
if (breakp) {
*breakp = '\0';
attribute_s = breakp + 1;
}
for (const auto & sensor : sensors_) {
if (Helpers::toLower(command_s) == Helpers::toLower(sensor.name().c_str()) || Helpers::atoint(command_s) == sensor.gpio()) {
output["gpio"] = sensor.gpio();
output["name"] = sensor.name();
output["type"] = F_(number);
output["analog"] = FL_(list_sensortype)[sensor.type()];
output["uom"] = EMSdevice::uom_to_string(sensor.uom());
output["offset"] = sensor.offset();
output["factor"] = sensor.factor();
output["value"] = sensor.value();
output["writeable"] = sensor.type() == AnalogType::COUNTER || (sensor.type() >= AnalogType::DIGITAL_OUT && sensor.type() <= AnalogType::PWM_2);
// min/max for writeable analogs
if (sensor.type() == AnalogType::COUNTER) {
output["min"] = 0;
output["max"] = 4000000;
} else if (sensor.type() == AnalogType::DIGITAL_OUT) {
output["min"] = 0;
output["max"] = sensor.gpio() == 25 || sensor.gpio() == 26 ? 255 : 1;
} else if (sensor.type() >= AnalogType::PWM_0 && sensor.type() <= AnalogType::PWM_2) {
output["min"] = 0;
output["max"] = 100;
}
if (sensor_lowercase == Helpers::toLower(sensor.name().c_str()) || Helpers::atoint(sensor_name) == sensor.gpio()) {
// add the details
addSensorJson(output, sensor);
/*
// if someone wants gpio numbers
char gpio_str[9];
snprintf(gpio_str, sizeof(gpio_str), "gpio_%02d", sensor.gpio());
output[gpio_str] = sensor.value();
*/
// if we're filtering on an attribute, go find it
if (attribute_s) {
if (output.containsKey(attribute_s)) {
@@ -685,56 +687,45 @@ bool AnalogSensor::get_value_info(JsonObject output, const char * cmd, const int
return true;
} else {
char error[100];
snprintf(error, sizeof(error), "cannot find attribute %s in entity %s", attribute_s, command_s);
snprintf(error, sizeof(error), "cannot find attribute %s in entity %s", attribute_s, sensor_name);
output.clear();
output["message"] = error;
return false;
}
}
return true;
return true; // found a match, exit
}
}
return false;
return false; // not found
}
// creates JSON doc from values
// returns true if there are no sensors
bool AnalogSensor::command_info(const char * value, const int8_t id, JsonObject output) const {
if (sensors_.empty()) {
return true;
void AnalogSensor::addSensorJson(JsonObject output, const Sensor & sensor) {
output["gpio"] = sensor.gpio();
output["type"] = F_(number);
output["analog"] = FL_(list_sensortype)[sensor.type()];
output["value"] = sensor.value();
output["writeable"] = sensor.type() == AnalogType::COUNTER || (sensor.type() >= AnalogType::DIGITAL_OUT && sensor.type() <= AnalogType::PWM_2);
if (sensor.type() == AnalogType::COUNTER) {
output["min"] = 0;
output["max"] = 4000000;
output["start_value"] = sensor.offset();
output["factor"] = sensor.factor();
output["uom"] = EMSdevice::uom_to_string(sensor.uom());
} else if (sensor.type() == AnalogType::ADC) {
output["offset"] = sensor.offset();
output["factor"] = sensor.factor();
output["uom"] = EMSdevice::uom_to_string(sensor.uom());
} else if (sensor.type() == AnalogType::TIMER || sensor.type() == AnalogType::RATE) {
output["factor"] = sensor.factor();
} else if (sensor.type() >= AnalogType::PWM_0 && sensor.type() <= AnalogType::PWM_2) {
output["frequency"] = sensor.factor();
output["min"] = 0;
output["max"] = 100;
output["uom"] = EMSdevice::uom_to_string(sensor.uom());
} else if (sensor.type() == AnalogType::DIGITAL_OUT) {
output["min"] = 0;
output["max"] = sensor.gpio() == 25 || sensor.gpio() == 26 ? 255 : 1;
}
for (const auto & sensor : sensors_) {
if (id == -1) { // show number and id for info command
JsonObject dataSensor = output[sensor.name()].to<JsonObject>();
dataSensor["gpio"] = sensor.gpio();
dataSensor["type"] = F_(number);
dataSensor["value"] = sensor.value();
dataSensor["analog"] = FL_(list_sensortype)[sensor.type()];
if (sensor.type() == AnalogType::ADC) {
dataSensor["uom"] = EMSdevice::uom_to_string(sensor.uom());
dataSensor["offset"] = sensor.offset();
dataSensor["factor"] = sensor.factor();
} else if (sensor.type() == AnalogType::COUNTER) {
dataSensor["uom"] = EMSdevice::uom_to_string(sensor.uom());
dataSensor["start_value"] = sensor.offset();
dataSensor["factor"] = sensor.factor();
} else if (sensor.type() == AnalogType::TIMER || sensor.type() == AnalogType::RATE) {
dataSensor["factor"] = sensor.factor();
} else if (sensor.type() >= AnalogType::PWM_0 && sensor.type() <= AnalogType::PWM_2) {
dataSensor["uom"] = EMSdevice::uom_to_string(sensor.uom());
dataSensor["frequency"] = sensor.factor();
}
} else if (id == 0) { // output values command
output[sensor.name()] = sensor.value();
} else { // if someone wants gpio numbers
char gpio_str[9];
snprintf(gpio_str, sizeof(gpio_str), "gpio_%02d", sensor.gpio());
output[gpio_str] = sensor.value();
}
}
return (output.size() > 0);
}
// this creates the sensor, initializing everything
@@ -850,20 +841,4 @@ bool AnalogSensor::command_setvalue(const char * value, const int8_t gpio) {
return false;
}
// list commands
bool AnalogSensor::command_commands(const char * value, const int8_t id, JsonObject output) {
return Command::list(EMSdevice::DeviceType::ANALOGSENSOR, output);
}
// hard coded tests
#ifdef EMSESP_TEST
void AnalogSensor::test() {
sensors_.emplace_back(36, "test12", 0, 0.1, 17, AnalogType::ADC);
sensors_.back().set_value(12.4);
sensors_.emplace_back(37, "test13", 0, 0, 0, AnalogType::DIGITAL_IN);
sensors_.back().set_value(13);
}
#endif
} // namespace emsesp

View File

@@ -153,16 +153,9 @@ class AnalogSensor {
}
bool update(uint8_t gpio, const 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) const;
bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1);
void store_counters();
bool command_info(const char * value, const int8_t id, JsonObject output) const;
#if defined(EMSESP_TEST)
void test();
#endif
private:
static constexpr uint8_t MAX_SENSORS = 20;
static constexpr uint32_t MEASURE_ANALOG_INTERVAL = 500;
@@ -172,7 +165,7 @@ class AnalogSensor {
void remove_ha_topic(const int8_t type, const uint8_t id) const;
bool command_setvalue(const char * value, const int8_t gpio);
void measure();
bool command_commands(const char * value, const int8_t id, JsonObject output);
void addSensorJson(JsonObject output, const Sensor & sensor);
std::vector<Sensor> sensors_; // our list of sensors

View File

@@ -304,6 +304,7 @@ uint8_t Command::call(const uint8_t device_type, const char * cmd, const char *
// check if its a call to an end-point of a device
// this is used to fetch the attributes of the device entity, or call a command directly
// for example info, values, commands, etc
bool single_command = (!value || !strlen(value));
if (single_command) {
// exception 1: anything that is from System
@@ -365,10 +366,10 @@ uint8_t Command::call(const uint8_t device_type, const char * cmd, const char *
// report back. If not OK show output from error, other return the HTTP code
if (return_code != CommandRet::OK) {
if (value == nullptr) {
if ((value == nullptr) || (strlen(value) == 0)) {
LOG_ERROR("Command '%s' failed with code: %d", cmd, return_code);
} else {
LOG_ERROR("Command '%s:%s' failed with code: %d", cmd, value, return_code);
LOG_ERROR("Command '%s/%s' failed with code: %d", cmd, value, return_code);
}
return message(return_code, "callback function failed", output);
}
@@ -443,7 +444,7 @@ bool Command::list(const uint8_t device_type, JsonObject output) {
return false;
}
// create a list of commands, sort them
// create a list of commands we have registered, and sort them
std::list<std::string> sorted_cmds;
for (const auto & cf : cmdfunctions_) {
if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN)) {
@@ -452,6 +453,12 @@ bool Command::list(const uint8_t device_type, JsonObject output) {
}
sorted_cmds.sort();
// force add info and commands for those non-EMS devices
if (device_type == EMSdevice::DeviceType::TEMPERATURESENSOR) {
output[F_(info)] = Helpers::translated_word(FL_(info_cmd));
output[F_(commands)] = Helpers::translated_word(FL_(commands_cmd));
}
for (const auto & cl : sorted_cmds) {
for (const auto & cf : cmdfunctions_) {
if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN) && cf.description_ && (cl == std::string(cf.cmd_))) {
@@ -476,16 +483,26 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo
return;
}
// create a list of commands, sort them
// create list of commands we have registered
std::list<std::string> sorted_cmds;
for (const auto & cf : cmdfunctions_) {
if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN)) {
sorted_cmds.push_back((cf.cmd_));
}
}
sorted_cmds.sort();
// if not in verbose mode, just print them on a single line
// non EMS devices always have an info and commands command
bool show_info = (device_type == EMSdevice::DeviceType::TEMPERATURESENSOR || device_type == EMSdevice::DeviceType::ANALOGSENSOR
|| device_type == EMSdevice::DeviceType::SCHEDULER || device_type == EMSdevice::DeviceType::CUSTOM);
if (!verbose && show_info) {
sorted_cmds.push_back(F_(info));
sorted_cmds.push_back(F_(commands));
}
sorted_cmds.sort(); // sort them
// if not in verbose mode, just print them on a single line and exit
if (!verbose) {
for (const auto & cl : sorted_cmds) {
shell.print(cl);
@@ -496,7 +513,16 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo
}
// verbose mode
shell.println();
shell.printfln("\n%s%s %s:%s", COLOR_BOLD_ON, COLOR_YELLOW, EMSdevice::device_type_2_device_name(device_type), COLOR_RESET);
// we hard code 'info' and 'commmands' commands so print them first
if (show_info) {
shell.printf(" info:\t\t\t\t%slists all values %s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN);
shell.println(COLOR_RESET);
shell.printf(" commands:\t\t\t%slists all commands %s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN);
shell.println(COLOR_RESET);
}
for (const auto & cl : sorted_cmds) {
// find and print the description
for (const auto & cf : cmdfunctions_) {
@@ -531,8 +557,6 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo
}
shell.println();
}
shell.println();
}
// see if a device_type is active and has associated commands
@@ -600,67 +624,32 @@ void Command::show_devices(uuid::console::Shell & shell) {
shell.println();
}
// output list of all commands to console
// 'show commmands' : output list of all commands to console
// calls show with verbose mode set
void Command::show_all(uuid::console::Shell & shell) {
shell.printfln("Showing all available commands (%s*%s=authentication not required):", COLOR_BRIGHT_GREEN, COLOR_RESET);
// show system first
shell.print(COLOR_BOLD_ON);
shell.print(COLOR_YELLOW);
shell.printf(" %s: ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::SYSTEM));
shell.print(COLOR_RESET);
// show system ones first
show(shell, EMSdevice::DeviceType::SYSTEM, true);
// show Custom
shell.print(COLOR_BOLD_ON);
shell.print(COLOR_YELLOW);
shell.printf(" %s: ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::CUSTOM));
shell.println(COLOR_RESET);
shell.printf(" info:\t\t\t\t%slists all values %s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN);
shell.println(COLOR_RESET);
shell.printf(" commands:\t\t\t%slists all commands %s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN);
shell.print(COLOR_RESET);
show(shell, EMSdevice::DeviceType::CUSTOM, true);
// show scheduler
shell.print(COLOR_BOLD_ON);
shell.print(COLOR_YELLOW);
shell.printf(" %s: ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::SCHEDULER));
shell.println(COLOR_RESET);
shell.printf(" info:\t\t\t\t%slists all values %s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN);
shell.println(COLOR_RESET);
shell.printf(" commands:\t\t\t%slists all commands %s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN);
shell.print(COLOR_RESET);
show(shell, EMSdevice::DeviceType::SCHEDULER, true);
// show sensors
// then sensors
if (EMSESP::sensor_enabled()) {
shell.print(COLOR_BOLD_ON);
shell.print(COLOR_YELLOW);
shell.printf(" %s: ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::TEMPERATURESENSOR));
shell.print(COLOR_RESET);
show(shell, EMSdevice::DeviceType::TEMPERATURESENSOR, true);
}
if (EMSESP::analog_enabled()) {
shell.print(COLOR_BOLD_ON);
shell.print(COLOR_YELLOW);
shell.printf(" %s: ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::ANALOGSENSOR));
shell.print(COLOR_RESET);
show(shell, EMSdevice::DeviceType::ANALOGSENSOR, true);
}
// do this in the order of factory classes to keep a consistent order when displaying
// now EMS devices, do this in the order of factory classes to keep a consistent order when displaying
for (const auto & device_class : EMSFactory::device_handlers()) {
if (Command::device_has_commands(device_class.first)) {
shell.print(COLOR_BOLD_ON);
shell.print(COLOR_YELLOW);
shell.printf(" %s: ", EMSdevice::device_type_2_device_name(device_class.first));
shell.print(COLOR_RESET);
show(shell, device_class.first, true);
}
}
shell.println();
}
// Extract only the path component from the passed URI and normalized it

View File

@@ -946,7 +946,7 @@ void EMSdevice::generate_values_web(JsonObject output) {
// add name, prefixing the tag if it exists. This is the id used in the WebUI table and must be unique
obj["id"] = dv.has_tag() ? mask + tag_to_string(dv.tag) + " " + fullname : mask + fullname; // suffix tag
// TODO check TAG https://github.com/emsesp/EMS-ESP32/issues/1338
// TAG https://github.com/emsesp/EMS-ESP32/issues/1338
// obj["id"] = dv.has_tag() ? mask + fullname + " " + tag_to_string(dv.tag) : mask + fullname; // suffix tag
// add commands and options
@@ -1060,7 +1060,7 @@ void EMSdevice::generate_values_web_customization(JsonArray output) {
if (fullname) {
obj["n"] = dv.has_tag() ? std::string(tag_to_string(dv.tag)) + " " + fullname : fullname; // prefix tag
// TODO check TAG https://github.com/emsesp/EMS-ESP32/issues/1338
// TAG https://github.com/emsesp/EMS-ESP32/issues/1338
// obj["n"] = (dv.has_tag()) ? fullname + " " + tag_to_string(dv.tag) : fullname; // suffix tag
}
@@ -1408,7 +1408,7 @@ bool EMSdevice::get_value_info(JsonObject output, const char * cmd, const int8_t
if (!fullname.empty()) {
json["fullname"] = dv.has_tag() ? fullname + " " + tag_to_string(dv.tag) : fullname; // suffix tag
// TODO check TAG https://github.com/emsesp/EMS-ESP32/issues/1338
// TAG https://github.com/emsesp/EMS-ESP32/issues/1338
json["fullname"] = dv.has_tag() ? std::string(tag_to_string(dv.tag)) + " " + fullname.c_str() : fullname; // prefix tag
}
@@ -1614,7 +1614,7 @@ bool EMSdevice::generate_values(JsonObject output, const uint8_t tag_filter, con
// add tag
if (have_tag) {
snprintf(name, sizeof(name), "%s %s (%s)", tag_to_string(dv.tag), fullname.c_str(), dv.short_name); // prefix tag
// TODO check TAG https://github.com/emsesp/EMS-ESP32/issues/1338
// TAG https://github.com/emsesp/EMS-ESP32/issues/1338
// snprintf(name, sizeof(name), "%s %s (%s)", fullname.c_str(), tag_to_string(dv.tag), dv.short_name); // sufix tag
} else {
snprintf(name, sizeof(name), "%s (%s)", fullname.c_str(), dv.short_name);

View File

@@ -664,9 +664,10 @@ void EMSESP::publish_response(std::shared_ptr<const Telegram> telegram) {
buffer = nullptr;
}
// builds json with the detail of each value,
// for a specific EMS device type or the sensors, scheduler and custom entities
// builds json with the detail of each value, for an EMS device
// for other types like sensors, scheduler, custom entities it will process single commands like 'info', 'values', 'commands'...
bool EMSESP::get_device_value_info(JsonObject root, const char * cmd, const int8_t id, const uint8_t devicetype) {
// check first for EMS devices
for (const auto & emsdevice : emsdevices) {
if (emsdevice->device_type() == devicetype) {
if (emsdevice->get_value_info(root, cmd, id)) {
@@ -675,24 +676,24 @@ bool EMSESP::get_device_value_info(JsonObject root, const char * cmd, const int8
}
}
// specific for the temperaturesensor
// temperaturesensor
if (devicetype == DeviceType::TEMPERATURESENSOR) {
return EMSESP::temperaturesensor_.get_value_info(root, cmd, id);
return temperaturesensor_.get_value_info(root, cmd, id);
}
// analog sensor
if (devicetype == DeviceType::ANALOGSENSOR) {
return EMSESP::analogsensor_.get_value_info(root, cmd, id);
return analogsensor_.get_value_info(root, cmd, id);
}
// scheduler
if (devicetype == DeviceType::SCHEDULER) {
return EMSESP::webSchedulerService.get_value_info(root, cmd);
return webSchedulerService.get_value_info(root, cmd);
}
// custom entities
if (devicetype == DeviceType::CUSTOM) {
return EMSESP::webCustomEntityService.get_value_info(root, cmd);
return webCustomEntityService.get_value_info(root, cmd);
}
char error[100];
@@ -1200,7 +1201,9 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const
// Print to LOG showing we've added a new device
LOG_INFO("Recognized new %s with deviceID 0x%02X", EMSdevice::device_type_2_device_name(device_type), device_id);
// add command commands for all devices, except for connect, controller and gateway
// add commands 'info', 'commands', 'values', 'entities' for all EMS devices
// and register the MQTT subscribe topic for this device
// except for connect, controller and gateway
if ((device_type == DeviceType::CONNECT) || (device_type == DeviceType::CONTROLLER) || (device_type == DeviceType::GATEWAY)) {
return true;
}

View File

@@ -119,15 +119,15 @@ bool System::command_allvalues(const char * value, const int8_t id, JsonObject o
emsdevice->generate_values(device_output, DeviceValueTAG::TAG_NONE, true, EMSdevice::OUTPUT_TARGET::API_VERBOSE); // use nested for id -1 and 0
}
// Custom entities
// Custom Entities
device_output = output["Custom Entities"].to<JsonObject>();
EMSESP::webCustomEntityService.get_value_info(device_output, "");
// Sensors
device_output = output["Analog Sensors"].to<JsonObject>();
EMSESP::analogsensor_.command_info(nullptr, 0, device_output);
EMSESP::analogsensor_.get_value_info(device_output, "values");
device_output = output["Temperature Sensors"].to<JsonObject>();
EMSESP::temperaturesensor_.command_info(nullptr, 0, device_output);
EMSESP::temperaturesensor_.get_value_info(device_output, "values");
return true;
}
@@ -1233,23 +1233,23 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output
node["network"] = "Ethernet";
node["hostname"] = ETH.getHostname();
// node["MAC"] = ETH.macAddress();
node["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask());
node["IPv4 gateway"] = uuid::printable_to_string(ETH.gatewayIP());
node["IPv4 nameserver"] = uuid::printable_to_string(ETH.dnsIP());
if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000") {
node["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6());
}
// node["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask());
// node["IPv4 gateway"] = uuid::printable_to_string(ETH.gatewayIP());
// node["IPv4 nameserver"] = uuid::printable_to_string(ETH.dnsIP());
// if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000") {
// node["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6());
// }
} else if (WiFi.status() == WL_CONNECTED) {
node["network"] = "WiFi";
node["hostname"] = WiFi.getHostname();
node["RSSI"] = WiFi.RSSI();
// node["MAC"] = WiFi.macAddress();
node["IPv4 address"] = uuid::printable_to_string(WiFi.localIP()) + "/" + uuid::printable_to_string(WiFi.subnetMask());
node["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP());
node["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP());
if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000") {
node["IPv6 address"] = uuid::printable_to_string(WiFi.localIPv6());
}
// node["IPv4 address"] = uuid::printable_to_string(WiFi.localIP()) + "/" + uuid::printable_to_string(WiFi.subnetMask());
// node["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP());
// node["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP());
// if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000") {
// node["IPv6 address"] = uuid::printable_to_string(WiFi.localIPv6());
// }
}
#endif
EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & settings) {

View File

@@ -509,8 +509,8 @@ void TxService::add(uint8_t operation, const uint8_t * data, const uint8_t lengt
}
if (operation == Telegram::Operation::TX_RAW) {
if (src != ems_bus_id()) {
operation = Telegram::Operation::NONE; // do not check reply/ack for other ids
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);

View File

@@ -45,24 +45,6 @@ void TemperatureSensor::start() {
LOG_INFO("Starting Temperature sensor service");
#endif
// Add API calls
Command::add(
EMSdevice::DeviceType::TEMPERATURESENSOR,
F_(info),
[&](const char * value, const int8_t id, JsonObject output) { return command_info(value, id, output); },
FL_(info_cmd));
Command::add(
EMSdevice::DeviceType::TEMPERATURESENSOR,
F_(values),
[&](const char * value, const int8_t id, JsonObject output) { return command_info(value, 0, output); },
nullptr,
CommandFlag::HIDDEN); // this command is hidden
Command::add(
EMSdevice::DeviceType::TEMPERATURESENSOR,
F_(commands),
[&](const char * value, const int8_t id, JsonObject output) { return command_commands(value, id, output); },
FL_(commands_cmd));
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf(topic, sizeof(topic), "%s/#", F_(temperaturesensor));
Mqtt::subscribe(EMSdevice::DeviceType::TEMPERATURESENSOR, topic, nullptr); // use empty function callback
@@ -361,68 +343,59 @@ bool TemperatureSensor::updated_values() {
return false;
}
// list commands
bool TemperatureSensor::command_commands(const char * value, const int8_t id, JsonObject output) {
return Command::list(EMSdevice::DeviceType::TEMPERATURESENSOR, output);
}
// creates JSON doc from values
// returns true if there are no sensors
bool TemperatureSensor::command_info(const char * value, const int8_t id, JsonObject output) {
if (sensors_.empty()) {
return true;
}
for (const auto & sensor : sensors_) {
char val[10];
if (id == -1) { // show number and id, info command
JsonObject dataSensor = output[sensor.name()].to<JsonObject>();
dataSensor["id"] = sensor.id();
dataSensor["uom"] = EMSdevice::uom_to_string(DeviceValueUOM::DEGREES);
dataSensor["type"] = F_(number);
if (Helpers::hasValue(sensor.temperature_c)) {
dataSensor["temp"] = serialized(Helpers::render_value(val, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0));
}
} else if (id == 0 && Helpers::hasValue(sensor.temperature_c)) { // values command
output[sensor.name()] = serialized(Helpers::render_value(val, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0));
} else if (Helpers::hasValue(sensor.temperature_c)) {
output[sensor.id()] = serialized(Helpers::render_value(val, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0));
}
}
return (output.size() > 0);
}
// called from emsesp.cpp, similar to the emsdevice->get_value_info
// called from emsesp.cpp for commands
bool TemperatureSensor::get_value_info(JsonObject output, const char * cmd, const int8_t id) {
if (sensors_.empty()) {
return true; // no sensors, return true
}
uint8_t show_all = 0;
if (Helpers::hasValue(cmd)) {
show_all = (strncmp(cmd, F_(info), 4) == 0) ? 1 : (strncmp(cmd, F_(values), 6) == 0) ? 2 : 0;
}
// see if we're showing all sensors
if (show_all) {
for (const auto & sensor : sensors_) {
if (show_all == 1) {
// info
JsonObject dataSensor = output[sensor.name()].to<JsonObject>();
addSensorJson(dataSensor, sensor);
} else {
// values, shortname version. Also used in 'system allvalues'
if (Helpers::hasValue(sensor.temperature_c)) {
char val[10];
output[sensor.name()] = serialized(Helpers::render_value(val, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0));
}
}
}
return true;
}
// make a copy of the string command for parsing
char command_s[30];
strlcpy(command_s, cmd, sizeof(command_s));
char * attribute_s = nullptr;
// check specific attribute to fetch instead of the complete record
char * breakp = strchr(command_s, '/');
// check of it a 'commmands' command
if (Helpers::toLower(cmd) == F_(commands)) {
return Command::list(EMSdevice::DeviceType::TEMPERATURESENSOR, output);
}
// this is for a specific sensor
// make a copy of the string command for parsing, and lowercase it
char sensor_name[30] = {'\0'};
char * attribute_s = nullptr;
strlcpy(sensor_name, cmd, sizeof(sensor_name));
auto sensor_lowercase = Helpers::toLower(sensor_name);
// check for a specific attribute to fetch instead of the complete record
char * breakp = strchr(sensor_name, '/');
if (breakp) {
*breakp = '\0';
attribute_s = breakp + 1;
}
for (const auto & sensor : sensors_) {
if (Helpers::toLower(command_s) == Helpers::toLower(sensor.name().c_str()) || Helpers::toLower(command_s) == Helpers::toLower(sensor.id().c_str())) {
output["id"] = sensor.id();
output["name"] = sensor.name();
if (Helpers::hasValue(sensor.temperature_c)) {
char val[10];
output["value"] = serialized(Helpers::render_value(val, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0));
}
output["type"] = F_(number);
output["uom"] = EMSdevice::uom_to_string(DeviceValueUOM::DEGREES);
output["writeable"] = false;
// match custom name or sensor ID
if (sensor_lowercase == Helpers::toLower(sensor.name().c_str()) || sensor_lowercase == Helpers::toLower(sensor.id().c_str())) {
// add values
addSensorJson(output, sensor);
// if we're filtering on an attribute, go find it
if (attribute_s) {
if (output.containsKey(attribute_s)) {
@@ -432,18 +405,33 @@ bool TemperatureSensor::get_value_info(JsonObject output, const char * cmd, cons
return true;
} else {
char error[100];
snprintf(error, sizeof(error), "cannot find attribute %s in entity %s", attribute_s, command_s);
snprintf(error, sizeof(error), "cannot find attribute %s in entity %s", attribute_s, sensor_name);
output.clear();
output["message"] = error;
return false;
}
}
return true;
return true; // found a match, exit
}
}
return false;
return false; // not found
}
void TemperatureSensor::addSensorJson(JsonObject output, const Sensor & sensor) {
output["id"] = sensor.id();
output["name"] = sensor.name();
if (Helpers::hasValue(sensor.temperature_c)) {
char val[10];
output["value"] = serialized(Helpers::render_value(val, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0));
}
output["type"] = F_(number);
output["uom"] = EMSdevice::uom_to_string(DeviceValueUOM::DEGREES);
output["writeable"] = false;
}
// publish a single sensor to MQTT
void TemperatureSensor::publish_sensor(const Sensor & sensor) {
if (Mqtt::enabled() && Mqtt::publish_single()) {
@@ -640,16 +628,18 @@ bool TemperatureSensor::Sensor::apply_customization() {
#if defined(EMSESP_TEST)
void TemperatureSensor::test() {
// add 2 temperature sensors
// Sensor ID: 01-0203-0405-0607
uint8_t addr[ADDR_LEN] = {1, 2, 3, 4, 5, 6, 7, 8};
sensors_.emplace_back(addr);
// sensors_.back().apply_customization();
sensors_.back().apply_customization();
sensors_.back().temperature_c = 123;
sensors_.back().read = true;
publish_sensor(sensors_.back()); // call publish single
// Sensor ID: 0B-0C0D-0E0F-1011
uint8_t addr2[ADDR_LEN] = {11, 12, 13, 14, 15, 16, 17, 18};
sensors_.emplace_back(addr2);
// sensors_.back().apply_customization();
sensors_.back().apply_customization();
sensors_.back().temperature_c = 456;
sensors_.back().read = true;
publish_sensor(sensors_.back()); // call publish single

View File

@@ -83,7 +83,7 @@ class TemperatureSensor {
void publish_values(const bool force);
void reload();
bool updated_values();
bool get_value_info(JsonObject output, const char * cmd, const int8_t id);
bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1);
// return back reference to the sensor list, used by other classes
std::vector<Sensor> sensors() const {
@@ -112,8 +112,6 @@ class TemperatureSensor {
bool update(const std::string & id, const std::string & name, int16_t offset);
bool command_info(const char * value, const int8_t id, JsonObject output);
#if defined(EMSESP_TEST)
void test();
#endif
@@ -154,8 +152,7 @@ class TemperatureSensor {
int16_t get_temperature_c(const uint8_t addr[]);
uint64_t get_id(const uint8_t addr[]);
void remove_ha_topic(const std::string & id);
bool command_commands(const char * value, const int8_t id, JsonObject output);
void addSensorJson(JsonObject output, const Sensor & sensor);
std::vector<Sensor> sensors_; // our list of active sensors

View File

@@ -55,7 +55,7 @@ bool Test::test(const std::string & cmd, int8_t id1, int8_t id2) {
// System::test_set_all_active(true); // uncomment if we want to show all entities and give them fake values
add_device(0x08, 123); // Nefit Trendline
add_device(0x18, 157); // Bosch CR100
add_device(0x18, 157); // RC200/CW100
// add_device(0x10, 158); // RC300 - there's no data here
@@ -307,12 +307,14 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
}
if (command == "general") {
shell.printfln("Testing adding a boiler, thermostat and sensors...");
shell.printfln("Testing adding a boiler, thermostat, all sensors, scheduler and custom entities...");
test("general");
// add sensors
emsesp::EMSESP::analogsensor_.test();
emsesp::EMSESP::temperaturesensor_.test();
// setup fake data
EMSESP::webCustomizationService.test(); // set customizations
EMSESP::temperaturesensor_.test(); // add temperature sensors
EMSESP::webSchedulerService.test(); // add scheduler items
EMSESP::webCustomEntityService.test(); // add custom entities
// shell.invoke_command("show devices");
// shell.invoke_command("show values");
@@ -331,16 +333,40 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
}
if (command == "custom_entities") {
shell.printfln("custom entities...");
test("general");
shell.printfln("Adding custom entities...");
// add some dummy entities
EMSESP::webCustomEntityService.test();
#ifdef EMSESP_STANDALONE
AsyncWebServerRequest request;
request.method(HTTP_GET);
request.url("/api/custom");
request.url("/api/custom/boiler_flowtemp");
request.url("/api/custom/boiler_flowtemp2");
EMSESP::webAPIService.webAPIService_get(&request);
request.url("/api/custom/test_custom");
EMSESP::webAPIService.webAPIService_get(&request);
request.url("/api/custom/test_read_only");
EMSESP::webAPIService.webAPIService_get(&request);
request.url("/api/custom/test_ram");
EMSESP::webAPIService.webAPIService_get(&request);
shell.invoke_command("call custom info");
#endif
ok = true;
}
if (command == "scheduler") {
shell.printfln("Adding Scheduler items...");
// add some dummy entities
EMSESP::webSchedulerService.test();
#ifdef EMSESP_STANDALONE
AsyncWebServerRequest request;
request.method(HTTP_GET);
request.url("/api/scheduler");
EMSESP::webAPIService.webAPIService_get(&request);
shell.invoke_command("call scheduler info");
#endif
ok = true;
}
@@ -574,7 +600,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
test("boiler");
test("thermostat");
JsonDocument doc; // some absurd high number
JsonDocument doc;
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice) {
doc.clear();
@@ -718,7 +744,11 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
shell.printfln("Testing device value rendering");
Mqtt::ha_enabled(true);
// Mqtt::ha_enabled(false);
Mqtt::nested_format(1);
// Mqtt::nested_format(0);
// Mqtt::send_response(false);
test("boiler");
@@ -731,8 +761,28 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
if (command == "temperature") {
shell.printfln("Testing adding Temperature sensor");
shell.invoke_command("show commands");
// load some EMS data
// test("general");
emsesp::EMSESP::temperaturesensor_.test();
shell.invoke_command("call temperaturesensor");
shell.invoke_command("show values");
shell.invoke_command("call system allvalues");
shell.invoke_command("call temperaturesensor info");
shell.invoke_command("call temperaturesensor values");
AsyncWebServerRequest request;
request.method(HTTP_GET);
request.url("/api/temperaturesensor/commands");
EMSESP::webAPIService.webAPIService_get(&request);
request.url("/api/temperaturesensor/info");
EMSESP::webAPIService.webAPIService_get(&request);
request.url("/api/temperaturesensor/01-0203-0405-0607");
EMSESP::webAPIService.webAPIService_get(&request);
ok = true;
}
@@ -757,18 +807,37 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
shell.printfln("Testing adding Analog sensor");
Mqtt::ha_enabled(true);
// Mqtt::ha_enabled(false);
Mqtt::nested_format(1);
// Mqtt::nested_format(0);
emsesp::EMSESP::analogsensor_.test();
shell.invoke_command("show values");
// shell.invoke_command("call system publish");
// shell.invoke_command("show mqtt");
// Mqtt::send_response(false);
// rename
// bool update(uint8_t id, const std::string & name, int16_t offset, float factor, uint8_t uom, uint8_t type);
EMSESP::analogsensor_.update(36, "analogtest", 2, 0.7, 17, 1);
// load some EMS data
test("general");
EMSESP::webCustomizationService.test(); // load the analog sensors
shell.invoke_command("call analogsensor");
shell.invoke_command("show values");
shell.invoke_command("call system allvalues");
shell.invoke_command("call analogsensor info");
shell.invoke_command("call analogsensor values");
AsyncWebServerRequest request;
request.method(HTTP_GET);
request.url("/api/analogsensor/commands");
EMSESP::webAPIService.webAPIService_get(&request);
request.url("/api/analogsensor/info");
EMSESP::webAPIService.webAPIService_get(&request);
request.url("/api/analogsensor/test_analog1");
request.url("/api/analogsensor/36");
EMSESP::webAPIService.webAPIService_get(&request);
// test renaming it
// bool update(uint8_t id, const std::string & name, int16_t offset, float factor, uint8_t uom, uint8_t type);
// EMSESP::analogsensor_.update(36, "test_analog1_new", 2, 0.7, 17, 1);
// shell.invoke_command("show values");
// shell.invoke_command("call system publish");
ok = true;
}
@@ -784,25 +853,26 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const
ok = true;
}
if (command == "custom") {
shell.printfln("Testing custom entities");
if (command == "customization") {
shell.printfln("Testing customization renaming entity");
Mqtt::ha_enabled(true);
// Mqtt::send_response(false);
test("thermostat");
// before
// shell.invoke_command("call thermostat seltemp");
// shell.invoke_command("call system publish");
// toggle mode
// find thermostat
for (const auto & emsdevice : EMSESP::emsdevices) {
Serial.print("Custom: ");
Serial.print(emsdevice->device_type_name());
Serial.print(" uniqueid=");
Serial.println(emsdevice->unique_id());
if (emsdevice->unique_id() == 1) { // thermostat
if (emsdevice->unique_id() == 1) {
Serial.println();
Serial.print("Custom: ");
Serial.print(emsdevice->device_type_name());
Serial.print(" uniqueid=");
Serial.println(emsdevice->unique_id());
std::string a = "00hc1/seltemp|new name>5<52";
emsdevice->setCustomizationEntity(a);
break;

View File

@@ -27,7 +27,7 @@
namespace emsesp {
#define EMSESP_DEBUG_DEFAULT "general"
// #define EMSESP_DEBUG_DEFAULT "general"
// #define EMSESP_DEBUG_DEFAULT "thermostat"
// #define EMSESP_DEBUG_DEFAULT "solar"
@@ -48,15 +48,16 @@ namespace emsesp {
// #define EMSESP_DEBUG_DEFAULT "lastcode"
// #define EMSESP_DEBUG_DEFAULT "2thermostats"
// #define EMSESP_DEBUG_DEFAULT "temperature"
// #define EMSESP_DEBUG_DEFAULT "analog"
#define EMSESP_DEBUG_DEFAULT "analog"
// #define EMSESP_DEBUG_DEFAULT "api_values"
// #define EMSESP_DEBUG_DEFAULT "mqtt_post"
// #define EMSESP_DEBUG_DEFAULT "api_wwmode"
// #define EMSESP_DEBUG_DEFAULT "custom"
// #define EMSESP_DEBUG_DEFAULT "customization"
// #define EMSESP_DEBUG_DEFAULT "entity_dump"
// #define EMSESP_DEBUG_DEFAULT "memory"
// #define EMSESP_DEBUG_DEFAULT "coldshot"
// #define EMSESP_DEBUG_DEFAULT "custom_entities"
// #define EMSESP_DEBUG_DEFAULT "scheduler"
// #define EMSESP_DEBUG_DEFAULT "heat_exchange"
class Test {

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.6.5-test.10"
#define EMSESP_APP_VERSION "3.6.5-test.11"

View File

@@ -152,12 +152,13 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) {
#if defined(EMSESP_STANDALONE)
Serial.print(COLOR_YELLOW);
Serial.print("web response code: ");
Serial.println(ret_codes[return_code]);
Serial.print("data: ");
if (output.size()) {
serializeJsonPretty(output, Serial);
serializeJson(output, Serial);
}
Serial.println();
Serial.print(" (response code ");
Serial.print(ret_codes[return_code]);
Serial.println(")");
Serial.print(COLOR_RESET);
#endif
}

View File

@@ -36,7 +36,7 @@ WebCustomEntityService::WebCustomEntityService(AsyncWebServer * server, FS * fs,
// load the settings when the service starts
void WebCustomEntityService::begin() {
_fsPersistence.readFromFS();
EMSESP::logger().info("Starting Custom entity service");
EMSESP::logger().info("Starting Custom Entity service");
Mqtt::subscribe(EMSdevice::DeviceType::CUSTOM, "custom/#", nullptr); // use empty function callback
}
@@ -61,31 +61,17 @@ void WebCustomEntity::read(WebCustomEntity & webEntity, JsonObject root) {
}
}
// call on initialization and also when the Entity web page is updated
// call on initialization and also when the Entity web page is updated/saved
// this loads the data into the internal class
StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & webCustomEntity) {
#ifdef EMSESP_STANDALONE
// invoke some fake data for testing
// clang-format off
/* prettier-ignore */
const char * json =
"{\"entities\": [{\"id\":0,\"device_id\":8,\"type_id\":24,\"offset\":0,\"factor\":1,\"name\":\"boiler_flowtemp\",\"uom\":1,\"value_type\":1,\"writeable\":true}]}";
// clang-format on
JsonDocument doc;
deserializeJson(doc, json);
root = doc.as<JsonObject>();
Serial.print(COLOR_BRIGHT_MAGENTA);
Serial.print(" Using fake custom entity file: ");
serializeJson(root, Serial);
Serial.println(COLOR_RESET);
#endif
// reset everything to start fresh
for (CustomEntityItem & entityItem : webCustomEntity.customEntityItems) {
Command::erase_command(EMSdevice::DeviceType::CUSTOM, entityItem.name.c_str());
}
webCustomEntity.customEntityItems.clear();
EMSESP::webCustomEntityService.ha_reset();
// rebuild the list
if (root["entities"].is<JsonArray>()) {
for (const JsonObject ei : root["entities"].as<JsonArray>()) {
auto entityItem = CustomEntityItem();
@@ -120,10 +106,13 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web
} else if (entityItem.value_type == DeviceValueType::ULONG || entityItem.value_type == DeviceValueType::TIME) {
entityItem.value = EMS_VALUE_DEFAULT_ULONG;
}
if (entityItem.factor == 0) {
entityItem.factor = 1;
}
webCustomEntity.customEntityItems.push_back(entityItem); // add to list
if (entityItem.writeable) {
Command::add(
EMSdevice::DeviceType::CUSTOM,
@@ -262,15 +251,16 @@ void WebCustomEntityService::show_values(JsonObject output) {
}
}
// process json output for info/commands and value_info
bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) {
EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; });
// if it's commands...
if (Helpers::toLower(cmd) == F_(commands)) {
output[F_(info)] = Helpers::translated_word(FL_(info_cmd));
output[F_(commands)] = Helpers::translated_word(FL_(commands_cmd));
for (const auto & entity : *customEntityItems) {
output[entity.name] = "custom entitiy";
output[entity.name] = "custom entity";
}
return true;
}
@@ -281,6 +271,7 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd)
return true;
}
// if it's info or values...
if (strlen(cmd) == 0 || Helpers::toLower(cmd) == F_(values) || Helpers::toLower(cmd) == F_(info)) {
// list all names
for (const CustomEntityItem & entity : *customEntityItems) {
@@ -350,12 +341,14 @@ void WebCustomEntityService::publish_single(const CustomEntityItem & entity) {
if (!Mqtt::enabled() || !Mqtt::publish_single()) {
return;
}
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
if (Mqtt::publish_single2cmd()) {
snprintf(topic, sizeof(topic), "%s/%s", "custom", entity.name.c_str());
} else {
snprintf(topic, sizeof(topic), "%s/%s", "custom_data", entity.name.c_str());
}
JsonDocument doc;
JsonObject output = doc.to<JsonObject>();
render_value(output, entity, true);
@@ -473,6 +466,7 @@ uint8_t WebCustomEntityService::count_entities() {
render_value(output, entity);
count += (output.containsKey(entity.name) || entity.writeable) ? 1 : 0;
}
return count;
}
@@ -482,6 +476,7 @@ uint8_t WebCustomEntityService::has_commands() {
for (const CustomEntityItem & entity : *customEntityItems) {
count += entity.writeable ? 1 : 0;
}
return count;
}
@@ -547,6 +542,7 @@ void WebCustomEntityService::generate_value_web(JsonObject output) {
default:
break;
}
// show only entities with value or command
if (!obj.containsKey("v") && !obj.containsKey("c")) {
data.remove(index);
@@ -560,6 +556,7 @@ void WebCustomEntityService::generate_value_web(JsonObject output) {
void WebCustomEntityService::fetch() {
EMSESP::webCustomEntityService.read([&](WebCustomEntity & webEntity) { customEntityItems = &webEntity.customEntityItems; });
const uint8_t len[] = {1, 1, 1, 2, 2, 3, 3};
for (auto & entity : *customEntityItems) {
if (entity.device_id > 0 && entity.type_id > 0) { // ths excludes also RAM type
bool needFetch = true;
@@ -615,11 +612,65 @@ bool WebCustomEntityService::get_value(std::shared_ptr<const Telegram> telegram)
// EMSESP::logger().debug("custom entity %s received with value %d", entity.name.c_str(), (int)entity.val);
}
}
if (has_change) {
publish();
return true;
}
return false;
}
// hard coded tests
#ifdef EMSESP_TEST
void WebCustomEntityService::test() {
update(
[&](WebCustomEntity & webCustomEntity) {
webCustomEntity.customEntityItems.clear();
// test 1
auto entityItem = CustomEntityItem();
entityItem.ram = 0;
entityItem.device_id = 8;
entityItem.type_id = 24;
entityItem.offset = 0;
entityItem.factor = 1;
entityItem.name = "test_custom";
entityItem.uom = 1;
entityItem.value_type = 1;
entityItem.writeable = true;
entityItem.data = "70";
webCustomEntity.customEntityItems.push_back(entityItem);
// test 2
entityItem.ram = 0;
entityItem.device_id = 24;
entityItem.type_id = 677;
entityItem.offset = 3;
entityItem.factor = 1;
entityItem.name = "test_read_only";
entityItem.uom = 0;
entityItem.value_type = 2;
entityItem.writeable = false;
entityItem.data = "48";
webCustomEntity.customEntityItems.push_back(entityItem);
// test 2
entityItem.ram = 1;
entityItem.device_id = 0;
entityItem.type_id = 0;
entityItem.offset = 0;
entityItem.factor = 1;
entityItem.name = "test_ram";
entityItem.uom = 0;
entityItem.value_type = 8;
entityItem.writeable = true;
entityItem.data = "14";
webCustomEntity.customEntityItems.push_back(entityItem);
return StateUpdateResult::CHANGED; // persist the changes
},
"local");
}
#endif
} // namespace emsesp

View File

@@ -38,7 +38,7 @@ class CustomEntityItem {
bool writeable;
uint32_t value;
std::string data;
uint8_t ram;
uint8_t ram;
};
class WebCustomEntity {
@@ -70,6 +70,10 @@ class WebCustomEntityService : public StatefulService<WebCustomEntity> {
ha_registered_ = false;
}
#if defined(EMSESP_TEST)
void test();
#endif
private:
HttpEndpoint<WebCustomEntity> _httpEndpoint;
FSPersistence<WebCustomEntity> _fsPersistence;

View File

@@ -89,19 +89,6 @@ void WebCustomization::read(WebCustomization & customizations, JsonObject root)
// call on initialization and also when the page is saved via web UI
// this loads the data into the internal class
StateUpdateResult WebCustomization::update(JsonObject root, WebCustomization & customizations) {
#ifdef EMSESP_STANDALONE
// invoke some fake data for testing
const char * json = "{\"ts\":[],\"as\":[],\"masked_entities\":[{\"product_id\":123,\"device_id\":8,\"entity_ids\":[\"08heatingactive|my custom "
"name for heating active (HS1)\",\"08tapwateractive\"]}]}";
JsonDocument doc;
deserializeJson(doc, json);
root = doc.as<JsonObject>();
Serial.print(COLOR_BRIGHT_MAGENTA);
Serial.print(" Using fake customization file: ");
serializeJson(root, Serial);
Serial.println(COLOR_RESET);
#endif
// Temperature Sensor customization
customizations.sensorCustomizations.clear();
if (root["ts"].is<JsonArray>()) {
@@ -120,35 +107,36 @@ StateUpdateResult WebCustomization::update(JsonObject root, WebCustomization & c
if (root["as"].is<JsonArray>()) {
for (const JsonObject analogJson : root["as"].as<JsonArray>()) {
// create each of the sensor, overwriting any previous settings
auto sensor = AnalogCustomization();
sensor.gpio = analogJson["gpio"];
sensor.name = analogJson["name"].as<std::string>();
sensor.offset = analogJson["offset"];
sensor.factor = analogJson["factor"];
sensor.uom = analogJson["uom"];
sensor.type = analogJson["type"];
if (_start && sensor.type == EMSESP::analogsensor_.AnalogType::DIGITAL_OUT && sensor.uom > DeviceValue::DeviceValueUOM::NONE) {
sensor.offset = sensor.uom - 1;
auto analog = AnalogCustomization();
analog.gpio = analogJson["gpio"];
analog.name = analogJson["name"].as<std::string>();
analog.offset = analogJson["offset"];
analog.factor = analogJson["factor"];
analog.uom = analogJson["uom"];
analog.type = analogJson["type"];
if (_start && analog.type == EMSESP::analogsensor_.AnalogType::DIGITAL_OUT && analog.uom > DeviceValue::DeviceValueUOM::NONE) {
analog.offset = analog.uom - 1;
}
customizations.analogCustomizations.push_back(sensor); // add to list
customizations.analogCustomizations.push_back(analog); // add to list
}
}
_start = false;
// load array of entities id's with masks, building up the object class
customizations.entityCustomizations.clear();
if (root["masked_entities"].is<JsonArray>()) {
for (const JsonObject masked_entities : root["masked_entities"].as<JsonArray>()) {
auto new_entry = EntityCustomization();
new_entry.product_id = masked_entities["product_id"];
new_entry.device_id = masked_entities["device_id"];
auto emsEntity = EntityCustomization();
emsEntity.product_id = masked_entities["product_id"];
emsEntity.device_id = masked_entities["device_id"];
for (const JsonVariant masked_entity_id : masked_entities["entity_ids"].as<JsonArray>()) {
if (masked_entity_id.is<std::string>()) {
new_entry.entity_ids.push_back(masked_entity_id.as<std::string>()); // add entity list
emsEntity.entity_ids.push_back(masked_entity_id.as<std::string>()); // add entity list
}
}
customizations.entityCustomizations.push_back(new_entry); // save the new object
customizations.entityCustomizations.push_back(emsEntity); // save the new object
}
}
@@ -256,6 +244,7 @@ void WebCustomizationService::customization_entities(AsyncWebServerRequest * req
}
// emsesp::EMSESP::logger().info(id.as<const char *>());
}
// add deleted entities from file
read([&](WebCustomization & settings) {
for (EntityCustomization entityCustomization : settings.entityCustomizations) {
@@ -282,6 +271,7 @@ void WebCustomizationService::customization_entities(AsyncWebServerRequest * req
}
}
});
// get list of entities that have masks set or a custom fullname
emsdevice->getCustomizationEntities(entity_ids);
@@ -326,4 +316,61 @@ void WebCustomizationService::begin() {
_fsPersistence.readFromFS();
}
} // namespace emsesp
// hard coded tests
#ifdef EMSESP_TEST
void WebCustomizationService::test() {
update(
[&](WebCustomization & webCustomization) {
// Temperature sensors
webCustomization.sensorCustomizations.clear();
auto sensor = SensorCustomization();
sensor.id = "01-0203-0405-0607";
sensor.name = "test_sensor1";
sensor.offset = 0;
webCustomization.sensorCustomizations.push_back(sensor);
sensor = SensorCustomization();
sensor.id = "0B-0C0D-0E0F-1011";
sensor.name = "test_sensor2";
sensor.offset = 4;
webCustomization.sensorCustomizations.push_back(sensor);
// Analog sensors
// This actually adds the sensors as we use customizations to store them
webCustomization.analogCustomizations.clear();
auto analog = AnalogCustomization();
analog.gpio = 36;
analog.name = "test_analog1";
analog.offset = 0;
analog.factor = 0.1;
analog.uom = 17;
analog.type = 3;
webCustomization.analogCustomizations.push_back(analog);
analog = AnalogCustomization();
analog.gpio = 37;
analog.name = "test_analog2";
analog.offset = 0;
analog.factor = 1;
analog.uom = 0;
analog.type = 1;
webCustomization.analogCustomizations.push_back(analog);
// EMS entities
webCustomization.entityCustomizations.clear();
auto emsEntity = EntityCustomization();
emsEntity.product_id = 123;
emsEntity.device_id = 8;
emsEntity.entity_ids.push_back("08heatingactive|is my heating on?");
webCustomization.entityCustomizations.push_back(emsEntity);
return StateUpdateResult::CHANGED; // persist the changes
},
"local");
EMSESP::analogsensor_.reload(); // this is needed to active the analog sensors
}
#endif
} // namespace emsesp

View File

@@ -83,6 +83,10 @@ class WebCustomizationService : public StatefulService<WebCustomization> {
void begin();
#if defined(EMSESP_TEST)
void test();
#endif
// make all functions public so we can test in the debug and standalone mode
#ifndef EMSESP_STANDALONE
private:

View File

@@ -17,6 +17,7 @@
*/
#include "emsesp.h"
#include "WebSchedulerService.h"
namespace emsesp {
@@ -30,6 +31,13 @@ WebSchedulerService::WebSchedulerService(AsyncWebServer * server, FS * fs, Secur
// load the settings when the service starts
void WebSchedulerService::begin() {
_fsPersistence.readFromFS();
// save a local pointer to the scheduler item list
EMSESP::webSchedulerService.read([&](WebScheduler & webScheduler) {
//
scheduleItems_ = &webScheduler.scheduleItems;
});
EMSESP::logger().info("Starting Scheduler service");
Mqtt::subscribe(EMSdevice::DeviceType::SCHEDULER, "scheduler/#", nullptr); // use empty function callback
}
@@ -54,26 +62,15 @@ void WebScheduler::read(WebScheduler & webScheduler, JsonObject root) {
// call on initialization and also when the Schedule web page is saved
// this loads the data into the internal class
StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webScheduler) {
#ifdef EMSESP_STANDALONE
// invoke some fake data for testing
const char * json =
"{\"schedule\": [{\"id\":1,\"active\":true,\"flags\":31,\"time\": \"07:30\",\"cmd\": \"hc1mode\",\"value\": \"day\",\"name\": \"turn on "
"central heating\"}]}";
JsonDocument doc;
deserializeJson(doc, json);
root = doc.as<JsonObject>();
Serial.print(COLOR_BRIGHT_MAGENTA);
Serial.print(" Using fake scheduler file: ");
serializeJson(root, Serial);
Serial.println(COLOR_RESET);
#endif
for (ScheduleItem & scheduleItem : webScheduler.scheduleItems) {
Command::erase_command(EMSdevice::DeviceType::SCHEDULER, scheduleItem.name.c_str());
}
// reset the list
webScheduler.scheduleItems.clear();
EMSESP::webSchedulerService.ha_reset();
// build up the list of schedule items
if (root["schedule"].is<JsonArray>()) {
for (const JsonObject schedule : root["schedule"].as<JsonArray>()) {
// create each schedule item, overwriting any previous settings
@@ -103,7 +100,9 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu
}
}
}
EMSESP::webSchedulerService.publish(true);
return StateUpdateResult::CHANGED;
}
@@ -114,17 +113,19 @@ bool WebSchedulerService::command_setvalue(const char * value, const std::string
return false;
}
EMSESP::webSchedulerService.read([&](WebScheduler & webScheduler) { scheduleItems = &webScheduler.scheduleItems; });
for (ScheduleItem & scheduleItem : *scheduleItems) {
for (ScheduleItem & scheduleItem : *scheduleItems_) {
if (scheduleItem.name == name) {
if (scheduleItem.active == v) {
return true;
}
scheduleItem.active = v;
publish_single(name.c_str(), v);
if (EMSESP::mqtt_.get_publish_onchange(0)) {
publish();
}
return true;
}
}
@@ -133,25 +134,27 @@ bool WebSchedulerService::command_setvalue(const char * value, const std::string
// process json output for info/commands and value_info
bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) {
EMSESP::webSchedulerService.read([&](WebScheduler & webScheduler) { scheduleItems = &webScheduler.scheduleItems; });
// check of it a 'commmands' command
if (Helpers::toLower(cmd) == F_(commands)) {
output[F_(info)] = Helpers::translated_word(FL_(info_cmd));
output[F_(commands)] = Helpers::translated_word(FL_(commands_cmd));
for (const ScheduleItem & scheduleItem : *scheduleItems) {
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
if (!scheduleItem.name.empty()) {
output[scheduleItem.name] = "activate schedule";
}
}
return true;
}
if (scheduleItems->size() == 0) {
if (scheduleItems_->size() == 0) {
return true;
}
if (strlen(cmd) == 0 || Helpers::toLower(cmd) == F_(values) || Helpers::toLower(cmd) == F_(info)) {
// list all names
for (const ScheduleItem & scheduleItem : *scheduleItems) {
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
if (!scheduleItem.name.empty()) {
if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) {
output[scheduleItem.name] = scheduleItem.active;
@@ -163,6 +166,7 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) {
}
}
}
return (output.size() > 0);
}
@@ -177,7 +181,7 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) {
attribute_s = breakp + 1;
}
for (const ScheduleItem & scheduleItem : *scheduleItems) {
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
if (Helpers::toLower(scheduleItem.name) == Helpers::toLower(command_s)) {
output["name"] = scheduleItem.name;
output["type"] = "boolean";
@@ -238,20 +242,19 @@ void WebSchedulerService::publish(const bool force) {
return;
}
EMSESP::webSchedulerService.read([&](WebScheduler & webScheduler) { scheduleItems = &webScheduler.scheduleItems; });
if (scheduleItems->size() == 0) {
if (scheduleItems_->size() == 0) {
return;
}
if (Mqtt::publish_single() && force) {
for (const ScheduleItem & scheduleItem : *scheduleItems) {
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
publish_single(scheduleItem.name.c_str(), scheduleItem.active);
}
}
JsonDocument doc;
bool ha_created = ha_registered_;
for (const ScheduleItem & scheduleItem : *scheduleItems) {
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
if (!scheduleItem.name.empty() && !doc.containsKey(scheduleItem.name)) {
if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) {
doc[scheduleItem.name] = scheduleItem.active;
@@ -314,16 +317,16 @@ void WebSchedulerService::publish(const bool force) {
}
bool WebSchedulerService::has_commands() {
EMSESP::webSchedulerService.read([&](WebScheduler & webScheduler) { scheduleItems = &webScheduler.scheduleItems; });
if (scheduleItems->size() == 0) {
if (scheduleItems_->size() == 0) {
return false;
}
for (const ScheduleItem & scheduleItem : *scheduleItems) {
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
if (!scheduleItem.name.empty()) {
return true;
}
}
return false;
}
@@ -375,14 +378,13 @@ void WebSchedulerService::loop() {
static uint32_t last_uptime_min = 0;
// get list of scheduler events and exit if it's empty
EMSESP::webSchedulerService.read([&](WebScheduler & webScheduler) { scheduleItems = &webScheduler.scheduleItems; });
if (scheduleItems->size() == 0) {
if (scheduleItems_->size() == 0) {
return;
}
// check startup commands
if (last_tm_min == -1) {
for (ScheduleItem & scheduleItem : *scheduleItems) {
for (ScheduleItem & scheduleItem : *scheduleItems_) {
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min == 0) {
scheduleItem.retry_cnt = command(scheduleItem.cmd.c_str(), scheduleItem.value.c_str()) ? 0xFF : 0;
}
@@ -393,7 +395,7 @@ void WebSchedulerService::loop() {
// check timer every minute, sync to EMS-ESP clock
uint32_t uptime_min = uuid::get_uptime_sec() / 60;
if (last_uptime_min != uptime_min) {
for (ScheduleItem & scheduleItem : *scheduleItems) {
for (ScheduleItem & scheduleItem : *scheduleItems_) {
// retry startup commands not yet executed
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min == 0
&& scheduleItem.retry_cnt < MAX_STARTUP_RETRIES) {
@@ -416,7 +418,7 @@ void WebSchedulerService::loop() {
uint8_t real_dow = 1 << tm->tm_wday; // 1 is Sunday
uint16_t real_min = tm->tm_hour * 60 + tm->tm_min;
for (const ScheduleItem & scheduleItem : *scheduleItems) {
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
if (scheduleItem.active && (real_dow & scheduleItem.flags) && real_min == scheduleItem.elapsed_min) {
command(scheduleItem.cmd.c_str(), scheduleItem.value.c_str());
}
@@ -425,4 +427,29 @@ void WebSchedulerService::loop() {
}
}
// hard coded tests
#if defined(EMSESP_TEST)
void WebSchedulerService::test() {
update(
[&](WebScheduler & webScheduler) {
webScheduler.scheduleItems.clear();
// test 1
auto si = ScheduleItem();
si.active = true;
si.flags = 1;
si.time = "12:00";
si.cmd = "system/fetch";
si.value = "10";
si.name = "test_scheduler";
si.elapsed_min = 0;
si.retry_cnt = 0xFF; // no startup retries
webScheduler.scheduleItems.push_back(si);
return StateUpdateResult::CHANGED; // persist the changes
},
"local");
}
#endif
} // namespace emsesp

View File

@@ -62,6 +62,10 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
ha_registered_ = false;
}
#if defined(EMSESP_TEST)
void test();
#endif
// make all functions public so we can test in the debug and standalone mode
#ifndef EMSESP_STANDALONE
private:
@@ -71,7 +75,7 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
HttpEndpoint<WebScheduler> _httpEndpoint;
FSPersistence<WebScheduler> _fsPersistence;
std::list<ScheduleItem> * scheduleItems; // pointer to the list of schedule events
std::list<ScheduleItem> * scheduleItems_; // pointer to the list of schedule events
bool ha_registered_ = false;
};