This commit is contained in:
proddy
2025-01-04 13:41:39 +01:00
parent 4138598db2
commit eb87651c47
166 changed files with 2099 additions and 10446 deletions

796
src/core/analogsensor.cpp Normal file
View File

@@ -0,0 +1,796 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020 Paul Derbyshire
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "analogsensor.h"
#include "emsesp.h"
namespace emsesp {
uuid::log::Logger AnalogSensor::logger_{F_(analogsensor), uuid::log::Facility::DAEMON};
void AnalogSensor::start() {
reload(true); // fetch the list of sensors from our customization service
if (!analog_enabled_) {
return;
}
analogSetAttenuation(ADC_2_5db); // for all channels 1.5V
LOG_INFO("Starting Analog Sensor service");
// 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);
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf(topic, sizeof(topic), "%s/#", F_(analogsensor));
Mqtt::subscribe(EMSdevice::DeviceType::ANALOGSENSOR, topic, nullptr); // use empty function callback
}
// load settings from the customization file, sorts them and initializes the GPIOs
void AnalogSensor::reload(bool get_nvs) {
EMSESP::webSettingsService.read([&](WebSettings & settings) { analog_enabled_ = settings.analog_enabled; });
#if defined(EMSESP_STANDALONE)
analog_enabled_ = true; // for local offline testing
#endif
for (auto sensor : sensors_) {
remove_ha_topic(sensor.type(), sensor.gpio());
sensor.ha_registered = false;
}
if (!analog_enabled_) {
sensors_.clear();
return;
}
// load the list of analog sensors from the customization service
// and store them locally and then activate them
EMSESP::webCustomizationService.read([&](WebCustomization & settings) {
auto it = sensors_.begin();
for (auto & sensor_ : sensors_) {
// update existing sensors
bool found = false;
for (const auto & sensor : settings.analogCustomizations) { // search customlist
if (sensor_.gpio() == sensor.gpio) {
// for output sensors set value to new start-value
if ((sensor.type == AnalogType::COUNTER || sensor.type >= AnalogType::DIGITAL_OUT)
&& (sensor_.type() != sensor.type || sensor_.offset() != sensor.offset || sensor_.factor() != sensor.factor)) {
sensor_.set_value(sensor.offset);
}
sensor_.set_name(sensor.name);
sensor_.set_type(sensor.type);
sensor_.set_offset(sensor.offset);
sensor_.set_factor(sensor.factor);
sensor_.set_uom(sensor.uom);
sensor_.ha_registered = false;
found = true;
}
}
if (!found) {
sensors_.erase(it);
}
it++;
}
// add new sensors from list
for (const auto & sensor : settings.analogCustomizations) {
bool found = false;
for (const auto & sensor_ : sensors_) {
if (sensor_.gpio() == sensor.gpio) {
found = true;
}
}
if (!found) {
sensors_.emplace_back(sensor.gpio, sensor.name, sensor.offset, sensor.factor, sensor.uom, sensor.type);
sensors_.back().ha_registered = false; // this will trigger recreate of the HA config
if (sensor.type == AnalogType::COUNTER || sensor.type >= AnalogType::DIGITAL_OUT) {
sensors_.back().set_value(sensor.offset);
} else {
sensors_.back().set_value(0); // reset value only for new sensors
}
}
if (sensor.type == AnalogType::COUNTER || sensor.type >= AnalogType::DIGITAL_OUT) {
Command::add(
EMSdevice::DeviceType::ANALOGSENSOR,
sensor.name.c_str(),
[&](const char * value, const int8_t id) { return command_setvalue(value, sensor.gpio); },
sensor.type == AnalogType::COUNTER ? FL_(counter)
: sensor.type == AnalogType::DIGITAL_OUT ? FL_(digital_out)
: FL_(pwm),
CommandFlag::ADMIN_ONLY);
}
}
return true;
});
// sort the list based on GPIO (id)
// std::sort(sensors_.begin(), sensors_.end(), [](const Sensor & a, const Sensor & b) { return a.id() < b.id(); });
// activate each sensor
for (auto & sensor : sensors_) {
sensor.ha_registered = false; // force HA configs to be re-created
// first check if the GPIO is valid. If not, force set it to disabled
if (!System::is_valid_gpio(sensor.gpio())) {
LOG_WARNING("Bad GPIO %d for Sensor %s. Disabling.", sensor.gpio(), sensor.name().c_str());
sensor.set_type(AnalogType::NOTUSED); // set disabled
continue; // skip this loop pass
}
if ((sensor.gpio() == 25 || sensor.gpio() == 26)
&& (sensor.type() == AnalogType::COUNTER || sensor.type() == AnalogType::DIGITAL_IN || sensor.type() == AnalogType::RATE
|| sensor.type() == AnalogType::TIMER)) {
// pullup is mapped to DAC, so set to 3.3V
#if CONFIG_IDF_TARGET_ESP32
if (sensor.gpio() == 25 || sensor.gpio() == 26) {
dacWrite(sensor.gpio(), 255);
}
#elif CONFIG_IDF_TARGET_ESP32S2
if (sensor.gpio() == 17 || sensor.gpio() == 18) {
dacWrite(sensor.gpio(), 255);
}
#endif
}
if (sensor.type() == AnalogType::ADC) {
LOG_DEBUG("ADC Sensor on GPIO %02d", sensor.gpio());
// analogSetPinAttenuation does not work with analogReadMilliVolts
sensor.analog_ = 0; // initialize
sensor.last_reading_ = 0;
} else if (sensor.type() == AnalogType::COUNTER) {
LOG_DEBUG("I/O Counter on GPIO %02d", sensor.gpio());
pinMode(sensor.gpio(), INPUT_PULLUP);
sensor.polltime_ = 0;
sensor.poll_ = digitalRead(sensor.gpio());
if (double_t val = EMSESP::nvs_.getDouble(sensor.name().c_str(), 0)) {
sensor.set_value(val);
}
publish_sensor(sensor);
} else if (sensor.type() == AnalogType::TIMER || sensor.type() == AnalogType::RATE) {
LOG_DEBUG("Timer/Rate on GPIO %02d", sensor.gpio());
pinMode(sensor.gpio(), INPUT_PULLUP);
sensor.polltime_ = uuid::get_uptime();
sensor.last_polltime_ = uuid::get_uptime();
sensor.poll_ = digitalRead(sensor.gpio());
sensor.set_offset(0);
sensor.set_value(0);
publish_sensor(sensor);
} else if (sensor.type() == AnalogType::DIGITAL_IN) {
LOG_DEBUG("Digital Read on GPIO %02d", sensor.gpio());
pinMode(sensor.gpio(), INPUT_PULLUP);
sensor.set_value(digitalRead(sensor.gpio())); // initial value
sensor.set_uom(0); // no uom, just for safe measures
sensor.polltime_ = 0;
sensor.poll_ = digitalRead(sensor.gpio());
publish_sensor(sensor);
} else if (sensor.type() == AnalogType::DIGITAL_OUT) {
LOG_DEBUG("Digital Write on GPIO %02d", sensor.gpio());
pinMode(sensor.gpio(), OUTPUT);
#if CONFIG_IDF_TARGET_ESP32
if (sensor.gpio() == 25 || sensor.gpio() == 26) {
if (sensor.offset() > 255) {
sensor.set_offset(255);
} else if (sensor.offset() < 0) {
sensor.set_offset(0);
}
dacWrite(sensor.gpio(), sensor.offset());
sensor.set_value(sensor.offset());
} else
#elif CONFIG_IDF_TARGET_ESP32S2
if (sensor.gpio() == 17 || sensor.gpio() == 18) {
if (sensor.offset() > 255) {
sensor.set_offset(255);
} else if (sensor.offset() < 0) {
sensor.set_offset(0);
}
dacWrite(sensor.gpio(), sensor.offset());
sensor.set_value(sensor.offset());
} else
#endif
{
if (sensor.uom() == 0) { // set state from NVS
if (!get_nvs || EMSESP::nvs_.getChar(sensor.name().c_str(), -1) == -1) {
EMSESP::nvs_.putChar(sensor.name().c_str(), (int8_t)sensor.offset());
} else {
sensor.set_offset(EMSESP::nvs_.getChar(sensor.name().c_str()));
}
}
digitalWrite(sensor.gpio(), (sensor.offset() == 0) ^ (sensor.factor() > 0));
sensor.set_value(sensor.offset());
}
publish_sensor(sensor);
} else if (sensor.type() >= AnalogType::PWM_0 && sensor.type() <= AnalogType::PWM_2) {
LOG_DEBUG("PWM output on GPIO %02d", sensor.gpio());
#if ESP_IDF_VERSION_MAJOR >= 5
ledcAttach(sensor.gpio(), sensor.factor(), 13);
#else
uint8_t channel = sensor.type() - AnalogType::PWM_0;
ledcSetup(channel, sensor.factor(), 13);
ledcAttachPin(sensor.gpio(), channel);
#endif
if (sensor.offset() > 100) {
sensor.set_offset(100);
} else if (sensor.offset() < 0) {
sensor.set_offset(0);
}
#if ESP_IDF_VERSION_MAJOR >= 5
ledcWrite(sensor.gpio(), (uint32_t)(sensor.offset() * 8191 / 100));
#else
ledcWrite(channel, (uint32_t)(sensor.offset() * 8191 / 100));
#endif
sensor.set_value(sensor.offset());
sensor.set_uom(DeviceValueUOM::PERCENT);
publish_sensor(sensor);
}
}
}
// measure input sensors and moving average adc
void AnalogSensor::measure() {
static uint32_t measure_last_ = uuid::get_uptime() - MEASURE_ANALOG_INTERVAL;
// measure interval 500ms for adc sensors
if ((uuid::get_uptime() - measure_last_) >= MEASURE_ANALOG_INTERVAL) {
measure_last_ = uuid::get_uptime();
// go through the list of adc sensors
for (auto & sensor : sensors_) {
if (sensor.type() == AnalogType::ADC) {
uint16_t a = analogReadMilliVolts(sensor.gpio()); // e.g. ADC1_CHANNEL_0_GPIO_NUM
if (!sensor.analog_) { // init first time
sensor.analog_ = a;
sensor.sum_ = a * 512;
} else { // simple moving average filter
sensor.sum_ = (sensor.sum_ * 511) / 512 + a;
sensor.analog_ = sensor.sum_ / 512;
}
// detect change with little hysteresis on raw mV value
if (sensor.last_reading_ + 1 < sensor.analog_ || sensor.last_reading_ > sensor.analog_ + 1) {
sensor.set_value(((int32_t)sensor.analog_ - sensor.offset()) * sensor.factor());
sensor.last_reading_ = sensor.analog_;
sensorreads_++;
changed_ = true;
publish_sensor(sensor);
}
}
}
}
// poll digital io every time with debounce
// go through the list of digital sensors
for (auto & sensor : sensors_) {
if (sensor.type() == AnalogType::DIGITAL_IN || sensor.type() == AnalogType::COUNTER || sensor.type() == AnalogType::TIMER
|| sensor.type() == AnalogType::RATE) {
auto old_value = sensor.value(); // remember current value before reading
auto current_reading = digitalRead(sensor.gpio());
if (sensor.poll_ != current_reading) { // check for pinchange
sensor.polltime_ = uuid::get_uptime(); // remember time of pinchange
sensor.poll_ = current_reading;
}
// debounce and check for real pinchange
if (uuid::get_uptime() - sensor.polltime_ >= 15 && sensor.poll_ != sensor.last_reading_) {
sensor.last_reading_ = sensor.poll_;
if (sensor.type() == AnalogType::DIGITAL_IN) {
sensor.set_value(sensor.poll_);
} else if (!sensor.poll_) { // falling edge
if (sensor.type() == AnalogType::COUNTER) {
sensor.set_value(old_value + sensor.factor());
// EMSESP::nvs_.putDouble(sensor.name().c_str(), sensor.value());
} else if (sensor.type() == AnalogType::RATE) { // default uom: Hz (1/sec) with factor 1
sensor.set_value(sensor.factor() * 1000 / (sensor.polltime_ - sensor.last_polltime_));
} else if (sensor.type() == AnalogType::TIMER) { // default seconds with factor 1
sensor.set_value(sensor.factor() * (sensor.polltime_ - sensor.last_polltime_) / 1000);
}
sensor.last_polltime_ = sensor.polltime_;
}
}
// see if there is a change and increment # reads
if (old_value != sensor.value()) {
sensorreads_++;
changed_ = true;
publish_sensor(sensor);
}
}
}
// store counter-values only every hour to reduce flash wear
static uint8_t lastSaveHour = 0;
time_t now = time(nullptr);
tm * tm_ = localtime(&now);
if (tm_->tm_hour != lastSaveHour) {
lastSaveHour = tm_->tm_hour;
store_counters();
}
}
// store counters to NVS, called every hour, on restart and update
void AnalogSensor::store_counters() {
for (auto & sensor : sensors_) {
if (sensor.type() == AnalogType::COUNTER) {
if (sensor.value() != EMSESP::nvs_.getDouble(sensor.name().c_str())) {
EMSESP::nvs_.putDouble(sensor.name().c_str(), sensor.value());
}
}
}
}
void AnalogSensor::loop() {
if (!analog_enabled_) {
return;
}
measure(); // take the measurements
}
// update analog information name and offset
// a type value of -1 is used to delete the sensor
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) {
for (auto & AnalogCustomization : settings.analogCustomizations) {
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
if (deleted) {
EMSESP::nvs_.remove(AnalogCustomization.name.c_str());
LOG_DEBUG("Removing analog sensor GPIO %02d", gpio);
settings.analogCustomizations.remove(AnalogCustomization);
} else {
// update existing record
if (name != AnalogCustomization.name) {
EMSESP::nvs_.remove(AnalogCustomization.name.c_str());
}
AnalogCustomization.name = name;
AnalogCustomization.offset = offset;
AnalogCustomization.factor = factor;
AnalogCustomization.uom = uom;
AnalogCustomization.type = type;
LOG_DEBUG("Customizing existing analog GPIO %02d", gpio);
}
return StateUpdateResult::CHANGED; // persist the change
}
}
return StateUpdateResult::UNCHANGED;
});
// if the sensor exists and we're using HA, delete the old HA record
if (found_sensor && Mqtt::ha_enabled()) {
remove_ha_topic(type, gpio); // the GPIO
}
// 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) {
auto newSensor = AnalogCustomization();
newSensor.gpio = gpio;
newSensor.name = name;
newSensor.offset = offset;
newSensor.factor = factor;
newSensor.uom = uom;
newSensor.type = type;
settings.analogCustomizations.push_back(newSensor);
LOG_DEBUG("Adding new customization for analog sensor GPIO %02d", gpio);
return StateUpdateResult::CHANGED; // persist the change
});
}
// reloads the sensors in the customizations file into the sensors list
reload();
// return false if it's an invalid GPIO, an error will show in WebUI
// and reported as an error in the log
return System::is_valid_gpio(gpio);
}
// check to see if values have been updated
bool AnalogSensor::updated_values() {
if (changed_) {
changed_ = false;
return true;
}
return false;
}
// publish a single sensor to MQTT
void AnalogSensor::publish_sensor(const Sensor & sensor) const {
if (Mqtt::publish_single()) {
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
if (Mqtt::publish_single2cmd()) {
snprintf(topic, sizeof(topic), "%s/%s", F_(analogsensor), sensor.name().c_str());
} else {
snprintf(topic, sizeof(topic), "%s%s/%s", F_(analogsensor), "_data", sensor.name().c_str());
}
char payload[10];
Mqtt::queue_publish(topic, Helpers::render_value(payload, sensor.value(), 2)); // always publish as doubles
}
char cmd[COMMAND_MAX_LENGTH];
snprintf(cmd, sizeof(cmd), "%s/%s", F_(analogsensor), sensor.name().c_str());
EMSESP::webSchedulerService.onChange(cmd);
}
// send empty config topic to remove the entry from HA
void AnalogSensor::remove_ha_topic(const int8_t type, const uint8_t gpio) const {
if (!Mqtt::ha_enabled()) {
return;
}
LOG_DEBUG("Removing HA config for analog sensor GPIO %02d", gpio);
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
#if CONFIG_IDF_TARGET_ESP32
if (type == AnalogType::DIGITAL_OUT && gpio != 25 && gpio != 26) {
#else
if (type == AnalogType::DIGITAL_OUT) {
#endif
snprintf(topic, sizeof(topic), "switch/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), gpio);
} else if (type == AnalogType::DIGITAL_OUT) { // DAC
snprintf(topic, sizeof(topic), "number/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), gpio);
} else if (type >= AnalogType::PWM_0) {
snprintf(topic, sizeof(topic), "number/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), gpio);
} else if (type == AnalogType::DIGITAL_IN) {
snprintf(topic, sizeof(topic), "binary_sensor/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), gpio);
} else {
snprintf(topic, sizeof(topic), "sensor/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), gpio);
}
Mqtt::queue_remove_topic(topic);
}
// send all sensor values as a JSON package to MQTT
void AnalogSensor::publish_values(const bool force) {
uint8_t num_sensors = sensors_.size();
if (num_sensors == 0) {
return;
}
if (force && Mqtt::publish_single()) {
for (const auto & sensor : sensors_) {
publish_sensor(sensor);
}
}
JsonDocument doc;
for (auto & sensor : sensors_) {
if (sensor.type() != AnalogType::NOTUSED) {
if (Mqtt::is_nested()) {
char s[10];
JsonObject dataSensor = doc[Helpers::smallitoa(s, sensor.gpio())].to<JsonObject>();
dataSensor["name"] = sensor.name();
switch (sensor.type()) {
case AnalogType::COUNTER:
case AnalogType::TIMER:
case AnalogType::RATE:
case AnalogType::ADC:
case AnalogType::PWM_0:
case AnalogType::PWM_1:
case AnalogType::PWM_2:
dataSensor["value"] = serialized(Helpers::render_value(s, sensor.value(), 2)); // double
break;
case AnalogType::DIGITAL_IN:
case AnalogType::DIGITAL_OUT:
if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) {
dataSensor["value"] = sensor.value() != 0;
} else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) {
dataSensor["value"] = sensor.value() != 0 ? 1 : 0;
} else {
char result[12];
dataSensor["value"] = Helpers::render_boolean(result, sensor.value() != 0);
}
break;
default:
break;
}
} else if (sensor.type() == AnalogType::DIGITAL_IN || sensor.type() == AnalogType::DIGITAL_OUT) {
if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) {
doc[sensor.name()] = sensor.value() != 0;
} else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) {
doc[sensor.name()] = sensor.value() != 0 ? 1 : 0;
} else {
char result[12];
doc[sensor.name()] = Helpers::render_boolean(result, sensor.value() != 0);
}
} else {
char s[10];
doc[sensor.name()] = serialized(Helpers::render_value(s, sensor.value(), 2));
}
// create HA config if hasn't already been done
if (Mqtt::ha_enabled() && (!sensor.ha_registered || force)) {
LOG_DEBUG("Recreating HA config for analog sensor GPIO %02d", sensor.gpio());
JsonDocument config;
char stat_t[50];
snprintf(stat_t, sizeof(stat_t), "%s/%s_data", Mqtt::base().c_str(), F_(analogsensor)); // use base path
config["stat_t"] = stat_t;
char val_obj[50];
char val_cond[95];
if (Mqtt::is_nested()) {
snprintf(val_obj, sizeof(val_obj), "value_json['%02d']['value']", sensor.gpio());
snprintf(val_cond, sizeof(val_cond), "value_json['%02d'] is defined and %s is defined", sensor.gpio(), val_obj);
} else {
snprintf(val_obj, sizeof(val_obj), "value_json['%s']", sensor.name().c_str());
snprintf(val_cond, sizeof(val_cond), "%s is defined", val_obj);
}
char sample_val[12] = "0";
if (sensor.type() == AnalogType::DIGITAL_IN || sensor.type() == AnalogType::DIGITAL_OUT) {
Helpers::render_boolean(sample_val, false);
}
config["val_tpl"] = (std::string) "{{" + val_obj + " if " + val_cond + " else " + sample_val + "}}";
char uniq_s[70];
if (Mqtt::entity_format() == Mqtt::entityFormat::MULTI_SHORT) {
snprintf(uniq_s, sizeof(uniq_s), "%s_%s_%02d", Mqtt::basename().c_str(), F_(analogsensor), sensor.gpio());
} else {
snprintf(uniq_s, sizeof(uniq_s), "%s_%02d", F_(analogsensor), sensor.gpio());
}
config["obj_id"] = uniq_s;
config["uniq_id"] = uniq_s; // same as object_id
char name[50];
snprintf(name, sizeof(name), "%s", sensor.name().c_str());
config["name"] = name;
if (sensor.uom() != DeviceValueUOM::NONE) {
config["unit_of_meas"] = EMSdevice::uom_to_string(sensor.uom());
}
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
// Set commands for some analog types
char command_topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
#if CONFIG_IDF_TARGET_ESP32
if (sensor.type() == AnalogType::DIGITAL_OUT && sensor.gpio() != 25 && sensor.gpio() != 26) {
#else
if (sensor.type() == AnalogType::DIGITAL_OUT) {
#endif
snprintf(topic, sizeof(topic), "switch/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), sensor.gpio());
snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name().c_str());
config["cmd_t"] = command_topic;
Mqtt::add_ha_bool(config);
} else if (sensor.type() == AnalogType::DIGITAL_OUT) { // DAC
snprintf(topic, sizeof(topic), "number/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), sensor.gpio());
snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name().c_str());
config["cmd_t"] = command_topic;
config["min"] = 0;
config["max"] = 255;
config["mode"] = "box"; // auto, slider or box
config["step"] = 1;
} else if (sensor.type() >= AnalogType::PWM_0 && sensor.type() <= AnalogType::PWM_2) {
snprintf(topic, sizeof(topic), "number/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), sensor.gpio());
snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name().c_str());
config["cmd_t"] = command_topic;
config["min"] = 0;
config["max"] = 100;
config["mode"] = "box"; // auto, slider or box
config["step"] = 0.1;
} else if (sensor.type() == AnalogType::COUNTER) {
snprintf(topic, sizeof(topic), "sensor/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), sensor.gpio());
snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name().c_str());
config["cmd_t"] = command_topic;
config["stat_cla"] = "total_increasing";
// config["mode"] = "box"; // auto, slider or box
// config["step"] = sensor.factor();
} else if (sensor.type() == AnalogType::DIGITAL_IN) {
snprintf(topic, sizeof(topic), "binary_sensor/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), sensor.gpio());
Mqtt::add_ha_bool(config);
} else {
snprintf(topic, sizeof(topic), "sensor/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), sensor.gpio());
config["stat_cla"] = "measurement";
}
// see if we need to create the [devs] discovery section, as this needs only to be done once for all sensors
bool is_ha_device_created = false;
for (auto const & sensor : sensors_) {
if (sensor.ha_registered) {
is_ha_device_created = true;
break;
}
}
Mqtt::add_ha_sections_to_doc("analog", stat_t, config, !is_ha_device_created, val_cond);
sensor.ha_registered = Mqtt::queue_ha(topic, config.as<JsonObject>());
}
}
}
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf(topic, sizeof(topic), "%s_data", F_(analogsensor));
Mqtt::queue_publish(topic, doc.as<JsonObject>());
}
// 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
}
if (!strcmp(cmd, F_(info)) || !strcmp(cmd, F_(values))) {
for (const auto & sensor : sensors_) {
output[sensor.name()] = sensor.value();
}
return true;
}
if (!strcmp(cmd, F_(entities))) {
for (const auto & sensor : sensors_) {
get_value_json(output[sensor.name()].to<JsonObject>(), sensor);
}
return true;
}
// this is for a specific sensor
const char * attribute_s = Command::get_attribute(cmd);
for (const auto & sensor : sensors_) {
// match custom name or sensor GPIO
if (cmd == Helpers::toLower(sensor.name()) || Helpers::atoint(cmd) == sensor.gpio()) {
get_value_json(output, sensor);
return Command::set_attribute(output, cmd, attribute_s);
}
}
return false; // not found
}
void AnalogSensor::get_value_json(JsonObject output, const Sensor & sensor) {
output["name"] = sensor.name();
output["fullname"] = sensor.name();
output["gpio"] = sensor.gpio();
output["type"] = F_(number);
output["analog"] = FL_(list_sensortype)[sensor.type()];
output["value"] = sensor.value();
output["readable"] = true;
output["writeable"] = sensor.type() == AnalogType::COUNTER || (sensor.type() >= AnalogType::DIGITAL_OUT && sensor.type() <= AnalogType::PWM_2);
output["visible"] = true;
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;
char state[][2] = {"?", "0", "1"};
output["start"] = state[sensor.uom()];
}
}
// this creates the sensor, initializing everything
AnalogSensor::Sensor::Sensor(const uint8_t gpio, const std::string & name, const double offset, const double factor, const uint8_t uom, const int8_t type)
: gpio_(gpio)
, name_(name)
, offset_(offset)
, factor_(factor)
, uom_(uom)
, type_(type) {
value_ = 0; // init value to 0 always
}
// 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;
if (!Helpers::value2float(value, val)) {
bool b;
if (!Helpers::value2bool(value, b)) {
return false;
}
val = b ? 1 : 0;
}
for (auto & sensor : sensors_) {
if (sensor.gpio() == gpio) {
double oldoffset = sensor.offset();
if (sensor.type() == AnalogType::COUNTER) {
if (val < 0 || value[0] == '+') { // sign corrects values
sensor.set_offset(sensor.value() + val);
sensor.set_value(sensor.value() + val);
} else { // positive values are set
sensor.set_offset(val);
sensor.set_value(val);
}
if (oldoffset != sensor.offset() && sensor.offset() != EMSESP::nvs_.getDouble(sensor.name().c_str())) {
EMSESP::nvs_.putDouble(sensor.name().c_str(), sensor.value());
}
} else if (sensor.type() == AnalogType::ADC) {
sensor.set_offset(val);
} else if (sensor.type() == AnalogType::DIGITAL_OUT) {
uint8_t v = val;
#if CONFIG_IDF_TARGET_ESP32
if ((sensor.gpio() == 25 || sensor.gpio() == 26) && v <= 255) {
sensor.set_offset(v);
sensor.set_value(v);
pinMode(sensor.gpio(), OUTPUT);
dacWrite(sensor.gpio(), sensor.offset());
} else
#elif CONFIG_IDF_TARGET_ESP32S2
if ((sensor.gpio() == 17 || sensor.gpio() == 18) && v <= 255) {
sensor.set_offset(v);
sensor.set_value(v);
pinMode(sensor.gpio(), OUTPUT);
dacWrite(sensor.gpio(), sensor.offset());
} else
#endif
if (v == 0 || v == 1) {
sensor.set_offset(v);
sensor.set_value(v);
pinMode(sensor.gpio(), OUTPUT);
digitalWrite(sensor.gpio(), (sensor.offset() == 0) ^ (sensor.factor() > 0));
if (sensor.uom() == 0 && EMSESP::nvs_.getChar(sensor.name().c_str()) != (int8_t)sensor.offset()) {
EMSESP::nvs_.putChar(sensor.name().c_str(), (int8_t)sensor.offset());
}
}
} else if (sensor.type() >= AnalogType::PWM_0 && sensor.type() <= AnalogType::PWM_2) {
if (val > 100) {
val = 100;
} else if (val < 0) {
val = 0;
}
sensor.set_offset(val);
sensor.set_value(val);
#if ESP_IDF_VERSION_MAJOR >= 5
ledcWrite(sensor.gpio(), (uint32_t)(sensor.offset() * 8191 / 100));
#else
uint8_t channel = sensor.type() - AnalogType::PWM_0;
ledcWrite(channel, (uint32_t)(val * 8191 / 100));
#endif
} else {
return false;
}
if (oldoffset != sensor.offset()) {
publish_sensor(sensor);
changed_ = true;
}
return true;
}
}
return false;
}
} // namespace emsesp

190
src/core/analogsensor.h Normal file
View File

@@ -0,0 +1,190 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_ANALOGSENSOR_H
#define EMSESP_ANALOGSENSOR_H
#include "helpers.h"
#include "mqtt.h"
#include "console.h"
#include <uuid/log.h>
namespace emsesp {
// names, same order as AnalogType
MAKE_ENUM_FIXED(AnalogTypeName, "disabled", "dig_in", "counter", "adc", "timer", "rate", "dig_out", "pwm0", "pwm1", "pwm2")
class AnalogSensor {
public:
class Sensor {
public:
Sensor(const uint8_t gpio, const std::string & name, const double offset, const double factor, const uint8_t uom, const int8_t type);
~Sensor() = default;
void set_offset(const double offset) {
offset_ = offset;
}
std::string name() const {
return name_;
}
void set_name(const std::string & name) {
name_ = name;
}
uint8_t gpio() const {
return gpio_;
}
double value() const {
return value_;
}
void set_value(const double value) {
value_ = value;
}
double factor() const {
return factor_;
}
void set_factor(const double factor) {
factor_ = factor;
}
double offset() const {
return offset_;
}
void set_uom(const uint8_t uom) {
uom_ = uom;
}
uint8_t uom() const {
return uom_;
}
int8_t type() const {
return type_;
}
void set_type(const int8_t type) {
type_ = type;
}
bool ha_registered = false;
uint16_t analog_ = 0; // ADC - average value
uint32_t sum_ = 0; // ADC - rolling sum
uint16_t last_reading_ = 0; // IO COUNTER & ADC - last reading
uint16_t count_ = 0; // counter raw counts
uint32_t polltime_ = 0; // digital IO & COUNTER debounce time
int poll_ = 0;
uint32_t last_polltime_ = 0; // for timer
private:
uint8_t gpio_;
std::string name_;
double offset_;
double factor_;
uint8_t uom_;
double value_; // double because of the factor is a double
int8_t type_; // one of the AnalogType enum
};
AnalogSensor() = default;
~AnalogSensor() = default;
enum AnalogType : int8_t {
NOTUSED, // 0 - disabled
DIGITAL_IN,
COUNTER,
ADC,
TIMER,
RATE,
DIGITAL_OUT,
PWM_0,
PWM_1,
PWM_2
};
void start();
void loop();
void publish_sensor(const Sensor & sensor) const;
void publish_values(const bool force);
void reload(bool get_nvs = false);
bool updated_values();
// return back reference to the sensor list, used by other classes
std::vector<Sensor> sensors() const {
return sensors_;
}
uint32_t reads() const {
return sensorreads_;
}
uint32_t fails() const {
return sensorfails_;
}
bool analog_enabled() const {
return (analog_enabled_);
}
bool have_sensors() const {
return (!sensors_.empty());
}
size_t count_entities(bool include_disabled = true) const {
if (!include_disabled) {
// count number of items in sensors_ where type is not set to disabled
return std::count_if(sensors_.begin(), sensors_.end(), [](const Sensor & sensor) { return sensor.type() != AnalogSensor::AnalogType::NOTUSED; });
}
return sensors_.size();
}
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();
private:
static constexpr uint8_t MAX_SENSORS = 20;
static constexpr uint32_t MEASURE_ANALOG_INTERVAL = 500;
static uuid::log::Logger logger_;
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();
void addSensorJson(JsonObject output, const Sensor & sensor);
void get_value_json(JsonObject output, const Sensor & sensor);
std::vector<Sensor> sensors_; // our list of sensors
bool analog_enabled_;
bool changed_ = false;
uint32_t sensorfails_ = 0;
uint32_t sensorreads_ = 0;
};
} // namespace emsesp
#endif

840
src/core/command.cpp Normal file
View File

@@ -0,0 +1,840 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "command.h"
#include "emsdevice.h"
#include "emsesp.h"
namespace emsesp {
uuid::log::Logger Command::logger_{F_(command), uuid::log::Facility::DAEMON};
std::vector<Command::CmdFunction> Command::cmdfunctions_;
// takes a URI path and a json body, parses the data and calls the command
// the path is leading so if duplicate keys are in the input JSON it will be ignored
// the entry point will be either via the Web API (api/) or MQTT (<base>/)
// returns a return code and json output
uint8_t Command::process(const char * path, const bool is_admin, const JsonObject input, JsonObject output) {
SUrlParser p; // parse URL for the path names
p.parse(path);
// check first if it's from API or MQTT, if so strip the "api/" or "<base>/" from the path
if (p.paths().size() && ((p.paths().front() == "api") || (p.paths().front() == Mqtt::base()))) {
p.paths().erase(p.paths().begin());
} else {
return json_message(CommandRet::ERROR, "invalid path", output, path); // error
}
// re-calculate new path
// if there is only a path (URL) and no body then error!
size_t num_paths = p.paths().size();
if (!num_paths && !input.size()) {
return json_message(CommandRet::ERROR, "missing command in path", output);
}
std::string cmd_s;
int8_t id_n = -1; // default hc
// check for a device as first item in the path
const char * device_s = nullptr;
if (!num_paths) {
// we must look for the device in the JSON body
if (input["device"].is<const char *>()) {
device_s = input["device"];
}
} else {
// extract it from the path
device_s = p.paths().front().c_str(); // get the device type name (boiler, thermostat, system etc)
}
// validate the device, make sure it exists
uint8_t device_type = EMSdevice::device_name_2_device_type(device_s);
if (!device_has_commands(device_type)) {
char err[100];
snprintf(err, sizeof(err), "unknown device %s", device_s);
LOG_WARNING("Command failed: %s", err);
output["message"] = err;
return CommandRet::NOT_FOUND;
}
// the next value on the path should be the command or entity name
const char * command_p = nullptr;
if (num_paths == 2) {
command_p = p.paths()[1].c_str();
} else if (num_paths == 3) {
// concatenate the path into one string as it could be in the format 'hc/XXX'
char command[COMMAND_MAX_LENGTH];
snprintf(command, sizeof(command), "%s/%s", p.paths()[1].c_str(), p.paths()[2].c_str());
command_p = command;
} else if (num_paths > 3) {
// concatenate the path into one string as it could be in the format 'hc/XXX/attribute'
char command[COMMAND_MAX_LENGTH];
snprintf(command, sizeof(command), "%s/%s/%s", p.paths()[1].c_str(), p.paths()[2].c_str(), p.paths()[3].c_str());
command_p = command;
} else {
// take it from the JSON
if (input["entity"].is<const char *>()) {
command_p = input["entity"];
} else if (input["cmd"].is<const char *>()) {
command_p = input["cmd"];
}
}
// 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
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 'value' for all devices
if (num_paths < (id_n > 0 ? 4 : 3)) {
command_p = F_(values);
} else {
return json_message(CommandRet::NOT_FOUND, "missing or bad command", output);
}
}
// if we don't have an id/hc/dhw try and get it from the JSON input. It must be a number.
// it's allowed to have no id, and then keep the default to -1
if (id_n == -1) {
if (input["hc"].is<int>()) {
id_n = input["hc"];
} else if (input["dhw"].is<int>()) {
id_n = input["dhw"];
id_n += DeviceValueTAG::TAG_DHW1 - DeviceValueTAG::TAG_HC1; // dhw1 has id 9
} else if (input["id"].is<int>()) {
id_n = input["id"];
} else if (input["ahs"].is<int>()) {
id_n = input["ahs"];
id_n += DeviceValueTAG::TAG_AHS1 - DeviceValueTAG::TAG_HC1; // ahs1 has id 19
} else if (input["hs"].is<int>()) {
id_n = input["hs"];
id_n += DeviceValueTAG::TAG_HS1 - DeviceValueTAG::TAG_HC1; // hs1 has id 20
}
}
// the value must always come from the input JSON. It's allowed to be empty.
JsonVariant data;
if (input["data"].is<JsonVariantConst>()) {
data = input["data"];
} else if (input["value"].is<JsonVariantConst>()) {
data = input["value"];
}
// check if data is entity like device/hc/name/value
if (data.is<const char *>()) {
const char * d = data.as<const char *>();
if (strlen(d)) {
char * device_end = (char *)strchr(d, '/');
if (device_end != nullptr) {
char device_s[20] = {'\0'};
const char * device_p = device_s;
const char * data_p = nullptr;
strlcpy(device_s, d, device_end - d + 1);
data_p = device_end + 1;
int8_t id_d = -1;
uint8_t device_type1 = EMSdevice::device_name_2_device_type(device_p);
if (device_type1 >= EMSdevice::DeviceType::BOILER) {
data_p = parse_command_string(data_p, id_d);
}
if (data_p == nullptr) {
return CommandRet::INVALID;
}
char data_s[COMMAND_MAX_LENGTH];
strlcpy(data_s, Helpers::toLower(data_p).c_str(), 30);
if (strstr(data_s, "/value") == nullptr) {
strlcat(data_s, "/value", sizeof(data_s) - 6);
}
if (Command::call(device_type1, data_s, "", true, id_d, output) != CommandRet::OK) {
return CommandRet::INVALID;
}
if (output["api_data"].is<std::string>()) {
std::string api_data = output["api_data"];
output.clear();
return Command::call(device_type, command_p, api_data.c_str(), is_admin, id_n, output);
}
return CommandRet::INVALID;
}
}
}
// call the command based on the type
uint8_t return_code = CommandRet::ERROR;
if (data.is<const char *>()) {
return_code = Command::call(device_type, command_p, data.as<const char *>(), is_admin, id_n, output);
} else if (data.is<int>()) {
char data_str[10];
return_code = Command::call(device_type, command_p, Helpers::itoa(data.as<int32_t>(), data_str), is_admin, id_n, output);
} else if (data.is<float>()) {
char data_str[10];
return_code = Command::call(device_type, command_p, Helpers::render_value(data_str, data.as<float>(), 2), is_admin, id_n, output);
} else if (data.is<bool>()) {
return_code = Command::call(device_type, command_p, data.as<bool>() ? "1" : "0", is_admin, id_n, output);
} else if (data.isNull()) {
return_code = Command::call(device_type, command_p, "", is_admin, id_n, output); // empty, will do a query instead
} else {
return json_message(CommandRet::ERROR, "cannot parse command", output); // can't process
}
return return_code;
}
const char * Command::return_code_string(const uint8_t return_code) {
switch (return_code) {
case CommandRet::ERROR:
return "Error";
case CommandRet::OK:
return "OK";
case CommandRet::NOT_FOUND:
return "Not Found";
case CommandRet::NOT_ALLOWED:
return "Not Authorized";
case CommandRet::FAIL:
return "Failed";
case CommandRet::INVALID:
return "Invalid";
default:
break;
}
char s[4];
return Helpers::smallitoa(s, return_code);
}
// takes a string like "hc1/seltemp" or "seltemp" or "dhw2.seltemp" and tries to get the id and cmd
// returns start position of the command string
const char * Command::parse_command_string(const char * command, int8_t & id) {
if (command == nullptr) {
return nullptr;
}
// remember unchanged command
const char * cmd_org = command;
int8_t id_org = id;
// convert cmd to lowercase and compare
char * lowerCmd = strdup(command);
for (char * p = lowerCmd; *p; p++) {
*p = tolower(*p);
}
// check prefix and valid number range, also check 'id'
if (!strncmp(lowerCmd, "hc", 2) && command[2] >= '1' && command[2] <= '8') {
id = command[2] - '0';
command += 3;
} else if (!strncmp(lowerCmd, "dhw", 3) && command[3] == '1' && command[4] == '0') {
id = DeviceValueTAG::TAG_DHW10; //18;
command += 5;
} else if (!strncmp(lowerCmd, "dhw", 3) && command[3] >= '1' && command[3] <= '9') {
id = command[3] - '1' + DeviceValueTAG::TAG_DHW1; //9;
command += 4;
} else if (!strncmp(lowerCmd, "id", 2) && command[2] == '1' && command[3] >= '0' && command[3] <= '9') {
id = command[3] - '0' + 10;
command += 4;
} else if (!strncmp(lowerCmd, "id", 2) && command[2] >= '1' && command[2] <= '9') {
id = command[2] - '0';
command += 3;
} else if (!strncmp(lowerCmd, "ahs", 3) && command[3] >= '1' && command[3] <= '1') { // only ahs1 for now
id = command[3] - '1' + DeviceValueTAG::TAG_AHS1; // 19;
command += 4;
} else if (!strncmp(lowerCmd, "hs", 2) && command[2] == '1' && command[3] >= '0' && command[3] <= '6') {
id = command[3] - '0' + DeviceValueTAG::TAG_HS10; //29;
command += 4;
} else if (!strncmp(lowerCmd, "hs", 2) && command[2] >= '1' && command[2] <= '9') {
id = command[2] - '1' + DeviceValueTAG::TAG_HS1; //20;
command += 3;
} else if (!strncmp(lowerCmd, "dhw", 3)) { // no number
id = DeviceValueTAG::TAG_DHW1;
command += 3;
}
free(lowerCmd);
// return original if no seperator
if (command[0] != '/' && command[0] != '.') {
id = id_org;
return cmd_org;
}
command++;
// return null for empty command
if (command[0] == '\0') {
return nullptr;
}
return command;
}
// check if command contains an attribute
const char * Command::get_attribute(const char * cmd) {
char * breakp = (char *)strchr(cmd, '/');
if (breakp) {
*breakp = '\0';
return breakp + 1;
}
return nullptr;
}
bool Command::set_attribute(JsonObject output, const char * cmd, const char * attribute) {
if (attribute == nullptr) {
return true;
}
if (output[attribute].is<JsonVariantConst>()) {
std::string data = output[attribute].as<std::string>();
output.clear();
output["api_data"] = data; // always as a string
return true;
}
return EMSESP::return_not_found(output, attribute, cmd); // not found
}
// calls a command directly
uint8_t Command::call(const uint8_t device_type, const char * cmd, const char * value, const int8_t id) {
// create a temporary buffer
JsonDocument output_doc;
JsonObject output = output_doc.to<JsonObject>();
// authenticated is always true and ID is the default value
return call(device_type, cmd, value, true, id, output);
}
// calls a command. Takes a json object for output.
// id may be used to represent a heating circuit for example
// returns 0 if the command errored, 1 (TRUE) if ok, 2 if not found, 3 if error or 4 if not allowed
uint8_t Command::call(const uint8_t device_type, const char * command, const char * value, const bool is_admin, const int8_t id, JsonObject output) {
if (command == nullptr) {
return CommandRet::NOT_FOUND;
}
char cmd[COMMAND_MAX_LENGTH];
strlcpy(cmd, Helpers::toLower(command).c_str(), sizeof(cmd));
auto dname = EMSdevice::device_type_2_device_name(device_type); // device name, not translated
// check first if there is only a command being called without a value
// it could be an endpoint like a device's entity or attribute e.g. api/boiler/nrgheat or /api/boiler/nrgheat/value
// or a special command like 'info', 'values', 'commands', 'entities' etc
bool single_command = (!value || !strlen(value));
if (single_command) {
if (!strcmp(cmd, F_(commands))) {
return Command::list(device_type, output);
}
if (EMSESP::get_device_value_info(output, cmd, id, device_type)) { // entity = cmd
LOG_DEBUG("Fetched device entity/attributes for %s/%s", dname, cmd);
return CommandRet::OK;
}
} else if (device_type == EMSdevice::DeviceType::SYSTEM && strchr(cmd, '/')) {
// check service commands, if not found continue with commandsfunctions
if (EMSESP::system_.command_service(cmd, value)) {
return CommandRet::OK;
}
}
uint8_t device_id = EMSESP::device_id_from_cmd(device_type, cmd, id);
// determine flags based on id (which is the tag)
uint8_t flag = CommandFlag::CMD_FLAG_DEFAULT;
if (id >= DeviceValueTAG::TAG_HC1 && id <= DeviceValueTAG::TAG_HC8) {
flag = CommandFlag::CMD_FLAG_HC;
} else if (id >= DeviceValueTAG::TAG_DHW1 && id <= DeviceValueTAG::TAG_DHW10) {
flag = CommandFlag::CMD_FLAG_DHW;
} else if (id >= DeviceValueTAG::TAG_HS1 && id <= DeviceValueTAG::TAG_HS16) {
flag = CommandFlag::CMD_FLAG_HS;
} else if (id >= DeviceValueTAG::TAG_AHS1 && id <= DeviceValueTAG::TAG_AHS1) {
flag = CommandFlag::CMD_FLAG_AHS;
}
// see if there is a command registered and it's valid
auto cf = find_command(device_type, device_id, cmd, flag);
if (!cf) {
// if we don't already have a message set, set it to invalid command
if (output["message"]) {
LOG_WARNING("Command failed: %s", output["message"].as<const char *>());
} else {
std::string err = "no " + std::string(cmd) + " in " + dname;
output["message"] = err;
LOG_WARNING("Command failed: %s", err.c_str());
}
return CommandRet::ERROR;
}
// before calling the command, check permissions and abort if not authorized
if (cf->has_flags(CommandFlag::ADMIN_ONLY) && !is_admin) {
LOG_WARNING("Command failed: authentication failed");
output["message"] = "authentication failed";
return CommandRet::NOT_ALLOWED; // command not allowed
}
// call the function based on command function type
// commands return true or false only (bool)
uint8_t return_code = CommandRet::OK;
if (cf->cmdfunction_json_) {
// handle commands that report back a JSON body
return_code = ((cf->cmdfunction_json_)(value, id, output)) ? CommandRet::OK : CommandRet::ERROR;
} else if (cf->cmdfunction_) {
// if it's a read only command and we're trying to set a value, return an error
if (!single_command && EMSESP::cmd_is_readonly(device_type, device_id, cmd, id)) {
return_code = CommandRet::INVALID; // error on readonly or invalid hc
} else {
// call the command...
return_code = ((cf->cmdfunction_)(value, id)) ? CommandRet::OK : CommandRet::ERROR;
}
}
// report back. If not OK show output from error, otherwise return the error code
if (return_code != CommandRet::OK) {
char error[100];
if (single_command) {
snprintf(error, sizeof(error), "Command %s failed (%s)", cmd, return_code_string(return_code));
} else {
snprintf(error, sizeof(error), "Command %s: %s failed (%s)", cmd, value, return_code_string(return_code));
}
output.clear();
output["message"] = error;
LOG_WARNING(error);
} else {
// build up the log string for reporting back
// We send the log message as Warning so it appears in the log (debug is only enabled when compiling with DEBUG)
std::string ro = EMSESP::system_.readonly_mode() ? "[readonly] " : "";
auto description = Helpers::translated_word(cf->description_);
char info_s[100];
if (strlen(description)) {
snprintf(info_s, sizeof(info_s), "%s/%s (%s)", dname, cmd, description);
} else {
snprintf(info_s, sizeof(info_s), "%s/%s", dname, cmd);
}
if (single_command) {
// log as DEBUG (TRACE) regardless if compiled with EMSESP_DEBUG
logger_.debug("%sCalled command %s", ro.c_str(), info_s);
} else {
if (id > 0) {
LOG_INFO(("%sCalled command %s with value %s and id %d on device 0x%02X"), ro.c_str(), info_s, value, id, device_id);
} else {
LOG_INFO(("%sCalled command %s with value %s"), ro.c_str(), info_s, value);
}
}
}
return return_code;
}
// add a command to the list, which does not return json
void Command::add(const uint8_t device_type, const uint8_t device_id, const char * cmd, const cmd_function_p cb, const char * const * description, uint8_t flags) {
// if the command already exists for that device type don't add it
if (find_command(device_type, device_id, cmd, flags) != nullptr) {
return;
}
// if the description is empty, it's hidden which means it will not show up in Web API or Console as an available command
if (!description) {
flags |= CommandFlag::HIDDEN;
}
cmdfunctions_.emplace_back(device_type, device_id, flags, cmd, cb, nullptr, description); // callback for json is nullptr
}
// add a command with no json output
// system/temperature/analog devices uses device_id 0
void Command::add(const uint8_t device_type, const char * cmd, const cmd_function_p cb, const char * const * description, uint8_t flags) {
add(device_type, 0, cmd, cb, description, flags);
}
// add a command to the list, which does return a json object as output
void Command::add(const uint8_t device_type, const char * cmd, const cmd_json_function_p cb, const char * const * description, uint8_t flags) {
// if the command already exists for that device type don't add it
if (find_command(device_type, 0, cmd, flags) != nullptr) {
return;
}
cmdfunctions_.emplace_back(device_type, 0, flags, cmd, nullptr, cb, description); // callback for json is included
}
// see if a command exists for that device type
// is not case sensitive
Command::CmdFunction * Command::find_command(const uint8_t device_type, const uint8_t device_id, const char * cmd, const uint8_t flag) {
if ((cmd == nullptr) || (strlen(cmd) == 0) || (cmdfunctions_.empty())) {
return nullptr;
}
for (auto & cf : cmdfunctions_) {
if (Helpers::toLower(cmd) == Helpers::toLower(cf.cmd_) && (cf.device_type_ == device_type) && (!device_id || cf.device_id_ == device_id)
&& (flag == CommandFlag::CMD_FLAG_DEFAULT || (flag & 0x3F) == (cf.flags_ & 0x3F))) {
return &cf;
}
}
return nullptr; // command not found
}
void Command::erase_device_commands(const uint8_t device_type) {
if (cmdfunctions_.empty()) {
return;
}
auto it = cmdfunctions_.end();
do {
int i = it - cmdfunctions_.begin();
if (cmdfunctions_[i].device_type_ == device_type) {
cmdfunctions_.erase(it);
}
} while (it-- > cmdfunctions_.begin());
}
void Command::erase_command(const uint8_t device_type, const char * cmd, uint8_t flag) {
if ((cmd == nullptr) || (strlen(cmd) == 0) || (cmdfunctions_.empty())) {
return;
}
auto it = cmdfunctions_.begin();
for (auto const & cf : cmdfunctions_) {
if (Helpers::toLower(cmd) == Helpers::toLower(cf.cmd_) && (cf.device_type_ == device_type) && ((flag & 0x3F) == (cf.flags_ & 0x3F))) {
cmdfunctions_.erase(it);
return;
}
it++;
}
}
// get the tagged command
std::string Command::tagged_cmd(const std::string & cmd, const uint8_t flag) {
switch (flag & 0x3F) {
case CommandFlag::CMD_FLAG_HC:
return "[hc<n>.]" + cmd;
case CommandFlag::CMD_FLAG_DHW:
return "dhw[n]." + cmd;
case CommandFlag::CMD_FLAG_HS:
return "hs<n>." + cmd;
case CommandFlag::CMD_FLAG_AHS:
return "ahs<n>." + cmd;
default:
return cmd;
}
}
// list all commands for a specific device, output as json
bool Command::list(const uint8_t device_type, JsonObject output) {
// common commands
output[F_(info)] = Helpers::translated_word(FL_(info_cmd));
output[F_(values)] = Helpers::translated_word(FL_(values_cmd));
output[F_(commands)] = Helpers::translated_word(FL_(commands_cmd));
output[F_(entities)] = Helpers::translated_word(FL_(entities_cmd));
if (device_type == EMSdevice::DeviceType::SYSTEM) {
output["settings/showertimer"] = Helpers::translated_word(FL_(system_cmd));
output["settings/showeralert"] = Helpers::translated_word(FL_(system_cmd));
output["settings/hideled"] = Helpers::translated_word(FL_(system_cmd));
output["settings/analogenabled"] = Helpers::translated_word(FL_(system_cmd));
output["mqtt/enabled"] = Helpers::translated_word(FL_(system_cmd));
output["ntp/enabled"] = Helpers::translated_word(FL_(system_cmd));
output["ap/enabled"] = Helpers::translated_word(FL_(system_cmd));
output["syslog/enabled"] = Helpers::translated_word(FL_(system_cmd));
}
// 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)) {
sorted_cmds.push_back(tagged_cmd(cf.cmd_, cf.flags_));
}
}
sorted_cmds.sort();
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 == tagged_cmd(cf.cmd_, cf.flags_))) {
output[cl] = Helpers::translated_word(cf.description_);
}
}
}
return true;
}
// output list of all commands to console for a specific DeviceType
void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbose) {
// 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(tagged_cmd(cf.cmd_, cf.flags_));
}
}
if (device_type == EMSdevice::DeviceType::SYSTEM) {
sorted_cmds.emplace_back("settings/showertimer");
sorted_cmds.emplace_back("settings/showeralert");
sorted_cmds.emplace_back("settings/hideled");
sorted_cmds.emplace_back("settings/analogenabled");
sorted_cmds.emplace_back("mqtt/enabled");
sorted_cmds.emplace_back("ntp/enabled");
sorted_cmds.emplace_back("ap/enabled");
sorted_cmds.emplace_back("syslog/enabled");
}
sorted_cmds.sort(); // sort them
// if not in verbose mode, just print them on a single line and exit
if (!verbose) {
sorted_cmds.emplace_front(F_(info));
sorted_cmds.emplace_front(F_(commands));
sorted_cmds.emplace_front(F_(values));
sorted_cmds.emplace_front(F_(entities));
for (const auto & cl : sorted_cmds) {
shell.print(cl);
shell.print(" ");
}
shell.println();
return;
}
// verbose mode
shell.printfln("\n%s%s %s:%s", COLOR_BOLD_ON, COLOR_YELLOW, EMSdevice::device_type_2_device_name(device_type), COLOR_RESET);
for (const auto & cl : sorted_cmds) {
// find and print the description
for (const auto & cf : cmdfunctions_) {
if ((cf.device_type_ == device_type) && !cf.has_flags(CommandFlag::HIDDEN) && cf.description_ && (cl == tagged_cmd(cf.cmd_, cf.flags_))) {
uint8_t i = cl.length();
shell.print(" ");
shell.print(cl);
// pad with spaces
while (i++ < 30) {
shell.print(' ');
}
shell.print(COLOR_BRIGHT_CYAN);
if (cf.has_flags(CommandFlag::CMD_FLAG_HC)) {
shell.print(Helpers::translated_word(FL_(tag_hcx)));
shell.print(' ');
} else if (cf.has_flags(CommandFlag::CMD_FLAG_DHW)) {
shell.print(Helpers::translated_word(FL_(tag_dhwx)));
shell.print(' ');
} else if (cf.has_flags(CommandFlag::CMD_FLAG_AHS)) {
shell.print(Helpers::translated_word(FL_(tag_ahsx)));
shell.print(' ');
} else if (cf.has_flags(CommandFlag::CMD_FLAG_HS)) {
shell.print(Helpers::translated_word(FL_(tag_hsx)));
shell.print(' ');
}
shell.print(Helpers::translated_word(cf.description_));
if (!cf.has_flags(CommandFlag::ADMIN_ONLY)) {
shell.print(' ');
shell.print(COLOR_BRIGHT_GREEN);
shell.print('*');
}
shell.print(COLOR_RESET);
}
}
shell.println();
}
}
// see if a device_type is active and has associated commands
// returns false if the device has no commands
bool Command::device_has_commands(const uint8_t device_type) {
if (device_type == EMSdevice::DeviceType::UNKNOWN) {
return false;
}
if (device_type == EMSdevice::DeviceType::SYSTEM) {
return true; // we always have System
}
if (device_type == EMSdevice::DeviceType::SCHEDULER) {
return true;
}
if (device_type == EMSdevice::DeviceType::CUSTOM) {
return true;
}
if (device_type == EMSdevice::DeviceType::TEMPERATURESENSOR) {
return EMSESP::sensor_enabled();
}
if (device_type == EMSdevice::DeviceType::ANALOGSENSOR) {
return EMSESP::analog_enabled();
}
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice && (emsdevice->device_type() == device_type)) {
return true;
}
}
return false;
}
// list sensors and EMS devices
void Command::show_devices(uuid::console::Shell & shell) {
shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::SYSTEM));
shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::CUSTOM));
shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::SCHEDULER));
if (EMSESP::sensor_enabled()) {
shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::TEMPERATURESENSOR));
}
if (EMSESP::analog_enabled()) {
shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::ANALOGSENSOR));
}
for (const auto & device_class : EMSFactory::device_handlers()) {
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice && (emsdevice->device_type() == device_class.first) && (device_has_commands(device_class.first))) {
shell.printf("%s ", EMSdevice::device_type_2_device_name(device_class.first));
break; // we only want to show one (not multiple of the same device types)
}
}
}
shell.println();
}
// 'show commands' : 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);
shell.println("Note, each listed device includes these additional commands:");
shell.printf(" info \t\t\t\t%slist all values with description%s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN);
shell.println(COLOR_RESET);
shell.printf(" commands \t\t\t%slist all commands %s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN);
shell.println(COLOR_RESET);
shell.printf(" values \t\t\t%slist all values %s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN);
shell.println(COLOR_RESET);
shell.printf(" entities \t\t\t%slist all entities %s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN);
shell.println(COLOR_RESET);
// show system ones first
show(shell, EMSdevice::DeviceType::SYSTEM, true);
show(shell, EMSdevice::DeviceType::CUSTOM, true);
show(shell, EMSdevice::DeviceType::SCHEDULER, true);
// then sensors
if (EMSESP::sensor_enabled()) {
show(shell, EMSdevice::DeviceType::TEMPERATURESENSOR, true);
}
if (EMSESP::analog_enabled()) {
show(shell, EMSdevice::DeviceType::ANALOGSENSOR, true);
}
// 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)) {
show(shell, device_class.first, true);
}
}
shell.println();
}
// creates a single json document with an error message
// object is optional
uint8_t Command::json_message(uint8_t error_code, const char * message, const JsonObject output, const char * object) {
output.clear();
if (object) {
output["message"] = std::string(message) + " " + object;
} else {
output["message"] = message;
}
return error_code;
}
// **************************
// **** SUrlParser class ****
// **************************
// Extract only the path component from the passed URI and normalized it
// e.g. //one/two////three/// becomes /one/two/three
std::string SUrlParser::path() {
std::string s = "/"; // set up the beginning slash
for (const std::string & f : m_folders) {
s += f;
s += "/";
}
s.pop_back(); // deleting last letter, that is slash '/'
return std::string(s);
}
SUrlParser::SUrlParser(const char * uri) {
parse(uri);
}
bool SUrlParser::parse(const char * uri) {
if (uri == nullptr) {
return false;
}
if (*uri == '\0') {
return false;
}
m_folders.clear();
m_keysvalues.clear();
enum Type { begin, folder, param, value };
std::string s;
const char * c = uri;
enum Type t = Type::begin;
std::string last_param;
do {
if (*c == '/') {
if (s.length() > 0) {
m_folders.push_back(s);
s.clear();
}
t = Type::folder;
} else if (*c == '?' && (t == Type::folder || t == Type::begin)) {
if (s.length() > 0) {
m_folders.push_back(s);
s.clear();
}
t = Type::param;
} else if (*c == '=' && (t == Type::param || t == Type::begin)) {
m_keysvalues[s] = "";
last_param = s;
s.clear();
t = Type::value;
} else if (*c == '&' && (t == Type::value || t == Type::param || t == Type::begin)) {
if (t == Type::value) {
m_keysvalues[last_param] = s;
} else if ((t == Type::param || t == Type::begin) && (s.length() > 0)) {
m_keysvalues[s] = "";
last_param = s;
}
t = Type::param;
s.clear();
} else if (*c == '\0' && s.length() > 0) {
if (t == Type::value) {
m_keysvalues[last_param] = s;
} else if (t == Type::folder || t == Type::begin) {
m_folders.push_back(s);
} else if (t == Type::param) {
m_keysvalues[s] = "";
last_param = s;
}
s.clear();
} else if (*c == '\0' && s.length() == 0) {
if (t == Type::param && last_param.length() > 0) {
m_keysvalues[last_param] = "";
}
s.clear();
} else {
s += *c;
}
} while (*c++ != '\0');
return true;
}
} // namespace emsesp

175
src/core/command.h Normal file
View File

@@ -0,0 +1,175 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_COMMAND_H_
#define EMSESP_COMMAND_H_
#include <unordered_map>
#include "console.h"
using uuid::console::Shell;
namespace emsesp {
#define COMMAND_MAX_LENGTH 50
// mqtt flags for command subscriptions
enum CommandFlag : uint8_t {
CMD_FLAG_DEFAULT = 0, // 0 no flags set, always subscribe to MQTT
CMD_FLAG_HC = (1 << 0), // 1 TAG_HC1 - TAG_HC8
CMD_FLAG_DHW = (1 << 1), // 2 TAG_DHW1 - TAG_DHW4
CMD_FLAG_AHS = (1 << 2), // 4 TAG_AHS1
CMD_FLAG_HS = (1 << 3), // 8 TAG_HS1 - TAG_HS16
HIDDEN = (1 << 6), // 64 do not show in API or Web
ADMIN_ONLY = (1 << 7) // 128 requires authentication
};
// return status after calling a Command
enum CommandRet : uint8_t {
FAIL = 0, // 0 or FALSE
OK, // 1 or TRUE
NOT_FOUND, // 2
ERROR, // 3
NOT_ALLOWED, // 4 - needs authentication
INVALID // 5 - invalid (tag)
};
using cmd_function_p = std::function<bool(const char * data, const int8_t id)>;
using cmd_json_function_p = std::function<bool(const char * data, const int8_t id, JsonObject output)>;
class Command {
public:
struct CmdFunction {
uint8_t device_type_; // DeviceType::
uint8_t device_id_;
uint8_t flags_; // mqtt flags for command subscriptions
const char * cmd_;
cmd_function_p cmdfunction_;
cmd_json_function_p cmdfunction_json_;
const char * const * description_;
CmdFunction(const uint8_t device_type,
const uint8_t device_id,
const uint8_t flags,
const char * cmd,
const cmd_function_p cmdfunction,
const cmd_json_function_p cmdfunction_json,
const char * const * description)
: device_type_(device_type)
, device_id_(device_id)
, flags_(flags)
, cmd_(cmd)
, cmdfunction_(cmdfunction)
, cmdfunction_json_(cmdfunction_json)
, description_(description) {
}
inline void add_flags(uint8_t flags) {
flags_ |= flags;
}
inline bool has_flags(uint8_t flags) const {
return (flags_ & flags) == flags;
}
inline void remove_flags(uint8_t flags) {
flags_ &= ~flags;
}
inline uint8_t flags() const {
return flags_;
}
};
static std::vector<CmdFunction> commands() {
return cmdfunctions_;
}
static uint8_t call(const uint8_t device_type, const char * cmd, const char * value, const bool is_admin, const int8_t id, JsonObject output);
static uint8_t call(const uint8_t device_type, const char * cmd, const char * value, const int8_t id = -1);
// with normal call back function taking a value and id
static void add(const uint8_t device_type,
const uint8_t device_id,
const char * cmd,
const cmd_function_p cb,
const char * const * description,
uint8_t flags = CommandFlag::CMD_FLAG_DEFAULT);
// same for system/temperature/analog devices
static void
add(const uint8_t device_type, const char * cmd, const cmd_function_p cb, const char * const * description, uint8_t flags = CommandFlag::CMD_FLAG_DEFAULT);
// callback function taking value, id and a json object for its output
static void add(const uint8_t device_type,
const char * cmd,
const cmd_json_function_p cb,
const char * const * description,
uint8_t flags = CommandFlag::CMD_FLAG_DEFAULT);
static void show_all(uuid::console::Shell & shell);
static Command::CmdFunction * find_command(const uint8_t device_type, const uint8_t device_id, const char * cmd, const uint8_t flag);
static std::string tagged_cmd(const std::string & cmd, const uint8_t flag);
static void erase_device_commands(const uint8_t device_type);
static void erase_command(const uint8_t device_type, const char * cmd, uint8_t flag = CommandFlag::CMD_FLAG_DEFAULT);
static void show(uuid::console::Shell & shell, uint8_t device_type, bool verbose);
static void show_devices(uuid::console::Shell & shell);
static bool device_has_commands(const uint8_t device_type);
static bool list(const uint8_t device_type, JsonObject output);
static uint8_t process(const char * path, const bool is_admin, const JsonObject input, JsonObject output);
static const char * parse_command_string(const char * command, int8_t & id);
static const char * get_attribute(const char * cmd);
static bool set_attribute(JsonObject output, const char * cmd, const char * attribute);
static const char * return_code_string(const uint8_t return_code);
private:
static uuid::log::Logger logger_;
static std::vector<CmdFunction> cmdfunctions_; // the list of commands
static uint8_t json_message(uint8_t error_code, const char * message, JsonObject output, const char * object = nullptr);
};
class SUrlParser {
private:
std::unordered_map<std::string, std::string> m_keysvalues;
std::vector<std::string> m_folders;
public:
SUrlParser() = default;
SUrlParser(const char * url);
bool parse(const char * url);
std::vector<std::string> & paths() {
return m_folders;
};
std::unordered_map<std::string, std::string> & params() {
return m_keysvalues;
};
std::string path();
};
} // namespace emsesp
#endif

87
src/core/common.h Normal file
View File

@@ -0,0 +1,87 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_COMMON_H
#define EMSESP_COMMON_H
#include <uuid/log.h>
using uuid::log::Level;
#if defined(EMSESP_DEBUG)
#define LOG_DEBUG(...) logger_.debug(__VA_ARGS__)
#else
#define LOG_DEBUG(...)
#endif
#define LOG_INFO(...) logger_.info(__VA_ARGS__)
#define LOG_TRACE(...) logger_.trace(__VA_ARGS__)
#define LOG_NOTICE(...) logger_.notice(__VA_ARGS__)
#define LOG_WARNING(...) logger_.warning(__VA_ARGS__)
#define LOG_ERROR(...) logger_.err(__VA_ARGS__)
// flash strings
using uuid::string_vector;
using string_vector = std::vector<const char *>;
#ifdef FPSTR
#undef FPSTR
#endif
// clang-format off
#define FPSTR(pstr_pointer) pstr_pointer
#define MAKE_WORD_CUSTOM(string_name, string_literal) static const char __pstr__##string_name[] = string_literal;
#define MAKE_WORD(string_name) MAKE_WORD_CUSTOM(string_name, #string_name)
#define F_(string_name) (__pstr__##string_name)
#define FL_(list_name) (__pstr__L_##list_name)
// The language settings below must match system.cpp
#if defined(EMSESP_TEST)
// in Test mode use two languages (en & de) to save flash memory needed for the tests
#define MAKE_WORD_TRANSLATION(list_name, en, de, ...) static const char * const __pstr__L_##list_name[] = {en, de, nullptr};
#define MAKE_TRANSLATION(list_name, shortname, en, de, ...) static const char * const __pstr__L_##list_name[] = {shortname, en, de, nullptr};
#elif defined(EMSESP_EN_ONLY)
// EN only
#define MAKE_WORD_TRANSLATION(list_name, en, ...) static const char * const __pstr__L_##list_name[] = {en, nullptr};
#define MAKE_TRANSLATION(list_name, shortname, en, ...) static const char * const __pstr__L_##list_name[] = {shortname, en, nullptr};
#elif defined(EMSESP_DE_ONLY)
// EN + DE
#define MAKE_WORD_TRANSLATION(list_name, en, de, ...) static const char * const __pstr__L_##list_name[] = {en, de, nullptr};
#define MAKE_TRANSLATION(list_name, shortname, en, de, ...) static const char * const __pstr__L_##list_name[] = {shortname, en, de, nullptr};
#else
#define MAKE_WORD_TRANSLATION(list_name, ...) static const char * const __pstr__L_##list_name[] = {__VA_ARGS__, nullptr};
#define MAKE_TRANSLATION(list_name, ...) static const char * const __pstr__L_##list_name[] = {__VA_ARGS__, nullptr};
#endif
#define MAKE_NOTRANSLATION(list_name, ...) static const char * const __pstr__L_##list_name[] = {__VA_ARGS__, nullptr};
// fixed strings, no translations
#define MAKE_ENUM_FIXED(enum_name, ...) static const char * const __pstr__L_##enum_name[] = {__VA_ARGS__, nullptr};
// with translations
#define MAKE_ENUM(enum_name, ...) static const char * const * __pstr__L_##enum_name[] = {__VA_ARGS__, nullptr};
// clang-format on
// load translations
#include "locale_translations.h"
#include "locale_common.h"
#endif

706
src/core/console.cpp Normal file
View File

@@ -0,0 +1,706 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "console.h"
#include "console_stream.h"
#include "emsesp.h"
#if defined(EMSESP_TEST)
#include "../test/test.h"
#endif
using ::uuid::console::Commands;
using ::uuid::console::Shell;
using LogLevel = ::uuid::log::Level;
using LogFacility = ::uuid::log::Facility;
namespace emsesp {
static constexpr unsigned long INVALID_PASSWORD_DELAY_MS = 3000;
static inline EMSESPShell & to_shell(Shell & shell) {
return static_cast<EMSESPShell &>(shell);
}
#define NO_ARGUMENTS \
std::vector<std::string> { \
}
// add static functions here....
static void console_log_level(Shell & shell, const std::vector<std::string> & arguments) {
if (!arguments.empty()) {
uuid::log::Level level;
if (uuid::log::parse_level_lowercase(arguments[0], level)) {
shell.log_level(level);
} else {
shell.printfln(F_(invalid_log_level));
return;
}
}
shell.printfln(F_(log_level_fmt), uuid::log::format_level_uppercase(shell.log_level()));
}
static std::vector<std::string> log_level_autocomplete(Shell & shell, const std::vector<std::string> & current_arguments, const std::string & next_argument) {
return uuid::log::levels_lowercase();
}
static void setup_commands(std::shared_ptr<Commands> const & commands) {
// exit, help, log
commands->add_command(ShellContext::MAIN, CommandFlags::USER, {F_(exit)}, EMSESPShell::main_exit_function);
commands->add_command(ShellContext::MAIN, CommandFlags::USER, {F_(help)}, EMSESPShell::main_help_function);
commands->add_command(ShellContext::MAIN, CommandFlags::USER, {F_(log)}, {F_(log_level_optional)}, console_log_level, log_level_autocomplete);
//
// Show commands
//
commands->add_command(
ShellContext::MAIN,
CommandFlags::USER,
{F_(show)},
{F_(show_commands)},
[](Shell & shell, const std::vector<std::string> & arguments) {
if (arguments.empty()) {
EMSESP::system_.show_system(shell);
return;
}
auto const & command = arguments.front();
if (command == F_(commands)) {
Command::show_all(shell);
} else if (command == F_(system)) {
EMSESP::system_.show_system(shell);
} else if (command == F_(users) && (shell.has_flags(CommandFlags::ADMIN))) {
EMSESP::system_.show_users(shell); // admin only
} else if (command == F_(devices)) {
EMSESP::show_devices(shell);
} else if (command == F_(log)) {
EMSESP::webLogService.show(shell);
} else if (command == F_(ems)) {
EMSESP::show_ems(shell);
} else if (command == F_(values)) {
EMSESP::show_device_values(shell);
EMSESP::show_sensor_values(shell);
} else if (command == F_(mqtt)) {
Mqtt::show_mqtt(shell);
} else {
shell.printfln("Unknown show command");
}
},
[](Shell const & shell, const std::vector<std::string> & current_arguments, const std::string & next_argument) -> std::vector<std::string> {
return std::vector<std::string>{"system", "users", "devices", "log", "ems", "values", "mqtt", "commands"};
});
//
// System commands
//
#if defined(EMSESP_TEST)
// create commands test
commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
{"test"},
string_vector{F_(name_optional), F_(data_optional), F_(id_optional)},
[=](Shell & shell, const std::vector<std::string> & arguments) {
if (arguments.empty()) {
Test::run_test(shell, "default");
} else if (arguments.size() == 1) {
Test::run_test(shell, arguments.front());
} else if (arguments.size() == 2) {
Test::run_test(shell, arguments[0].c_str(), arguments[1].c_str());
} else {
Test::run_test(shell, arguments[0].c_str(), arguments[1].c_str(), arguments[2].c_str());
}
});
commands->add_command(ShellContext::MAIN, CommandFlags::USER, {"t"}, [=](Shell & shell, const std::vector<std::string> & arguments) {
Test::run_test(shell, "default");
});
#endif
commands->add_command(ShellContext::MAIN, CommandFlags::USER, {F_(su)}, [=](Shell & shell, const std::vector<std::string> & arguments) {
auto become_admin = [](Shell & shell) {
shell.logger().log(LogLevel::NOTICE, LogFacility::AUTH, F("Admin session opened on console %s"), to_shell(shell).console_name().c_str());
shell.add_flags(CommandFlags::ADMIN);
};
if (shell.has_flags(CommandFlags::ADMIN)) {
return;
} else if (shell.has_flags(CommandFlags::LOCAL)) {
become_admin(shell);
} else {
shell.enter_password(F_(password_prompt), [=](Shell & shell, bool completed, const std::string & password) {
if (completed) {
uint64_t now = uuid::get_uptime_ms();
EMSESP::esp32React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) {
if (!password.empty() && (securitySettings.jwtSecret.equals(password.c_str()))) {
become_admin(shell);
} else {
shell.delay_until(now + INVALID_PASSWORD_DELAY_MS, [](Shell & shell) {
shell.logger().log(LogLevel::NOTICE,
LogFacility::AUTH,
F("Invalid admin password on console %s"),
to_shell(shell).console_name().c_str());
shell.println(F("su: incorrect password"));
});
}
});
}
});
}
});
commands->add_command(ShellContext::MAIN, CommandFlags::ADMIN, {F_(passwd)}, [](Shell & shell, const std::vector<std::string> & arguments) {
shell.enter_password(F_(new_password_prompt1), [](Shell & shell, bool completed, const std::string & password1) {
if (completed) {
shell.enter_password(F_(new_password_prompt2), [password1](Shell & shell, bool completed, const std::string & password2) {
if (completed) {
if (password1 == password2) {
EMSESP::esp32React.getSecuritySettingsService()->update([&](SecuritySettings & securitySettings) {
securitySettings.jwtSecret = password2.c_str();
return StateUpdateResult::CHANGED;
});
shell.println("Admin password updated");
} else {
shell.println("Passwords do not match");
}
}
});
}
});
});
commands->add_command(ShellContext::MAIN,
CommandFlags::ADMIN,
{F_(restart)},
{F_(partitionname_optional)},
[](Shell & shell, const std::vector<std::string> & arguments) {
if (arguments.size()) {
EMSESP::system_.system_restart(arguments.front().c_str());
} else {
EMSESP::system_.system_restart();
}
});
commands->add_command(ShellContext::MAIN,
CommandFlags::ADMIN,
string_vector{F_(wifi), F_(reconnect)},
[](Shell & shell, const std::vector<std::string> & arguments) { EMSESP::system_.wifi_reconnect(); });
//
// SET commands
//
commands->add_command(ShellContext::MAIN,
CommandFlags::ADMIN,
string_vector{F_(set), F_(wifi), F_(password)},
[](Shell & shell, const std::vector<std::string> & arguments) {
shell.enter_password(F_(new_password_prompt1), [](Shell & shell, bool completed, const std::string & password1) {
if (completed) {
shell.enter_password(F_(new_password_prompt2), [password1](Shell & shell, bool completed, const std::string & password2) {
if (completed) {
if (password1 == password2) {
EMSESP::esp32React.getNetworkSettingsService()->updateWithoutPropagation([&](NetworkSettings & networkSettings) {
networkSettings.password = password2.c_str();
return StateUpdateResult::CHANGED;
});
shell.println("WiFi password updated. Reconnecting...");
EMSESP::system_.wifi_reconnect();
} else {
shell.println("Passwords do not match");
}
}
});
}
});
});
commands->add_command(ShellContext::MAIN,
CommandFlags::ADMIN,
string_vector{F_(set), F_(hostname)},
{F_(name_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) {
shell.println("The network connection will be reset...");
Shell::loop_all();
delay(1000); // wait a second
EMSESP::esp32React.getNetworkSettingsService()->update([&](NetworkSettings & networkSettings) {
networkSettings.hostname = arguments.front().c_str();
return StateUpdateResult::CHANGED;
});
});
commands->add_command(ShellContext::MAIN,
CommandFlags::ADMIN,
string_vector{F_(set), F_(wifi), F_(ssid)},
{F_(name_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) {
EMSESP::esp32React.getNetworkSettingsService()->updateWithoutPropagation([&](NetworkSettings & networkSettings) {
networkSettings.ssid = arguments.front().c_str();
return StateUpdateResult::CHANGED;
});
shell.println("WiFi ssid updated. Reconnecting...");
EMSESP::system_.wifi_reconnect();
});
commands->add_command(
ShellContext::MAIN,
CommandFlags::ADMIN,
string_vector{F_(set), F_(board_profile)},
{F_(name_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) {
std::vector<int8_t> data; // led, dallas, rx, tx, button, phy_type, eth_power, eth_phy_addr, eth_clock_mode
std::string board_profile = Helpers::toUpper(arguments.front());
if (!EMSESP::system_.load_board_profile(data, board_profile)) {
shell.println("Invalid board profile (S32, E32, E32V2, MH-ET, NODEMCU, LOLIN, OLIMEX, OLIMEXPOE, C3MINI, S2MINI, S3MINI, S32S3, CUSTOM)");
return;
}
EMSESP::webSettingsService.update([&](WebSettings & settings) {
settings.board_profile = board_profile.c_str();
settings.led_gpio = data[0];
settings.dallas_gpio = data[1];
settings.rx_gpio = data[2];
settings.tx_gpio = data[3];
settings.pbutton_gpio = data[4];
settings.phy_type = data[5];
settings.eth_power = data[6]; // can be -1
settings.eth_phy_addr = data[7];
settings.eth_clock_mode = data[8];
return StateUpdateResult::CHANGED;
});
shell.printfln("Loaded board profile %s", board_profile.c_str());
EMSESP::system_.network_init(true);
});
commands->add_command(
ShellContext::MAIN,
CommandFlags::ADMIN,
string_vector{F_(set), F_(bus_id)},
{F_(deviceid_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) {
uint8_t device_id = Helpers::hextoint(arguments.front().c_str());
if ((device_id == 0x0B) || (device_id == 0x0D) || (device_id == 0x0A) || (device_id == 0x0F) || (device_id == 0x12)) {
EMSESP::webSettingsService.update([&](WebSettings & settings) {
settings.ems_bus_id = device_id;
shell.printfln(F_(bus_id_fmt), settings.ems_bus_id);
return StateUpdateResult::CHANGED;
});
} else {
shell.println("Must be 0B, 0D, 0A, 0E, 0F, or 48 - 4D");
}
},
[](Shell & shell, const std::vector<std::string> & current_arguments, const std::string & next_argument) -> std::vector<std::string> {
return std::vector<std::string>{"0B", "0D", "0A", "0E", "0F", "48", "49", "4A", "4B", "4C", "4D"};
});
commands->add_command(ShellContext::MAIN,
CommandFlags::ADMIN,
string_vector{F_(set), F_(tx_mode)},
{F_(n_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) {
uint8_t tx_mode = std::strtol(arguments[0].c_str(), nullptr, 10);
// save the tx_mode
EMSESP::webSettingsService.update([&](WebSettings & settings) {
settings.tx_mode = tx_mode;
shell.printfln(F_(tx_mode_fmt), settings.tx_mode);
return StateUpdateResult::CHANGED;
});
EMSESP::uart_init();
});
//
// EMS device commands
//
commands->add_command(ShellContext::MAIN, CommandFlags::ADMIN, {F_(scan)}, {F_(deep_optional)}, [](Shell & shell, const std::vector<std::string> & arguments) {
if (arguments.empty()) {
EMSESP::scan_devices();
} else {
shell.printfln("Performing a deep scan...");
EMSESP::clear_all_devices();
// device IDs taken from device_library.h
// send the read command with Version command
const std::vector<uint8_t> Device_Ids = {0x02, 0x08, 0x09, 0x10, 0x11, 0x12, 0x15, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x20, 0x21, 0x22, 0x23,
0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x30, 0x38, 0x40, 0x41, 0x48, 0x50, 0x51, 0x60};
for (const uint8_t device_id : Device_Ids) {
EMSESP::send_read_request(EMSdevice::EMS_TYPE_VERSION, device_id);
}
}
});
// read <deviceID> <type ID> [offset] [length]
commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
{F_(read)},
string_vector{F_(deviceid_mandatory), F_(typeid_mandatory), F_(offset_optional), F_(length_optional)},
[=](Shell & shell, const std::vector<std::string> & arguments) {
// loop through arguments and add to data as text, separated by a space
std::string data;
for (const auto & arg : arguments) {
if (!data.empty()) {
data += " ";
}
data += arg;
}
if (!System::readCommand(data.c_str())) {
shell.printfln("Invalid deviceID");
return;
}
});
commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
{F_(watch)},
string_vector{F_(watch_format_optional), F_(watchid_optional)},
[](Shell & shell, const std::vector<std::string> & arguments) {
uint16_t watch_id = WATCH_ID_NONE;
// only use english commands, not the translations
if (!arguments.empty()) {
// get raw/pretty
if (arguments[0] == F_(raw)) {
EMSESP::watch(EMSESP::WATCH_RAW); // raw
} else if (arguments[0] == (FL_(on)[0])) {
EMSESP::watch(EMSESP::WATCH_ON); // on
} else if (arguments[0] == (FL_(off)[0])) {
EMSESP::watch(EMSESP::WATCH_OFF); // off
} else if (arguments[0] == (FL_(unknown)[0])) {
EMSESP::watch(EMSESP::WATCH_UNKNOWN); // unknown
watch_id = WATCH_ID_NONE;
} else {
watch_id = Helpers::hextoint(arguments[0].c_str());
if (watch_id > 0 && ((EMSESP::watch() == EMSESP::WATCH_OFF) || (EMSESP::watch() == EMSESP::WATCH_UNKNOWN))) {
EMSESP::watch(EMSESP::WATCH_ON); // on
} else if (watch_id == 0) {
EMSESP::watch(EMSESP::WATCH_OFF); // off
return;
}
}
if (arguments.size() == 2) {
// get the watch_id if its set
watch_id = Helpers::hextoint(arguments[1].c_str());
}
EMSESP::watch_id(watch_id);
} else {
shell.printfln("Invalid: use watch raw|on|off|unknown|id [id]");
return;
}
uint8_t watch = EMSESP::watch();
if (watch == EMSESP::WATCH_OFF) {
shell.printfln("Watching telegrams is off");
return;
}
// if logging is off, the watch won't show anything, show force it back to NOTICE
if (shell.log_level() < Level::NOTICE) {
shell.log_level(Level::NOTICE);
shell.printfln("Setting log level to Notice");
}
if (watch == EMSESP::WATCH_ON) {
shell.printfln("Watching incoming telegrams, displayed in decoded format");
} else if (watch == EMSESP::WATCH_RAW) {
shell.printfln("Watching incoming telegrams, displayed as raw bytes"); // WATCH_RAW
} else {
shell.printfln("Watching unknown telegrams"); // WATCH_UNKNOWN
}
watch_id = EMSESP::watch_id();
if (watch_id > 0x80) {
shell.printfln("Filtering only telegrams that match a telegram type of 0x%02X", watch_id);
} else if (watch_id != WATCH_ID_NONE) {
shell.printfln("Filtering only telegrams that match a deviceID or telegram type of 0x%02X", watch_id);
}
});
commands->add_command(
ShellContext::MAIN,
CommandFlags::ADMIN,
{F_(call)},
string_vector{F_(device_type_optional), F_(cmd_optional), F_(data_optional), F_(id_optional)},
[&](Shell & shell, const std::vector<std::string> & arguments) {
if (arguments.empty()) {
Command::show_all(shell); // list options
return;
}
// validate the device_type
uint8_t device_type = EMSdevice::device_name_2_device_type(arguments[0].c_str());
if (!Command::device_has_commands(device_type)) {
shell.print("Invalid device. Available devices are: ");
Command::show_devices(shell);
return;
}
// validate that a command is present
if (arguments.size() < 2) {
shell.print("Missing command. Available commands are: ");
Command::show(shell, device_type, false); // non-verbose mode
return;
}
JsonDocument doc;
int8_t id = -1;
auto arg = Helpers::toLower(arguments[1]);
const char * cmd = arg.c_str(); // prevent loosing pointer after object destruction
uint8_t return_code = CommandRet::OK;
JsonObject json = doc.to<JsonObject>();
bool has_data = false;
if (device_type >= EMSdevice::DeviceType::BOILER) {
cmd = Command::parse_command_string(cmd, id); // extract hc or dhw
}
if (cmd == nullptr) {
cmd = F_(values);
}
if (arguments.size() == 2) {
// no value specified, just the cmd
return_code = Command::call(device_type, cmd, nullptr, true, id, json);
} else if (arguments.size() == 3) {
if (!strcmp(cmd, F_(info)) || !strcmp(cmd, F_(values))) {
// info has a id but no value
return_code = Command::call(device_type, cmd, nullptr, true, atoi(arguments.back().c_str()), json);
} 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);
}
} else {
// use value, which could be an id or hc
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);
}
}
if (return_code == CommandRet::OK) {
if (json.size()) {
if (json["api_data"].is<std::string>()) {
std::string data = json["api_data"];
shell.println(data.c_str());
return;
}
serializeJsonPretty(doc, shell);
shell.println();
return;
} 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");
shell.print("Available commands are: ");
Command::show(shell, device_type, false); // non-verbose mode
} else if ((return_code == CommandRet::ERROR) || (return_code == CommandRet::FAIL)) {
shell.printfln("Bad syntax. Check arguments.");
} else {
shell.printfln("Command failed with error code %d", return_code);
}
},
[](Shell & shell, const std::vector<std::string> & current_arguments, const std::string & next_argument) -> std::vector<std::string> {
if (current_arguments.empty()) {
std::vector<std::string> devices_list;
devices_list.emplace_back(EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::SYSTEM));
devices_list.emplace_back(EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::TEMPERATURESENSOR));
devices_list.emplace_back(EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::ANALOGSENSOR));
for (const auto & device_class : EMSFactory::device_handlers()) {
if (Command::device_has_commands(device_class.first)) {
devices_list.emplace_back(EMSdevice::device_type_2_device_name(device_class.first));
}
}
return devices_list;
} else if (current_arguments.size() == 1) {
uint8_t device_type = EMSdevice::device_name_2_device_type(current_arguments[0].c_str());
if (Command::device_has_commands(device_type)) {
std::vector<std::string> command_list;
for (const auto & cf : Command::commands()) {
if (cf.device_type_ == device_type) {
command_list.emplace_back(cf.cmd_);
}
}
return command_list;
}
}
return {};
});
}
std::shared_ptr<Commands> EMSESPShell::commands_ = [] {
auto commands = std::make_shared<Commands>();
setup_commands(commands);
return commands;
}();
EMSESPShell::EMSESPShell(EMSESP & emsesp, Stream & stream, unsigned int context, unsigned int flags)
: Shell(stream, commands_, context, flags)
, emsesp_(emsesp) {
}
void EMSESPShell::started() {
logger().log(LogLevel::INFO, LogFacility::CONSOLE, F("User session opened on console %s"), console_name().c_str());
}
void EMSESPShell::stopped() {
if (has_flags(CommandFlags::ADMIN)) {
logger().log(LogLevel::INFO, LogFacility::AUTH, F("Admin session closed on console %s"), console_name().c_str());
}
logger().log(LogLevel::INFO, LogFacility::CONSOLE, F("User session closed on console %s"), console_name().c_str());
}
// show welcome banner
void EMSESPShell::display_banner() {
println();
printfln("┌───────────────────────────────────────┐");
printfln("│ %sEMS-ESP version %-20s%s │", COLOR_BOLD_ON, EMSESP_APP_VERSION, COLOR_BOLD_OFF);
printfln("│ │");
printfln("│ %shelp%s to show available commands │", COLOR_UNDERLINE, COLOR_RESET);
printfln("│ %ssu%s to access admin commands │", COLOR_UNDERLINE, COLOR_RESET);
printfln("│ │");
printfln("│ %s%shttps://github.com/emsesp/EMS-ESP32%s │", COLOR_BRIGHT_GREEN, COLOR_UNDERLINE, COLOR_RESET);
printfln("│ │");
printfln("└───────────────────────────────────────┘");
println();
// set console name
EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) { console_hostname_ = networkSettings.hostname.c_str(); });
if (console_hostname_.empty()) {
console_hostname_ = "ems-esp";
}
}
std::string EMSESPShell::hostname_text() {
return console_hostname_;
}
std::string EMSESPShell::context_text() {
return std::string{};
// auto shell_context = static_cast<ShellContext>(context());
// if (shell_context == ShellContext::MAIN) {
// return std::string{'/'};
// } else if (shell_context == ShellContext::FILESYSTEM) {
// return "/fs");
// } else {
// return std::string{};
// }
}
// when in su (admin) show # as the prompt suffix
std::string EMSESPShell::prompt_suffix() {
#ifndef EMSESP_UNITY
if (has_flags(CommandFlags::ADMIN)) {
return std::string{'#'};
} else {
return std::string{'$'};
}
#else
// don't bother with prompt suffix if we're testing Unity output
return "";
#endif
}
void EMSESPShell::end_of_transmission() {
invoke_command(F_(exit));
}
void EMSESPShell::main_help_function(Shell & shell, const std::vector<std::string> & arguments) {
shell.println();
#if defined(EMSESP_DEBUG)
shell.printfln("%s%sEMS-ESP version %s%s", COLOR_BRIGHT_GREEN, COLOR_BOLD_ON, EMSESP_APP_VERSION, COLOR_RESET);
#else
shell.printfln("%s%sEMS-ESP version %s%s (DEBUG)", COLOR_BRIGHT_GREEN, COLOR_BOLD_ON, EMSESP_APP_VERSION, COLOR_RESET);
#endif
shell.println();
shell.print_all_available_commands();
}
void EMSESPShell::main_exit_function(Shell & shell, const std::vector<std::string> & arguments) {
shell.stop();
}
// **** EMSESPConsole Class *****
#ifndef EMSESP_STANDALONE
std::vector<bool> EMSESPConsole::ptys_;
#endif
EMSESPConsole::EMSESPConsole(EMSESP & emsesp, Stream & stream, bool local)
: EMSESPShell(emsesp, stream, ShellContext::MAIN, local ? (CommandFlags::USER | CommandFlags::LOCAL) : CommandFlags::USER)
, name_("ttyS0")
#ifndef EMSESP_STANDALONE
, pty_(std::numeric_limits<size_t>::max())
, addr_()
, port_(0)
#endif
{
}
#ifndef EMSESP_STANDALONE
EMSESPConsole::EMSESPConsole(EMSESP & emsesp, Stream & stream, const IPAddress & addr, uint16_t port)
: EMSESPShell(emsesp, stream, ShellContext::MAIN, CommandFlags::USER)
, addr_(addr)
, port_(port) {
std::array<char, 16> text;
pty_ = 0;
while (pty_ < ptys_.size() && ptys_[pty_])
pty_++;
if (pty_ == ptys_.size()) {
ptys_.push_back(true);
} else {
ptys_[pty_] = true;
}
snprintf(text.data(), text.size(), "pty%u", pty_);
name_ = text.data();
logger().info("Allocated console %s for connection from [%s]:%u", name_.c_str(), uuid::printable_to_string(addr_).c_str(), port_);
}
#endif
EMSESPConsole::~EMSESPConsole() {
#ifndef EMSESP_STANDALONE
if (pty_ != SIZE_MAX) {
logger().info("Shutdown console %s for connection from [%s]:%u", name_.c_str(), uuid::printable_to_string(addr_).c_str(), port_);
ptys_[pty_] = false;
ptys_.shrink_to_fit();
}
#endif
}
std::string EMSESPConsole::console_name() {
return name_;
}
} // namespace emsesp

70
src/core/console.h Normal file
View File

@@ -0,0 +1,70 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_CONSOLE_H
#define EMSESP_CONSOLE_H
#include <uuid/console.h>
#include <uuid/log.h>
#include "helpers.h"
#include "system.h"
#include "mqtt.h"
#ifdef LOCAL
#undef LOCAL
#endif
namespace emsesp {
enum ShellContext : uint8_t { MAIN = 0, SYSTEM, END };
class EMSESP;
class EMSESPShell : public uuid::console::Shell {
public:
~EMSESPShell() override = default;
virtual std::string console_name() = 0;
static void main_help_function(Shell & shell, const std::vector<std::string> & arguments);
static void main_exit_function(Shell & shell, const std::vector<std::string> & arguments);
EMSESP & emsesp_;
protected:
EMSESPShell(EMSESP & emsesp, Stream & stream, unsigned int context, unsigned int flags);
static std::shared_ptr<uuid::console::Commands> commands_;
// our custom functions for Shell
void started() override;
void stopped() override;
void display_banner() override;
std::string hostname_text() override;
std::string context_text() override;
std::string prompt_suffix() override;
void end_of_transmission() override;
private:
std::string console_hostname_;
};
} // namespace emsesp
#endif

50
src/core/console_stream.h Normal file
View File

@@ -0,0 +1,50 @@
/*
* mcu-app - Microcontroller application framework
* Copyright 2022 Simon Arlott
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <uuid/console.h>
#include "console.h"
namespace emsesp {
class EMSESPConsole : public EMSESPShell {
public:
EMSESPConsole(EMSESP & emsesp, Stream & stream, bool local);
#ifndef ENV_NATIVE
EMSESPConsole(EMSESP & emsesp, Stream & stream, const IPAddress & addr, uint16_t port);
#endif
~EMSESPConsole() override;
std::string console_name() override;
private:
#ifndef EMSESP_STANDALONE
static std::vector<bool> ptys_;
#endif
std::string name_;
#ifndef EMSESP_STANDALONE
size_t pty_;
IPAddress addr_;
uint16_t port_;
#endif
};
} // namespace emsesp

300
src/core/default_settings.h Normal file
View File

@@ -0,0 +1,300 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_DEFAULT_SETTINGS_H
#define EMSESP_DEFAULT_SETTINGS_H
// GENERAL SETTINGS
#ifndef EMSESP_DEFAULT_LOCALE
#define EMSESP_DEFAULT_LOCALE EMSESP_LOCALE_EN // English
#endif
#ifndef EMSESP_DEFAULT_VERSION
#define EMSESP_DEFAULT_VERSION ""
#endif
#ifndef EMSESP_DEFAULT_TX_MODE
#define EMSESP_DEFAULT_TX_MODE 1 // EMS1.0
#endif
#ifndef EMSESP_DEFAULT_EMS_BUS_ID
#define EMSESP_DEFAULT_EMS_BUS_ID 0x0B // service key
#endif
#ifndef EMSESP_DEFAULT_SYSLOG_ENABLED
#define EMSESP_DEFAULT_SYSLOG_ENABLED false
#endif
#ifndef EMSESP_DEFAULT_SYSLOG_LEVEL
#define EMSESP_DEFAULT_SYSLOG_LEVEL 3 // ERR
#endif
#ifndef EMSESP_DEFAULT_SYSLOG_MARK_INTERVAL
#define EMSESP_DEFAULT_SYSLOG_MARK_INTERVAL 0
#endif
#ifndef EMSESP_DEFAULT_SYSLOG_HOST
#define EMSESP_DEFAULT_SYSLOG_HOST ""
#endif
#ifndef EMSESP_DEFAULT_SYSLOG_PORT
#define EMSESP_DEFAULT_SYSLOG_PORT 514
#endif
#ifndef EMSESP_DEFAULT_TRACELOG_RAW
#define EMSESP_DEFAULT_TRACELOG_RAW false
#endif
#ifndef EMSESP_DEFAULT_BOILER_HEATINGOFF
#define EMSESP_DEFAULT_BOILER_HEATINGOFF false
#endif
#ifndef EMSESP_DEFAULT_REMOTE_TIMEOUT
#define EMSESP_DEFAULT_REMOTE_TIMEOUT 24
#endif
#ifndef EMSESP_DEFAULT_REMOTE_TIMEOUT_EN
#define EMSESP_DEFAULT_REMOTE_TIMEOUT_EN false
#endif
#ifndef EMSESP_DEFAULT_SHOWER_TIMER
#define EMSESP_DEFAULT_SHOWER_TIMER false
#endif
#ifndef EMSESP_DEFAULT_SHOWER_ALERT
#define EMSESP_DEFAULT_SHOWER_ALERT false
#endif
#ifndef EMSESP_DEFAULT_SHOWER_ALERT_TRIGGER
#define EMSESP_DEFAULT_SHOWER_ALERT_TRIGGER 7
#endif
#ifndef EMSESP_DEFAULT_SHOWER_MIN_DURATION
#define EMSESP_DEFAULT_SHOWER_MIN_DURATION 180
#endif
#ifndef EMSESP_DEFAULT_SHOWER_ALERT_COLDSHOT
#define EMSESP_DEFAULT_SHOWER_ALERT_COLDSHOT 10
#endif
#ifndef EMSESP_DEFAULT_HIDE_LED
#define EMSESP_DEFAULT_HIDE_LED false
#endif
#ifndef EMSESP_DEFAULT_DALLAS_PARASITE
#define EMSESP_DEFAULT_DALLAS_PARASITE false
#endif
#ifndef EMSESP_DEFAULT_NOTOKEN_API
#define EMSESP_DEFAULT_NOTOKEN_API false
#endif
#ifndef EMSESP_DEFAULT_BOOL_FORMAT
#define EMSESP_DEFAULT_BOOL_FORMAT 1 // on/off
#endif
#ifndef EMSESP_DEFAULT_ENUM_FORMAT
#define EMSESP_DEFAULT_ENUM_FORMAT 1 // Text
#endif
#ifndef EMSESP_DEFAULT_ANALOG_ENABLED
#define EMSESP_DEFAULT_ANALOG_ENABLED true
#endif
#ifndef EMSESP_DEFAULT_TELNET_ENABLED
#define EMSESP_DEFAULT_TELNET_ENABLED true
#endif
#ifndef EMSESP_DEFAULT_MODBUS_ENABLED
#define EMSESP_DEFAULT_MODBUS_ENABLED false
#endif
#ifndef EMSESP_DEFAULT_MODBUS_PORT
#define EMSESP_DEFAULT_MODBUS_PORT 502
#endif
#ifndef EMSESP_DEFAULT_MODBUS_MAX_CLIENTS
#define EMSESP_DEFAULT_MODBUS_MAX_CLIENTS 10
#endif
#ifndef EMSESP_DEFAULT_MODBUS_TIMEOUT
#define EMSESP_DEFAULT_MODBUS_TIMEOUT 300
#endif
#ifndef EMSESP_DEFAULT_BOARD_PROFILE
#define EMSESP_DEFAULT_BOARD_PROFILE "default"
#endif
// Default GPIO PIN definitions
#ifndef EMSESP_DEFAULT_RX_GPIO
#define EMSESP_DEFAULT_RX_GPIO 23 // D7
#endif
#ifndef EMSESP_DEFAULT_TX_GPIO
#define EMSESP_DEFAULT_TX_GPIO 5 // D8
#endif
#ifndef EMSESP_DEFAULT_DALLAS_GPIO
#define EMSESP_DEFAULT_DALLAS_GPIO 18
#endif
#ifndef EMSESP_DEFAULT_LED_GPIO
#define EMSESP_DEFAULT_LED_GPIO 2
#endif
#ifndef EMSESP_DEFAULT_PBUTTON_GPIO
#define EMSESP_DEFAULT_PBUTTON_GPIO 0
#endif
#ifndef EMSESP_DEFAULT_PHY_TYPE
#define EMSESP_DEFAULT_PHY_TYPE 0 // No Ethernet, just Wifi. PHY_type::PHY_TYPE_NONE,
#endif
// MQTT
#ifndef EMSESP_DEFAULT_BOOL_FORMAT
#define EMSESP_DEFAULT_BOOL_FORMAT 1 // on/off
#endif
#ifndef EMSESP_DEFAULT_MQTT_QOS
#define EMSESP_DEFAULT_MQTT_QOS 0
#endif
#ifndef EMSESP_DEFAULT_MQTT_RETAIN
#define EMSESP_DEFAULT_MQTT_RETAIN false
#endif
#ifndef EMSESP_DEFAULT_HA_ENABLED
#define EMSESP_DEFAULT_HA_ENABLED false
#endif
#ifndef EMSESP_DEFAULT_PUBLISH_TIME
#define EMSESP_DEFAULT_PUBLISH_TIME 10
#endif
// default for scheduler etc
#ifndef EMSESP_DEFAULT_PUBLISH_TIME_OTHER
#define EMSESP_DEFAULT_PUBLISH_TIME_OTHER 60
#endif
#ifndef EMSESP_DEFAULT_PUBLISH_HEARTBEAT
#define EMSESP_DEFAULT_PUBLISH_HEARTBEAT 60
#endif
#ifndef EMSESP_DEFAULT_NESTED_FORMAT
#define EMSESP_DEFAULT_NESTED_FORMAT 1
#endif
#ifndef EMSESP_DEFAULT_DISCOVERY_PREFIX
#define EMSESP_DEFAULT_DISCOVERY_PREFIX "homeassistant"
#endif
#ifndef EMSESP_DEFAULT_DISCOVERY_TYPE
#define EMSESP_DEFAULT_DISCOVERY_TYPE 0 // HA
#endif
#ifndef EMSESP_DEFAULT_PUBLISH_SINGLE
#define EMSESP_DEFAULT_PUBLISH_SINGLE false
#endif
#ifndef EMSESP_DEFAULT_PUBLISH_SINGLE2CMD
#define EMSESP_DEFAULT_PUBLISH_SINGLE2CMD false
#endif
#ifndef EMSESP_DEFAULT_SEND_RESPONSE
#define EMSESP_DEFAULT_SEND_RESPONSE false
#endif
#ifndef EMSESP_DEFAULT_SOLAR_MAXFLOW
#define EMSESP_DEFAULT_SOLAR_MAXFLOW 30
#endif
#ifndef EMSESP_DEFAULT_SENSOR_NAME
#define EMSESP_DEFAULT_SENSOR_NAME ""
#endif
#ifndef EMSESP_DEFAULT_ANALOG_NAME
#define EMSESP_DEFAULT_ANALOG_NAME ""
#endif
#ifndef EMSESP_DEFAULT_ANALOG_UOM
#define EMSESP_DEFAULT_ANALOG_UOM "mV"
#endif
#ifndef EMSESP_DEFAULT_WEBLOG_LEVEL
#define EMSESP_DEFAULT_WEBLOG_LEVEL 6 // INFO
#endif
#ifndef EMSESP_DEFAULT_WEBLOG_BUFFER
#define EMSESP_DEFAULT_WEBLOG_BUFFER 50
#endif
#ifndef EMSESP_DEFAULT_WEBLOG_COMPACT
#define EMSESP_DEFAULT_WEBLOG_COMPACT true
#endif
#ifndef EMSESP_DEFAULT_ENTITY_FORMAT
#define EMSESP_DEFAULT_ENTITY_FORMAT 1 // in MQTT discovery, single instance, shortname (EntityFormat::SINGLE_SHORT)
#endif
// matches Web UI settings
enum {
BOOL_FORMAT_ONOFF_STR = 1, // 1
BOOL_FORMAT_ONOFF_STR_CAP, // 2
BOOL_FORMAT_TRUEFALSE_STR, // 3
BOOL_FORMAT_TRUEFALSE, // 4
BOOL_FORMAT_10_STR, // 5
BOOL_FORMAT_10 // 6
};
enum {
ENUM_FORMAT_VALUE = 1, // 1
ENUM_FORMAT_INDEX // 2
};
#if CONFIG_IDF_TARGET_ESP32C3
#define EMSESP_PLATFORM "ESP32C3"
#elif CONFIG_IDF_TARGET_ESP32S2
#define EMSESP_PLATFORM "ESP32S2"
#elif CONFIG_IDF_TARGET_ESP32S3
#define EMSESP_PLATFORM "ESP32S3"
#elif CONFIG_IDF_TARGET_ESP32 || EMSESP_STANDALONE
#define EMSESP_PLATFORM "ESP32"
#else
#error Target CONFIG_IDF_TARGET is not supported
#endif
#ifndef ARDUINO_VERSION
#ifndef STRINGIZE
#define STRINGIZE(s) #s
#endif
#if TASMOTA_SDK
#define ARDUINO_VERSION_STR(major, minor, patch) "Tasmota Arduino v" STRINGIZE(major) "." STRINGIZE(minor) "." STRINGIZE(patch)
#else
#define ARDUINO_VERSION_STR(major, minor, patch) "ESP32 Arduino v" STRINGIZE(major) "." STRINGIZE(minor) "." STRINGIZE(patch)
#endif
#define ARDUINO_VERSION ARDUINO_VERSION_STR(ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH)
#endif
#endif

203
src/core/device_library.h Normal file
View File

@@ -0,0 +1,203 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// clang-format off
/*
* These is the EMS devices that we currently recognize
* The types and flags are stored in emsdevice.h
*/
// Boilers - 0x08
{ 8, DeviceType::BOILER, "CS*800i, Logatherm WLW*", DeviceFlags::EMS_DEVICE_FLAG_HEATPUMP},
{ 12, DeviceType::BOILER, "C1200W", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{ 64, DeviceType::BOILER, "BK13/BK15, Smartline, GB1*2", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{ 72, DeviceType::BOILER, "Logano GB1*5, Logamatic MC10", DeviceFlags::EMS_DEVICE_FLAG_EMS},
{ 81, DeviceType::BOILER, "Cascade CM10", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{ 84, DeviceType::BOILER, "Logamax Plus GB022", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{ 95, DeviceType::BOILER, "Condens, Logamax/Logomatic, Cerapur Top, Greenstar, Generic HT3", DeviceFlags::EMS_DEVICE_FLAG_HT3},
{115, DeviceType::BOILER, "Topline, GB162", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{121, DeviceType::BOILER, "Cascade MCM10", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{122, DeviceType::BOILER, "Proline", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{123, DeviceType::BOILER, "GB*72, Trendline, Cerapur, Greenstar Si", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{131, DeviceType::BOILER, "GB212", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{132, DeviceType::BOILER, "GC7000F", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{133, DeviceType::BOILER, "Logano GB125/KB195i, Logamatic MC110", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{154, DeviceType::BOILER, "Greenstar 30Ri Compact", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{167, DeviceType::BOILER, "Cerapur Aero", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{168, DeviceType::BOILER, "Hybrid Heatpump", DeviceFlags::EMS_DEVICE_FLAG_HYBRID},
{170, DeviceType::BOILER, "Logano GB212", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{172, DeviceType::BOILER, "Enviline, Compress 6000AW, Hybrid 3000-7000iAW, SupraEco/Geo 5xx, WLW196i/WSW196i", DeviceFlags::EMS_DEVICE_FLAG_HEATPUMP},
{173, DeviceType::BOILER, "Geo 5xx", DeviceFlags::EMS_DEVICE_FLAG_HEATPUMP},
{195, DeviceType::BOILER, "Condens 5000i, Greenstar 8000/GC9800IW, GB192i*2", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{203, DeviceType::BOILER, "Logamax U122, Cerapur", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{206, DeviceType::BOILER, "Ecomline Excellent", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{208, DeviceType::BOILER, "Logamax Plus, GB192, Condens GC9000, Greenstar ErP", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{210, DeviceType::BOILER, "Cascade MC400", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{211, DeviceType::BOILER, "EasyControl Adapter", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{219, DeviceType::BOILER, "Greenstar HIU, Logamax kompakt WS170", DeviceFlags::EMS_DEVICE_FLAG_HIU},
{234, DeviceType::BOILER, "Logamax Plus GB122, Condense 2300, Junkers Cerapur GC2200W", DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Controllers - 0x09 / 0x10 / 0x50
{ 68, DeviceType::CONTROLLER, "BC10, RFM20", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{ 81, DeviceType::CONTROLLER, "CM10", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{ 84, DeviceType::CONTROLLER, "GB022", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{ 89, DeviceType::CONTROLLER, "BC10, GB142", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{ 95, DeviceType::CONTROLLER, "HT3", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{114, DeviceType::CONTROLLER, "BC10", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{121, DeviceType::CONTROLLER, "MCM10", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{125, DeviceType::CONTROLLER, "BC25", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{152, DeviceType::CONTROLLER, "Controller", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{168, DeviceType::CONTROLLER, "Hybrid Heatpump", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{169, DeviceType::CONTROLLER, "BC40", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{190, DeviceType::CONTROLLER, "BC10", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{194, DeviceType::CONTROLLER, "BC10", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{206, DeviceType::CONTROLLER, "Ecomline", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{207, DeviceType::CONTROLLER, "Sense II/CS200", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x10
{209, DeviceType::CONTROLLER, "ErP", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{224, DeviceType::CONTROLLER, "9000i", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{229, DeviceType::CONTROLLER, "8700i", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{230, DeviceType::CONTROLLER, "BC Base", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{240, DeviceType::CONTROLLER, "Rego 3000", DeviceFlags::EMS_DEVICE_FLAG_IVT}, // 0x09
{241, DeviceType::CONTROLLER, "Condens 5000i", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
// Thermostat - not currently supporting write operations, like the Easy/100 types - 0x18
{202, DeviceType::THERMOSTAT, "Logamatic TC100, Moduline Easy", DeviceFlags::EMS_DEVICE_FLAG_EASY | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write
{203, DeviceType::THERMOSTAT, "EasyControl, CT200", DeviceFlags::EMS_DEVICE_FLAG_EASY | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write
// Thermostat - Buderus/Nefit/Bosch specific - 0x17 / 0x10 / 0x18 / 0x19-0x1B for hc2-4 / 0x38
{ 4, DeviceType::THERMOSTAT, "UI800, BC400", DeviceFlags::EMS_DEVICE_FLAG_BC400}, // 0x10
{ 10, DeviceType::THERMOSTAT, "CR11", DeviceFlags::EMS_DEVICE_FLAG_CR11}, // 0x18
{ 65, DeviceType::THERMOSTAT, "RC10", DeviceFlags::EMS_DEVICE_FLAG_RC20_N},// 0x17
{ 67, DeviceType::THERMOSTAT, "RC30", DeviceFlags::EMS_DEVICE_FLAG_RC30_N},// 0x10 - based on RC35
{ 77, DeviceType::THERMOSTAT, "RC20, Moduline 300", DeviceFlags::EMS_DEVICE_FLAG_RC20},// 0x17
{ 78, DeviceType::THERMOSTAT, "Moduline 400", DeviceFlags::EMS_DEVICE_FLAG_RC30}, // 0x10
{ 79, DeviceType::THERMOSTAT, "RC10, Moduline 100", DeviceFlags::EMS_DEVICE_FLAG_RC10},// 0x17
{ 80, DeviceType::THERMOSTAT, "Moduline 200", DeviceFlags::EMS_DEVICE_FLAG_RC10}, // 0x17
{ 86, DeviceType::THERMOSTAT, "RC35", DeviceFlags::EMS_DEVICE_FLAG_RC35}, // 0x10
{ 90, DeviceType::THERMOSTAT, "RC10, Moduline 100", DeviceFlags::EMS_DEVICE_FLAG_RC20_N}, // 0x17
{ 93, DeviceType::THERMOSTAT, "RC20RF", DeviceFlags::EMS_DEVICE_FLAG_RC20}, // 0x19
{ 94, DeviceType::THERMOSTAT, "RFM20 Remote", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x18
{151, DeviceType::THERMOSTAT, "RC25", DeviceFlags::EMS_DEVICE_FLAG_RC25}, // 0x17
{157, DeviceType::THERMOSTAT, "RC200, CW100, CR120, CR50", DeviceFlags::EMS_DEVICE_FLAG_RC100}, // 0x18,, CR120 v22 is FLAG_BC400
{158, DeviceType::THERMOSTAT, "RC3*0, Moduline 3000/1010H, CW400, Sense II, HPC410", DeviceFlags::EMS_DEVICE_FLAG_RC300}, // 0x10
{165, DeviceType::THERMOSTAT, "RC100, CR10, Moduline 1000/1010", DeviceFlags::EMS_DEVICE_FLAG_RC100}, // 0x18, 0x38
{172, DeviceType::THERMOSTAT, "Rego 2000/3000", DeviceFlags::EMS_DEVICE_FLAG_R3000}, // 0x10
{215, DeviceType::THERMOSTAT, "Comfort RF", DeviceFlags::EMS_DEVICE_FLAG_CRF | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE}, // 0x18
{216, DeviceType::THERMOSTAT, "CRF200S", DeviceFlags::EMS_DEVICE_FLAG_CRF | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE}, // 0x18
{246, DeviceType::THERMOSTAT, "Comfort+2RF", DeviceFlags::EMS_DEVICE_FLAG_CRF | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE}, // 0x18
{253, DeviceType::THERMOSTAT, "Rego 3000, UI800, Logamatic BC400", DeviceFlags::EMS_DEVICE_FLAG_BC400}, // 0x10
// Thermostat - Sieger - 0x10 / 0x17
{ 66, DeviceType::THERMOSTAT, "ES72, RC20", DeviceFlags::EMS_DEVICE_FLAG_RC20_N}, // 0x17 or remote
{ 76, DeviceType::THERMOSTAT, "ES73", DeviceFlags::EMS_DEVICE_FLAG_RC30_N}, // 0x10
{113, DeviceType::THERMOSTAT, "ES72, RC20", DeviceFlags::EMS_DEVICE_FLAG_RC20_N}, // 0x17
{156, DeviceType::THERMOSTAT, "ES79", DeviceFlags::EMS_DEVICE_FLAG_RC35}, // 0x10
// Thermostat - Junkers - 0x10
{105, DeviceType::THERMOSTAT, "FW100", DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{106, DeviceType::THERMOSTAT, "FW200", DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{107, DeviceType::THERMOSTAT, "FR100", DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_OLD}, // older model
{108, DeviceType::THERMOSTAT, "FR110", DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_OLD}, // older model
{109, DeviceType::THERMOSTAT, "FB10", DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{110, DeviceType::THERMOSTAT, "FB100", DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{111, DeviceType::THERMOSTAT, "FR10", DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_OLD}, // older model
{116, DeviceType::THERMOSTAT, "FW500", DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{147, DeviceType::THERMOSTAT, "FR50", DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_OLD},
{191, DeviceType::THERMOSTAT, "FR120", DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_OLD}, // older model
{192, DeviceType::THERMOSTAT, "FW120", DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
// Thermostat remote - 0x38
{ 3, DeviceType::THERMOSTAT, "RT800, RC220", DeviceFlags::EMS_DEVICE_FLAG_RC100H},
{200, DeviceType::THERMOSTAT, "RC100H, CR10H", DeviceFlags::EMS_DEVICE_FLAG_RC100H},
{249, DeviceType::THERMOSTAT, "TR120RF, CR20RF", DeviceFlags::EMS_DEVICE_FLAG_RC100H},
// Solar Modules - 0x30 (for solar), 0x2A, 0x41 (for ww)
{ 73, DeviceType::SOLAR, "SM10", DeviceFlags::EMS_DEVICE_FLAG_SM10},
{101, DeviceType::SOLAR, "ISM1", DeviceFlags::EMS_DEVICE_FLAG_ISM},
{103, DeviceType::SOLAR, "ISM2", DeviceFlags::EMS_DEVICE_FLAG_ISM},
{162, DeviceType::SOLAR, "SM50", DeviceFlags::EMS_DEVICE_FLAG_SM100},
{163, DeviceType::SOLAR, "SM100, MS100", DeviceFlags::EMS_DEVICE_FLAG_SM100},
{164, DeviceType::SOLAR, "SM200, MS200", DeviceFlags::EMS_DEVICE_FLAG_SM100},
// Mixer Modules - 0x20-0x27 for HC, 0x28-0x29 for WWC and 0x11 for the MP100
{ 69, DeviceType::MIXER, "MM10", DeviceFlags::EMS_DEVICE_FLAG_MM10},
{100, DeviceType::MIXER, "IPM", DeviceFlags::EMS_DEVICE_FLAG_IPM},
{102, DeviceType::MIXER, "IPM2", DeviceFlags::EMS_DEVICE_FLAG_IPM},
{159, DeviceType::MIXER, "MM50", DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{160, DeviceType::MIXER, "MM100", DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{161, DeviceType::MIXER, "MM200", DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{193, DeviceType::MIXER, "MZ100", DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{204, DeviceType::POOL, "MP100", DeviceFlags::EMS_DEVICE_FLAG_MP}, // pool
// Heat Pumps - 0x38? This is a thermostat like RC100H
// also prod-id of wifi module and wireless base
{252, DeviceType::HEATPUMP, "HP Module", DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Heat Pumps - 0x53
{248, DeviceType::HEATPUMP, "Hybrid Manager HM200", DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Ventilation - 0x51
{231, DeviceType::VENTILATION, "Logavent HRV176", DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Heatsource - 0x60
{228, DeviceType::HEATSOURCE, "AM200", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // alternative heatsource
// Connect devices - 0x02
{171, DeviceType::CONNECT, "OpenTherm Converter", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{205, DeviceType::CONNECT, "Moduline Easy Connect", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{206, DeviceType::CONNECT, "Easy Connect", DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Wireless sensor base - 0x50
{218, DeviceType::CONNECT, "M200, RFM200", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{236, DeviceType::CONNECT, "Wireless sensor base", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{238, DeviceType::CONNECT, "Wireless sensor base", DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Switches - 0x11
{ 71, DeviceType::SWITCH, "WM10", DeviceFlags::EMS_DEVICE_FLAG_NONE},
// EM10/100 extension module, pump module - 0x15
{ 243, DeviceType::EXTENSION, "EM10, EM100", DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Wireless outdoor sensor T1RF - 0x16
{ 220, DeviceType::EXTENSION, "T1RF", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x16
// EM10 error contact and analog flowtemp control- 0x12
{ 74, DeviceType::ALERT, "EM10", DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Gateways - 0x48
{17, DeviceType::GATEWAY, "MX400", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x48 and 0x4B
{189, DeviceType::GATEWAY, "KM200, MB LAN 2", DeviceFlags::EMS_DEVICE_FLAG_NONE},
{252, DeviceType::GATEWAY, "K30RF, MX300", DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Generic - 0x40 or other with no product-id and no version
{0, DeviceType::GENERIC, "unknown", DeviceFlags::EMS_DEVICE_FLAG_NONE}
#if defined(EMSESP_STANDALONE)
,
{100, DeviceType::WATER, "IPM", DeviceFlags::EMS_DEVICE_FLAG_IPM},
{102, DeviceType::WATER, "IPM2", DeviceFlags::EMS_DEVICE_FLAG_IPM},
{160, DeviceType::WATER, "MM100", DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{161, DeviceType::WATER, "MM200", DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{163, DeviceType::WATER, "SM100, MS100", DeviceFlags::EMS_DEVICE_FLAG_SM100},
{164, DeviceType::WATER, "SM200, MS200", DeviceFlags::EMS_DEVICE_FLAG_SM100},
{248, DeviceType::MIXER, "HM210", DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{157, DeviceType::THERMOSTAT, "RC120", DeviceFlags::EMS_DEVICE_FLAG_CR120}
#endif
// clang-format on

2177
src/core/emsdevice.cpp Normal file

File diff suppressed because it is too large Load Diff

542
src/core/emsdevice.h Normal file
View File

@@ -0,0 +1,542 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_EMSDEVICE_H_
#define EMSESP_EMSDEVICE_H_
#include "emsfactory.h"
#include "telegram.h"
#include "mqtt.h"
#include "helpers.h"
#include "emsdevicevalue.h"
namespace emsesp {
class EMSdevice {
public:
virtual ~EMSdevice() = default; // destructor of base class must always be virtual because it's a polymorphic class
using process_function_p = std::function<void(std::shared_ptr<const Telegram>)>;
// device_type defines which derived class to use, e.g. BOILER, THERMOSTAT etc..
EMSdevice(uint8_t device_type, uint8_t device_id, uint8_t product_id, const char * version, const char * default_name, uint8_t flags, uint8_t brand)
: device_type_(device_type)
, device_id_(device_id)
, product_id_(product_id)
, default_name_(default_name)
, flags_(flags)
, brand_(brand) {
strlcpy(version_, version, sizeof(version_));
}
// static functions, used outside the class like in console.cpp, command.cpp, emsesp.cpp, mqtt.cpp
static const char * device_type_2_device_name(const uint8_t device_type);
static uint8_t device_name_2_device_type(const char * topic);
static const char * tag_to_string(int8_t tag, const bool translate = true);
static const char * uom_to_string(uint8_t uom);
static const char * tag_to_mqtt(int8_t tag);
static uint8_t decode_brand(uint8_t value);
static bool export_values(uint8_t device_type, JsonObject output, const int8_t id, const uint8_t output_target);
// non static functions
const char * device_type_name(); // returns short non-translated device type name
const char * device_type_2_device_name_translated(); // returns translated device type name
bool has_tags(const int8_t tag) const;
bool has_cmd(const char * cmd, const int8_t id) const;
const char * brand_to_char();
std::string to_string();
std::string to_string_short();
std::string name(); // returns either default or custom name of a device (if defined)
bool is_device_id(uint8_t device_id) const {
return ((device_id & 0x7F) == (device_id_ & 0x7F));
}
// Getters
uint8_t device_id() const {
return device_id_;
}
uint8_t product_id() const {
return product_id_;
}
uint8_t device_type() const {
return device_type_; // see enum DeviceType below
}
const char * version() const {
return version_;
}
uint8_t brand() const {
return brand_;
}
void active(bool active) {
active_ = active;
}
const char * default_name() const {
return default_name_;
}
// flags
inline void add_flags(uint8_t flags) {
flags_ |= flags;
}
inline bool has_flags(uint8_t flags) const {
return (flags_ & flags) == flags;
}
inline void remove_flags(uint8_t flags) {
flags_ &= ~flags;
}
inline uint8_t flags() const {
return flags_;
}
// set custom device name
void custom_name(std::string const & custom_name) {
custom_name_ = custom_name;
}
std::string custom_name() const {
return custom_name_;
}
// set device model
void model(std::string const & model) {
model_ = model;
}
std::string model() const {
return model_;
}
inline uint8_t unique_id() const {
return unique_id_;
}
void unique_id(uint8_t unique_id) {
unique_id_ = unique_id;
}
bool has_update() const {
return has_update_;
}
void has_update(bool flag) {
has_update_ = flag;
}
void has_update(void * value) {
has_update_ = true;
publish_value(value);
}
void has_update(char * value, const char * newvalue, size_t len) {
if (strcmp(value, newvalue) != 0) {
strlcpy(value, newvalue, len);
has_update_ = true;
publish_value(value);
}
}
void has_update(uint8_t & value, uint8_t newvalue) {
if (value != newvalue) {
value = newvalue;
has_update_ = true;
publish_value((void *)&value);
}
}
void has_update(uint16_t & value, uint16_t newvalue) {
if (value != newvalue) {
value = newvalue;
has_update_ = true;
publish_value((void *)&value);
}
}
void has_update(int16_t & value, int16_t newvalue) {
if (value != newvalue) {
value = newvalue;
has_update_ = true;
publish_value((void *)&value);
}
}
void has_update(uint32_t & value, uint32_t newvalue) {
if (value != newvalue) {
value = newvalue;
has_update_ = true;
publish_value((void *)&value);
}
}
void has_enumupdate(std::shared_ptr<const Telegram> telegram, uint8_t & value, const uint8_t index, int8_t s = 0) {
if (telegram->read_enumvalue(value, index, s)) {
has_update_ = true;
publish_value((void *)&value);
}
}
template <typename Value>
void has_update(std::shared_ptr<const Telegram> telegram, Value & value, const uint8_t index, uint8_t s = 0) {
if (telegram->read_value(value, index, s)) {
has_update_ = true;
publish_value((void *)&value);
}
}
template <typename BitValue>
void has_bitupdate(std::shared_ptr<const Telegram> telegram, BitValue & value, const uint8_t index, uint8_t b) {
if (telegram->read_bitvalue(value, index, b)) {
has_update_ = true;
publish_value((void *)&value);
}
}
// modbus
int get_modbus_value(uint8_t tag, const std::string & shortname, std::vector<uint16_t> & result);
int modbus_value_to_json(uint8_t tag, const std::string & shortname, const std::vector<uint8_t> & modbus_data, JsonObject jsonValue);
enum Handlers : uint8_t { ALL, RECEIVED, FETCHED, PENDING, IGNORED };
void show_telegram_handlers(uuid::console::Shell & shell) const;
char * show_telegram_handlers(char * result, const size_t len, const uint8_t handlers);
void show_mqtt_handlers(uuid::console::Shell & shell) const;
void add_handlers_ignored(const uint16_t handler);
void set_climate_minmax(int8_t tag, int16_t min, uint32_t max);
void setValueEnum(const void * value_p, const char * const ** options);
void setCustomizationEntity(const std::string & entity_id);
void getCustomizationEntities(std::vector<std::string> & entity_ids);
void register_telegram_type(const uint16_t telegram_type_id, const char * telegram_type_name, bool fetch, const process_function_p cb);
bool handle_telegram(std::shared_ptr<const Telegram> telegram);
std::string get_value_uom(const std::string & shortname) const;
bool get_value_info(JsonObject root, const char * cmd, const int8_t id);
void get_value_json(JsonObject output, DeviceValue & dv);
void get_dv_info(JsonObject json);
enum OUTPUT_TARGET : uint8_t { API_VERBOSE, API_SHORTNAMES, MQTT, CONSOLE };
bool generate_values(JsonObject output, const int8_t tag_filter, const bool nested, const uint8_t output_target);
void generate_values_web(JsonObject output, const bool is_dashboard = false);
void generate_values_web_customization(JsonArray output);
void add_device_value(int8_t tag,
void * value_p,
uint8_t type,
const char * const ** options,
const char * const * options_single,
int8_t numeric_operator,
const char * const * name,
uint8_t uom,
const cmd_function_p f,
int16_t min,
uint32_t max);
void register_device_value(int8_t tag,
void * value_p,
uint8_t type,
const char * const ** options,
const char * const * name,
uint8_t uom,
const cmd_function_p f,
int16_t min,
uint32_t max);
void
register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const ** options, const char * const * name, uint8_t uom, const cmd_function_p f);
void register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const ** options, const char * const * name, uint8_t uom);
void register_device_value(int8_t tag,
void * value_p,
uint8_t type,
int8_t numeric_operator,
const char * const * name,
uint8_t uom,
const cmd_function_p f = nullptr);
void register_device_value(int8_t tag,
void * value_p,
uint8_t type,
int8_t numeric_operator,
const char * const * name,
uint8_t uom,
const cmd_function_p f,
int16_t min,
uint32_t max);
// single list of options
void register_device_value(int8_t tag,
void * value_p,
uint8_t type,
const char * const * options_single,
const char * const * name,
uint8_t uom,
const cmd_function_p f = nullptr);
// single list of options, with no translations, with min and max
void register_device_value(int8_t tag,
void * value_p,
uint8_t type,
const char * const * options_single,
const char * const * name,
uint8_t uom,
const cmd_function_p f,
int16_t min,
uint32_t max);
// no options, optional function f
void register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const * name, uint8_t uom, const cmd_function_p f = nullptr);
// no options, with min/max
void register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const * name, uint8_t uom, const cmd_function_p f, int16_t min, uint32_t max);
void write_command(const uint16_t type_id, const uint8_t offset, uint8_t * message_data, const uint8_t message_length, const uint16_t validate_typeid) const;
void write_command(const uint16_t type_id, const uint8_t offset, const uint8_t value, const uint16_t validate_typeid) const;
void write_command(const uint16_t type_id, const uint8_t offset, const uint8_t value) const;
void read_command(const uint16_t type_id, uint8_t offset = 0, uint8_t length = 0) const;
bool is_readable(const void * value_p) const;
bool is_readonly(const std::string & cmd, const int8_t id) const;
bool has_command(const void * value_p) const;
void set_minmax(const void * value_p, int16_t min, uint32_t max);
void publish_value(void * value_p) const;
void publish_all_values();
void mqtt_ha_entity_config_create();
const char * telegram_type_name(std::shared_ptr<const Telegram> telegram);
void fetch_values();
void toggle_fetch(uint16_t telegram_id, bool toggle);
bool is_fetch(uint16_t telegram_id) const;
bool is_received(uint16_t telegram_id) const;
bool has_telegram_id(uint16_t id) const;
void ha_config_clear();
bool ha_config_done() const {
return ha_config_done_;
}
void ha_config_done(const bool v) {
ha_config_done_ = v;
}
enum Brand : uint8_t {
NO_BRAND = 0, // 0
BOSCH, // 1
JUNKERS, // 2
BUDERUS, // 3
NEFIT, // 4
SIEGER, // 5
WORCESTER, // 11
IVT // 13
};
// Unique Identifiers for each Device type, used in Dashboard table
// 100 and above is reserved for DeviceType
enum DeviceTypeUniqueID : uint8_t {
SCHEDULER_UID = 96,
ANALOGSENSOR_UID = 97,
TEMPERATURESENSOR_UID = 98,
CUSTOM_UID = 99 // always 99
};
enum DeviceType : uint8_t {
SYSTEM = 0, // this is us (EMS-ESP)
TEMPERATURESENSOR, // for internal temperature sensors
ANALOGSENSOR, // for internal analog sensors
SCHEDULER, // for internal schedule
CUSTOM, // for user defined entities
BOILER, // from here on enum the ems-devices
THERMOSTAT,
MIXER,
SOLAR,
HEATPUMP,
GATEWAY,
SWITCH,
CONTROLLER,
CONNECT,
ALERT,
EXTENSION,
GENERIC,
HEATSOURCE,
VENTILATION,
WATER,
POOL,
UNKNOWN
};
static constexpr uint8_t EMS_DEVICES_MAX_TELEGRAMS = 20;
// static device IDs
static constexpr uint8_t EMS_DEVICE_ID_BOILER = 0x08; // fixed device_id for Master Boiler/UBA
static constexpr uint8_t EMS_DEVICE_ID_HS1 = 0x70; // fixed device_id for 1st. Cascade Boiler/UBA
static constexpr uint8_t EMS_DEVICE_ID_HS16 = 0x7F; // fixed device_id for last Cascade Boiler/UBA
static constexpr uint8_t EMS_DEVICE_ID_AHS1 = 0x60; // fixed device_id for alternative Heating AM200
static constexpr uint8_t EMS_DEVICE_ID_CONTROLLER = 0x09;
static constexpr uint8_t EMS_DEVICE_ID_RS232 = 0x04;
static constexpr uint8_t EMS_DEVICE_ID_TERMINAL = 0x0A;
static constexpr uint8_t EMS_DEVICE_ID_SERVICEKEY = 0x0B;
static constexpr uint8_t EMS_DEVICE_ID_CASCADE = 0x0C;
static constexpr uint8_t EMS_DEVICE_ID_EASYCOM = 0x0D;
static constexpr uint8_t EMS_DEVICE_ID_CONVERTER = 0x0E;
static constexpr uint8_t EMS_DEVICE_ID_CLOCK = 0x0F;
static constexpr uint8_t EMS_DEVICE_ID_SWITCH = 0x11; // Switch WM10
static constexpr uint8_t EMS_DEVICE_ID_ALERT = 0x12; // Error module EM10
static constexpr uint8_t EMS_DEVICE_ID_EXTENSION = 0x15; // Extension module EM1000, Pump module PM10
static constexpr uint8_t EMS_DEVICE_ID_MODEM = 0x48;
static constexpr uint8_t EMS_DEVICE_ID_RFSENSOR = 0x40; // RF sensor only sending, no reply
static constexpr uint8_t EMS_DEVICE_ID_RFBASE = 0x50;
static constexpr uint8_t EMS_DEVICE_ID_ROOMTHERMOSTAT = 0x17; // TADO using this with no version reply #174
static constexpr uint8_t EMS_DEVICE_ID_TADO_OLD = 0x19; // older TADO using this with no version reply, #1031
static constexpr uint8_t EMS_DEVICE_ID_MIXER1 = 0x20; // e.g MH210 module as mixer
static constexpr uint8_t EMS_DEVICE_ID_MIXER8 = 0x27;
static constexpr uint8_t EMS_DEVICE_ID_DHW1 = 0x28; // MM100 module as water station
static constexpr uint8_t EMS_DEVICE_ID_DHW2 = 0x29; // MM100 module as water station
static constexpr uint8_t EMS_DEVICE_ID_DHW8 = 0x2F; // last DHW module id?
// generic type IDs
static constexpr uint16_t EMS_TYPE_NAME = 0x01; // device config for ems devices, name ascii on offset 27ff for ems+
static constexpr uint16_t EMS_TYPE_VERSION = 0x02; // type ID for Version information. Generic across all EMS devices.
static constexpr uint16_t EMS_TYPE_UBADevices = 0x07; // EMS connected devices
static constexpr uint16_t EMS_TYPE_DEVICEERROR = 0xBE;
static constexpr uint16_t EMS_TYPE_SYSTEMERROR = 0xBF;
static constexpr uint16_t EMS_TYPE_MENUCONFIG = 0xF7;
static constexpr uint16_t EMS_TYPE_VALUECONFIG = 0xF9;
// device flags: The lower 4 bits hold the unique identifier, the upper 4 bits are used for specific flags
static constexpr uint8_t EMS_DEVICE_FLAG_NONE = 0;
// Controller
static constexpr uint8_t EMS_DEVICE_FLAG_IVT = 1;
// Boiler
static constexpr uint8_t EMS_DEVICE_FLAG_EMS = 1;
static constexpr uint8_t EMS_DEVICE_FLAG_EMSPLUS = 2;
static constexpr uint8_t EMS_DEVICE_FLAG_HT3 = 3;
static constexpr uint8_t EMS_DEVICE_FLAG_HEATPUMP = 4;
static constexpr uint8_t EMS_DEVICE_FLAG_HYBRID = 5;
static constexpr uint8_t EMS_DEVICE_FLAG_HIU = 6;
// Solar Module
static constexpr uint8_t EMS_DEVICE_FLAG_SM10 = 1;
static constexpr uint8_t EMS_DEVICE_FLAG_SM100 = 2;
static constexpr uint8_t EMS_DEVICE_FLAG_ISM = 3;
// Mixer Module
static constexpr uint8_t EMS_DEVICE_FLAG_MMPLUS = 1;
static constexpr uint8_t EMS_DEVICE_FLAG_MM10 = 2;
static constexpr uint8_t EMS_DEVICE_FLAG_IPM = 3;
static constexpr uint8_t EMS_DEVICE_FLAG_MP = 4;
// Thermostats
static constexpr uint8_t EMS_DEVICE_FLAG_NO_WRITE = (1 << 7); // last bit
static constexpr uint8_t EMS_DEVICE_FLAG_JUNKERS_OLD = (1 << 6); // 6th bit set if older models, like FR120, FR100
static constexpr uint8_t EMS_DEVICE_FLAG_EASY = 1;
static constexpr uint8_t EMS_DEVICE_FLAG_RC10 = 2;
static constexpr uint8_t EMS_DEVICE_FLAG_RC20 = 3;
static constexpr uint8_t EMS_DEVICE_FLAG_RC20_N = 4; // Variation on RC20, Older, like ES72
static constexpr uint8_t EMS_DEVICE_FLAG_RC25 = 5;
static constexpr uint8_t EMS_DEVICE_FLAG_RC30_N = 6; // variation on RC30, Newer models
static constexpr uint8_t EMS_DEVICE_FLAG_RC30 = 7;
static constexpr uint8_t EMS_DEVICE_FLAG_RC35 = 8;
static constexpr uint8_t EMS_DEVICE_FLAG_RC300 = 9;
static constexpr uint8_t EMS_DEVICE_FLAG_RC100 = 10;
static constexpr uint8_t EMS_DEVICE_FLAG_JUNKERS = 11;
static constexpr uint8_t EMS_DEVICE_FLAG_CRF = 12; // CRF200 only monitor
static constexpr uint8_t EMS_DEVICE_FLAG_RC100H = 13; // with humidity
static constexpr uint8_t EMS_DEVICE_FLAG_BC400 = 14; // mostly like RC300, but some changes
static constexpr uint8_t EMS_DEVICE_FLAG_R3000 = 15; // Rego3000, same as RC300 with different wwmodes
static constexpr uint8_t EMS_DEVICE_FLAG_CR120 = 16; // mostly like RC300, but some changes
static constexpr uint8_t EMS_DEVICE_FLAG_CR11 = 17; // CRF200 only monitor
uint8_t count_entities();
uint8_t count_entities_fav();
bool has_entities() const;
// void reserve_device_values(uint8_t elements) {
// devicevalues_.reserve(elements);
// }
// void reserve_telegram_functions(uint8_t elements) {
// telegram_functions_.reserve(elements);
// }
#if defined(EMSESP_STANDALONE)
struct TelegramFunctionDump {
uint16_t type_id_;
const char * name_;
bool fetch_;
TelegramFunctionDump(uint16_t type_id, const char * name, bool fetch)
: type_id_(type_id)
, name_(name)
, fetch_(fetch) {
}
};
void dump_telegram_info(std::vector<TelegramFunctionDump> & telegram_functions_dump);
void dump_devicevalue_info();
#endif
private:
uint8_t unique_id_;
uint8_t device_type_ = DeviceType::SYSTEM;
uint8_t device_id_ = 0;
uint8_t product_id_ = 0;
char version_[6];
const char * default_name_; // the fixed name the EMS model taken from the device library
std::string custom_name_ = ""; // custom name
std::string model_ = ""; // model, taken from the 0x01 telegram. see process_deviceName()
uint8_t flags_ = 0;
uint8_t brand_ = Brand::NO_BRAND;
bool active_ = true;
bool ha_config_done_ = false;
bool has_update_ = false;
struct TelegramFunction {
const uint16_t telegram_type_id_; // it's type_id
const char * telegram_type_name_; // e.g. RC20Message
bool fetch_; // if this type_id be queried automatically
bool received_;
const process_function_p process_function_;
TelegramFunction(uint16_t telegram_type_id, const char * telegram_type_name, bool fetch, bool received, const process_function_p process_function)
: telegram_type_id_(telegram_type_id)
, telegram_type_name_(telegram_type_name)
, fetch_(fetch)
, received_(received)
, process_function_(process_function) {
}
};
std::vector<uint16_t> handlers_ignored_;
#if defined(EMSESP_STANDALONE) || defined(EMSESP_TEST)
public: // so we can call it from WebCustomizationService::test() and EMSESP::dump_all_entities()
#endif
std::vector<TelegramFunction> telegram_functions_; // each EMS device has its own set of registered telegram types
std::vector<DeviceValue> devicevalues_; // all the device values
};
} // namespace emsesp
#endif

382
src/core/emsdevicevalue.cpp Normal file
View File

@@ -0,0 +1,382 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "emsdevicevalue.h"
#include "emsesp.h"
namespace emsesp {
// constructor
DeviceValue::DeviceValue(uint8_t device_type,
int8_t tag,
void * value_p,
uint8_t type,
const char * const ** options,
const char * const * options_single,
int8_t numeric_operator,
const char * const short_name,
const char * const * fullname,
std::string & custom_fullname,
uint8_t uom,
bool has_cmd,
int16_t min,
uint32_t max,
uint8_t state)
: device_type(device_type)
, tag(tag)
, value_p(value_p)
, type(type)
, options(options)
, options_single(options_single)
, numeric_operator(numeric_operator)
, short_name(short_name)
, fullname(fullname)
, custom_fullname(custom_fullname)
, uom(uom)
, has_cmd(has_cmd)
, min(min)
, max(max)
, state(state) {
// calculate #options in options list
if (options_single) {
options_size = 1;
} else {
options_size = Helpers::count_items(options);
}
// set the min/max
set_custom_minmax();
/*
#ifdef EMSESP_STANDALONE
// only added for debugging
Serial.print(COLOR_BRIGHT_RED_BACKGROUND);
Serial.print(" registering entity: ");
Serial.print((short_name));
Serial.print("/");
if (!custom_fullname.empty()) {
Serial.print(COLOR_BRIGHT_CYAN);
Serial.print(custom_fullname.c_str());
Serial.print(COLOR_RESET);
} else {
Serial.print(Helpers::translated_word(fullname));
}
Serial.print(" (#options=");
Serial.print(options_size);
Serial.print(",numop=");
Serial.print(numeric_operator);
Serial.print(") ");
if (options != nullptr) {
uint8_t i = 0;
while (i < options_size) {
Serial.print(" option");
Serial.print(i + 1);
Serial.print(":");
auto str = Helpers::translated_word(options[i]);
Serial.print(str);
i++;
}
} else if (options_single != nullptr) {
Serial.print("option1:!");
Serial.print((options_single[0]));
Serial.print("!");
}
Serial.println(COLOR_RESET);
#endif
*/
}
// mapping of UOM, to match order in DeviceValueUOM enum emsdevicevalue.h
// also maps to DeviceValueUOM in interface/src/project/types.ts for the Web UI
// must be an int of 4 bytes, 32bit aligned
const char * DeviceValue::DeviceValueUOM_s[] = {
F_(uom_blank), // 0
F_(uom_degrees), F_(uom_degrees), F_(uom_percent), F_(uom_lmin), F_(uom_kwh), F_(uom_wh), FL_(hours)[0],
FL_(minutes)[0], F_(uom_ua), F_(uom_bar), F_(uom_kw), F_(uom_w), F_(uom_kb), FL_(seconds)[0],
F_(uom_dbm), F_(uom_fahrenheit), F_(uom_mv), F_(uom_sqm), F_(uom_m3), F_(uom_l), F_(uom_kmin),
F_(uom_k), F_(uom_volts), F_(uom_mbar), F_(uom_lh), F_(uom_ctkwh), F_(uom_blank)
};
// mapping of TAGs, to match order in DeviceValueTAG enum in emsdevicevalue.h
const char * const * DeviceValue::DeviceValueTAG_s[] = {
FL_(tag_device_data), // ""
FL_(tag_hc1), // "hc1"
FL_(tag_hc2), // "hc2"
FL_(tag_hc3), // "hc3"
FL_(tag_hc4), // "hc4"
FL_(tag_hc5), // "hc5"
FL_(tag_hc6), // "hc6"
FL_(tag_hc7), // "hc7"
FL_(tag_hc8), // "hc8"
FL_(tag_dhw1), // "dhw"
FL_(tag_dhw2), // "dhw2"
FL_(tag_dhw3), // "dhw3"
FL_(tag_dhw4), // "dhw4"
FL_(tag_dhw5), // "dhw5"
FL_(tag_dhw6), // "dhw6"
FL_(tag_dhw7), // "dhw7"
FL_(tag_dhw8), // "dhw8"
FL_(tag_dhw9), // "dhw9"
FL_(tag_dhw10), // "dhw10"
FL_(tag_ahs1), // "ahs1"
FL_(tag_hs1), // "hs1"
FL_(tag_hs2), // "hs2"
FL_(tag_hs3), // "hs3"
FL_(tag_hs4), // "hs4"
FL_(tag_hs5), // "hs5"
FL_(tag_hs6), // "hs6"
FL_(tag_hs7), // "hs7"
FL_(tag_hs8), // "hs8"
FL_(tag_hs9), // "hs9"
FL_(tag_hs10), // "hs10"
FL_(tag_hs11), // "hs11"
FL_(tag_hs12), // "hs12"
FL_(tag_hs13), // "hs13"
FL_(tag_hs14), // "hs14"
FL_(tag_hs15), // "hs15"
FL_(tag_hs16) // "hs16"
};
// tags used in MQTT topic names. Matches sequence from DeviceValueTAG_s
const char * const DeviceValue::DeviceValueTAG_mqtt[] = {
FL_(tag_device_data)[0], // ""
FL_(tag_hc1)[0], // "hc1"
FL_(tag_hc2)[0], // "hc2"
FL_(tag_hc3)[0], // "hc3"
FL_(tag_hc4)[0], // "hc4"
FL_(tag_hc5)[0], // "hc5"
FL_(tag_hc6)[0], // "hc6"
FL_(tag_hc7)[0], // "hc7"
FL_(tag_hc8)[0], // "hc8"
FL_(tag_dhw1)[0], // "dhw"
FL_(tag_dhw2)[0], // "dhw2"
FL_(tag_dhw3)[0], // "dhw3"
FL_(tag_dhw4)[0], // "dhw4"
FL_(tag_dhw5)[0], // "dhw5"
FL_(tag_dhw6)[0], // "dhw6"
FL_(tag_dhw7)[0], // "dhw7"
FL_(tag_dhw8)[0], // "dhw8"
FL_(tag_dhw9)[0], // "dhw9"
FL_(tag_dhw10)[0], // "dhw10"
FL_(tag_ahs1)[0], // "ahs1"
FL_(tag_hs1)[0], // "hs1"
FL_(tag_hs2)[0], // "hs2"
FL_(tag_hs3)[0], // "hs3"
FL_(tag_hs4)[0], // "hs4"
FL_(tag_hs5)[0], // "hs5"
FL_(tag_hs6)[0], // "hs6"
FL_(tag_hs7)[0], // "hs7"
FL_(tag_hs8)[0], // "hs8"
FL_(tag_hs9)[0], // "hs9"
FL_(tag_hs10)[0], // "hs10"
FL_(tag_hs11)[0], // "hs11"
FL_(tag_hs12)[0], // "hs12"
FL_(tag_hs13)[0], // "hs13"
FL_(tag_hs14)[0], // "hs14"
FL_(tag_hs15)[0], // "hs15"
FL_(tag_hs16)[0] // "hs16"
};
// count #tags once at compile time
uint8_t DeviceValue::NUM_TAGS = sizeof(DeviceValue::DeviceValueTAG_s) / sizeof(char * const *);
// checks whether the device value has an actual value
// returns true if its valid
// state is stored in the dv object
bool DeviceValue::hasValue() const {
bool has_value = false;
switch (type) {
case DeviceValueType::BOOL:
has_value = Helpers::hasValue(*(uint8_t *)(value_p), EMS_VALUE_BOOL);
break;
case DeviceValueType::STRING:
has_value = Helpers::hasValue((char *)(value_p));
break;
case DeviceValueType::ENUM:
has_value = Helpers::hasValue(*(uint8_t *)(value_p));
break;
case DeviceValueType::INT8:
has_value = Helpers::hasValue(*(int8_t *)(value_p));
break;
case DeviceValueType::UINT8:
has_value = Helpers::hasValue(*(uint8_t *)(value_p));
break;
case DeviceValueType::INT16:
has_value = Helpers::hasValue(*(int16_t *)(value_p));
break;
case DeviceValueType::UINT16:
has_value = Helpers::hasValue(*(uint16_t *)(value_p));
break;
case DeviceValueType::UINT24:
case DeviceValueType::TIME:
case DeviceValueType::UINT32:
has_value = Helpers::hasValue(*(uint32_t *)(value_p));
break;
case DeviceValueType::CMD:
has_value = true; // we count command as an actual entity
break;
default:
break;
}
return has_value;
}
// See if the device value has a tag and it's not empty
bool DeviceValue::has_tag() const {
return ((tag < DeviceValue::NUM_TAGS) && (tag != TAG_NONE) && strlen(DeviceValueTAG_s[tag][0]));
}
// set the min and max value for a device value
// converts to signed int, which means rounding to an whole integer
// returns false if there is no min/max needed
// Types BOOL, ENUM, STRING and CMD are not used
bool DeviceValue::get_min_max(int16_t & dv_set_min, uint32_t & dv_set_max) {
uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (uom == DeviceValueUOM::DEGREES) ? 2 : (uom == DeviceValueUOM::DEGREES_R) ? 1 : 0;
// if we have individual limits set already, just do the conversion
// limits are not scaled with num operator and temperatures are °C
if (min != 0 || max != 0) {
dv_set_min = Helpers::transformNumFloat(min, 0, fahrenheit);
dv_set_max = Helpers::transformNumFloat(max, 0, fahrenheit);
return true;
}
// init default values to 0 and 0
dv_set_min = 0;
dv_set_max = 0;
if (type == DeviceValueType::UINT16) {
dv_set_min = Helpers::transformNumFloat(0, numeric_operator, fahrenheit);
dv_set_max = Helpers::transformNumFloat(EMS_VALUE_UINT16_NOTSET - 1, numeric_operator, fahrenheit);
return true;
}
if (type == DeviceValueType::INT16) {
dv_set_min = Helpers::transformNumFloat(-EMS_VALUE_INT16_NOTSET + 1, numeric_operator, fahrenheit);
dv_set_max = Helpers::transformNumFloat(EMS_VALUE_INT16_NOTSET - 1, numeric_operator, fahrenheit);
return true;
}
if (type == DeviceValueType::UINT8) {
if (uom == DeviceValueUOM::PERCENT) {
dv_set_max = 100;
} else {
dv_set_max = Helpers::transformNumFloat(EMS_VALUE_UINT8_NOTSET - 1, numeric_operator, fahrenheit);
}
return true;
}
if (type == DeviceValueType::INT8) {
if (uom == DeviceValueUOM::PERCENT) {
dv_set_min = -100;
dv_set_max = 100;
} else {
dv_set_min = Helpers::transformNumFloat(-EMS_VALUE_INT8_NOTSET + 1, numeric_operator, fahrenheit);
dv_set_max = Helpers::transformNumFloat(EMS_VALUE_INT8_NOTSET - 1, numeric_operator, fahrenheit);
}
return true;
}
if (type == DeviceValueType::UINT24 || type == DeviceValueType::TIME || type == DeviceValueType::UINT32) {
dv_set_max = Helpers::transformNumFloat(EMS_VALUE_UINT24_NOTSET - 1, numeric_operator);
return true;
}
return false; // nothing changed, not supported
}
// extract custom min from custom_fullname
bool DeviceValue::get_custom_min(int16_t & val) {
auto min_pos = custom_fullname.find('>');
bool has_min = (min_pos != std::string::npos);
uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (uom == DeviceValueUOM::DEGREES) ? 2 : (uom == DeviceValueUOM::DEGREES_R) ? 1 : 0;
if (has_min) {
int32_t v = Helpers::atoint(custom_fullname.substr(min_pos + 1).c_str());
if (fahrenheit) {
v = (v - (32 * (fahrenheit - 1))) / 1.8; // reset to °C
}
if (max > 0 && v > 0 && (uint32_t)v > max) {
return false;
}
val = v;
}
return has_min;
}
// extract custom max from custom_fullname
bool DeviceValue::get_custom_max(uint32_t & val) {
auto max_pos = custom_fullname.find('<');
bool has_max = (max_pos != std::string::npos);
uint8_t fahrenheit = !EMSESP::system_.fahrenheit() ? 0 : (uom == DeviceValueUOM::DEGREES) ? 2 : (uom == DeviceValueUOM::DEGREES_R) ? 1 : 0;
if (has_max) {
int32_t v = Helpers::atoint(custom_fullname.substr(max_pos + 1).c_str());
if (fahrenheit) {
v = (v - (32 * (fahrenheit - 1))) / 1.8; // reset to °C
}
if (v < 0 || v < (int32_t)min) {
return false;
}
val = v;
}
return has_max;
}
// sets min max to stored custom values (if set)
void DeviceValue::set_custom_minmax() {
get_custom_min(min);
get_custom_max(max);
}
std::string DeviceValue::get_custom_fullname() const {
auto min_pos = custom_fullname.find('>');
auto max_pos = custom_fullname.find('<');
auto minmax_pos = min_pos < max_pos ? min_pos : max_pos;
if (minmax_pos != std::string::npos) {
return custom_fullname.substr(0, minmax_pos);
}
return custom_fullname;
}
// returns the translated fullname or the custom fullname (if provided)
// always returns a std::string
std::string DeviceValue::get_fullname() const {
std::string customname = get_custom_fullname();
if (customname.empty()) {
return Helpers::translated_word(fullname);
}
return customname;
}
// returns any custom name defined in the entity_id
std::string DeviceValue::get_name(const std::string & entity) {
auto pos = entity.find('|');
if (pos != std::string::npos) {
return entity.substr(2, pos - 2);
}
return entity.substr(2);
}
} // namespace emsesp

219
src/core/emsdevicevalue.h Normal file
View File

@@ -0,0 +1,219 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_EMSDEVICEVALUE_H_
#define EMSESP_EMSDEVICEVALUE_H_
#include <Arduino.h>
#include <ArduinoJson.h>
#include "helpers.h" // for conversions
#include "default_settings.h" // for enum types
namespace emsesp {
// DeviceValue holds the information for a device entity
class DeviceValue {
public:
enum DeviceValueType : uint8_t {
BOOL,
INT8,
UINT8,
INT16,
UINT16,
UINT24,
TIME, // same as ULONG (32 bits)
UINT32,
ENUM,
STRING,
CMD // special for commands only
};
// Unit Of Measurement mapping - maps to DeviceValueUOM_s in emsdevicevalue.cpp. Sequence is important!!
// also used with HA as uom
enum DeviceValueUOM : uint8_t {
NONE = 0, // 0
DEGREES, // 1 - °C
DEGREES_R, // 2 - °C (relative temperature)
PERCENT, // 3 - %
LMIN, // 4 - l/min
KWH, // 5 - kWh
WH, // 6 - Wh
HOURS, // 7 - h
MINUTES, // 8 - m
UA, // 9 - µA
BAR, // 10 - bar
KW, // 11 - kW
W, // 12 - W
KB, // 13 - kB
SECONDS, // 14 - s
DBM, // 15 - dBm
FAHRENHEIT, // 16 - °F
MV, // 17 - mV
SQM, // 18 - m²
M3, // 19 - m³
L, // 20 - L
KMIN, // 21 - K*min
K, // 22 - K
VOLTS, // 23 - V
MBAR, // 24 - mbar
LH, // 25 - l/h
CTKWH, // 26 - ct/kWh
CONNECTIVITY // 27 - used in HA
};
// TAG mapping - maps to DeviceValueTAG_s in emsdevicevalue.cpp
enum DeviceValueTAG : int8_t {
TAG_NONE = -1, // wild card
TAG_DEVICE_DATA = 0,
TAG_HC1,
TAG_HC2,
TAG_HC3,
TAG_HC4,
TAG_HC5,
TAG_HC6,
TAG_HC7,
TAG_HC8,
TAG_DHW1,
TAG_DHW2,
TAG_DHW3,
TAG_DHW4,
TAG_DHW5,
TAG_DHW6,
TAG_DHW7,
TAG_DHW8,
TAG_DHW9,
TAG_DHW10,
TAG_AHS1,
TAG_HS1,
TAG_HS2,
TAG_HS3,
TAG_HS4,
TAG_HS5,
TAG_HS6,
TAG_HS7,
TAG_HS8,
TAG_HS9,
TAG_HS10,
TAG_HS11,
TAG_HS12,
TAG_HS13,
TAG_HS14,
TAG_HS15,
TAG_HS16
};
// states of a device value
enum DeviceValueState : uint8_t {
// low nibble active state of the device value
DV_DEFAULT = 0, // 0 - does not yet have a value
DV_ACTIVE = (1 << 0), // 1 - has a validated real value
DV_HA_CONFIG_CREATED = (1 << 1), // 2 - set if the HA config topic has been created
DV_HA_CLIMATE_NO_RT = (1 << 2), // 4 - climate created without roomTemp
// high nibble as mask for exclusions & special functions
DV_WEB_EXCLUDE = (1 << 4), // 16 - not shown on web
DV_API_MQTT_EXCLUDE = (1 << 5), // 32 - not shown on mqtt, API
DV_READONLY = (1 << 6), // 64 - read only
DV_FAVORITE = (1 << 7) // 128 - marked as a favorite
};
// numeric operators
// negative numbers used for multipliers
enum DeviceValueNumOp : int8_t {
DV_NUMOP_NONE = 0, // default
DV_NUMOP_DIV2 = 2,
DV_NUMOP_DIV10 = 10,
DV_NUMOP_DIV60 = 60,
DV_NUMOP_DIV100 = 100,
DV_NUMOP_MUL5 = -5,
DV_NUMOP_MUL10 = -10,
DV_NUMOP_MUL15 = -15,
DV_NUMOP_MUL50 = -50
};
uint8_t device_type; // EMSdevice::DeviceType
int8_t tag; // DeviceValueTAG::*
void * value_p; // pointer to variable of any type
uint8_t type; // DeviceValueType::*
const char * const ** options; // options as a flash char array
const char * const * options_single; // options are not translated
int8_t numeric_operator;
const char * const short_name; // used in MQTT and API
const char * const * fullname; // used in Web and Console, is translated
std::string custom_fullname; // optional, from customization
uint8_t uom; // DeviceValueUOM::*
bool has_cmd; // true if there is a Console/MQTT command which matches the short_name
int16_t min; // min range
uint32_t max; // max range
uint8_t state; // DeviceValueState::*
uint8_t options_size; // number of options in the char array, calculated at class initialization
DeviceValue(uint8_t device_type, // EMSdevice::DeviceType
int8_t tag, // DeviceValueTAG::*
void * value_p, // pointer to variable of any type
uint8_t type, // DeviceValueType::*
const char * const ** options, // options as a flash char array
const char * const * options_single, // options are not translated
int8_t numeric_operator,
const char * const short_name, // used in MQTT and API
const char * const * fullname, // used in Web and Console, is translated
std::string & custom_fullname, // optional, from customization
uint8_t uom, // DeviceValueUOM::*
bool has_cmd, // true if there is a Console/MQTT command which matches the short_name
int16_t min, // min range
uint32_t max, // max range
uint8_t state // DeviceValueState::* (also known as the mask)
);
bool hasValue() const;
bool has_tag() const;
bool get_min_max(int16_t & dv_set_min, uint32_t & dv_set_max);
void set_custom_minmax();
bool get_custom_min(int16_t & val);
bool get_custom_max(uint32_t & val);
std::string get_custom_fullname() const;
std::string get_fullname() const;
static std::string get_name(const std::string & entity);
// dv state flags
void add_state(uint8_t s) {
state |= s;
}
bool has_state(uint8_t s) const {
return (state & s) == s;
}
void remove_state(uint8_t s) {
state &= ~s;
}
uint8_t get_state() const {
return state;
}
static const char * DeviceValueUOM_s[];
static const char * const * DeviceValueTAG_s[];
static const char * const DeviceValueTAG_mqtt[];
static uint8_t NUM_TAGS; // # tags
};
}; // namespace emsesp
#endif

1797
src/core/emsesp.cpp Normal file

File diff suppressed because it is too large Load Diff

304
src/core/emsesp.h Normal file
View File

@@ -0,0 +1,304 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_EMSESP_H
#define EMSESP_EMSESP_H
#include <Arduino.h>
#include <vector>
#include <string>
#include <functional>
#include <deque>
#include <unordered_map>
#include <list>
#include <ArduinoJson.h>
#include <uuid/common.h>
#include <uuid/console.h>
#include <uuid/log.h>
#ifndef EMSESP_STANDALONE
#include <uuid/telnet.h>
#endif
#ifndef EMSESP_STANDALONE
#include "ESP32React/ESP32React.h"
#else
#include "../lib_standalone/ESP32React.h"
#endif
#include <Preferences.h>
#include "../web/WebStatusService.h"
#include "../web/WebActivityService.h"
#include "../web/WebDataService.h"
#include "../web/WebSettingsService.h"
#include "../web/WebCustomizationService.h"
#include "../web/WebSchedulerService.h"
#include "../web/WebAPIService.h"
#include "../web/WebLogService.h"
#include "../web/WebCustomEntityService.h"
#include "../web/WebModulesService.h"
#include "emsdevicevalue.h"
#include "emsdevice.h"
#include "emsfactory.h"
#include "telegram.h"
#include "mqtt.h"
#include "modbus.h"
#include "system.h"
#include "temperaturesensor.h"
#include "analogsensor.h"
#include "console.h"
#include "console_stream.h"
#include "shower.h"
#include "roomcontrol.h"
#include "command.h"
#include "../version.h"
// Load external modules
class Module {}; // forward declaration
#include <ModuleLibrary.h>
#define WATCH_ID_NONE 0 // no watch id set
// helpers for callback functions
#define MAKE_PF_CB(__f) [&](std::shared_ptr<const Telegram> t) { __f(t); } // for Process Function callbacks to EMSDevice::process_function_p
#define MAKE_CF_CB(__f) [&](const char * value, const int8_t id) { return __f(value, id); } // for Command Function callbacks Command::cmd_function_p
namespace emsesp {
using DeviceValueUOM = DeviceValue::DeviceValueUOM;
using DeviceValueType = DeviceValue::DeviceValueType;
using DeviceValueState = DeviceValue::DeviceValueState;
using DeviceValueTAG = DeviceValue::DeviceValueTAG;
using DeviceValueNumOp = DeviceValue::DeviceValueNumOp;
// forward declarations for compiler
class EMSESPShell;
class Shower;
class EMSESP {
public:
EMSESP();
~EMSESP() = default;
virtual void start();
virtual void loop();
static uuid::log::Logger logger();
static void publish_device_values(uint8_t device_type);
static void publish_other_values();
static void publish_sensor_values(const bool time, const bool force = false);
static void publish_all(bool force = false);
static void reset_mqtt_ha();
#ifdef EMSESP_STANDALONE
static void run_test(uuid::console::Shell & shell, const std::string & command); // only for testing
static void dummy_mqtt_commands(const char * message);
static void rx_telegram(const std::vector<uint8_t> & data);
static void uart_telegram(const std::vector<uint8_t> & rx_data);
#endif
static bool process_telegram(std::shared_ptr<const Telegram> telegram);
static std::string pretty_telegram(std::shared_ptr<const Telegram> telegram);
static void send_read_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset = 0, const uint8_t length = 0, const bool front = false);
static void send_write_request(const uint16_t type_id,
const uint8_t dest,
const uint8_t offset,
uint8_t * message_data,
const uint8_t message_length,
const uint16_t validate_typeid);
static void send_write_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset, const uint8_t value);
static void send_write_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset, const uint8_t value, const uint16_t validate_typeid);
static bool device_exists(const uint8_t device_id);
static void device_active(const uint8_t device_id, const bool active);
static bool cmd_is_readonly(const uint8_t device_type, const uint8_t device_id, const char * cmd, const int8_t id);
static uint8_t device_id_from_cmd(const uint8_t device_type, const char * cmd, const int8_t id);
static uint8_t count_devices(const uint8_t device_type);
static uint8_t count_devices();
static uint8_t device_index(const uint8_t device_type, const uint8_t unique_id);
static bool get_device_value_info(JsonObject root, const char * cmd, const int8_t id, const uint8_t devicetype);
static void show_device_values(uuid::console::Shell & shell);
static void show_sensor_values(uuid::console::Shell & shell);
static void show_devices(uuid::console::Shell & shell);
static void show_ems(uuid::console::Shell & shell);
static void dump_all_entities(uuid::console::Shell & shell);
static void dump_all_telegrams(uuid::console::Shell & shell);
static void uart_init();
static void incoming_telegram(uint8_t * data, const uint8_t length);
static bool sensor_enabled() {
return (temperaturesensor_.sensor_enabled());
}
static bool analog_enabled() {
return (analogsensor_.analog_enabled());
}
enum Watch : uint8_t { WATCH_OFF, WATCH_ON, WATCH_RAW, WATCH_UNKNOWN };
static void watch_id(uint16_t id);
static uint16_t watch_id() {
return watch_id_;
}
static void watch(uint8_t watch) {
watch_ = watch; // 0=off, 1=on, 2=raw
if (watch == WATCH_OFF) {
watch_id_ = 0; // reset watch id if watch is disabled
}
}
static uint8_t watch() {
return watch_;
}
static void set_read_id(uint16_t id) {
read_id_ = id;
}
static void set_response_id(uint16_t id) {
response_id_ = id;
}
static uint16_t response_id() {
return response_id_;
}
static bool wait_validate() {
return (wait_validate_ != 0);
}
static void wait_validate(uint16_t wait) {
wait_validate_ = wait;
}
enum Bus_status : uint8_t { BUS_STATUS_CONNECTED = 0, BUS_STATUS_TX_ERRORS, BUS_STATUS_OFFLINE };
static uint8_t bus_status();
static bool tap_water_active() {
return tap_water_active_;
}
static void tap_water_active(const bool tap_water_active) {
tap_water_active_ = tap_water_active;
}
static bool trace_raw() {
return trace_raw_;
}
static void trace_raw(bool set) {
trace_raw_ = set;
}
static void fetch_device_values(const uint8_t device_id = 0);
static void fetch_device_values_type(const uint8_t device_type);
static bool valid_device(const uint8_t device_id);
static void scheduled_fetch_values();
static bool add_device(const uint8_t device_id, const uint8_t product_id, const char * version, const uint8_t brand);
static void scan_devices();
static void clear_all_devices();
static bool return_not_found(JsonObject output, const char * msg, const char * cmd);
static std::deque<std::unique_ptr<EMSdevice>> emsdevices;
// services
static Mqtt mqtt_;
static Modbus * modbus_;
static System system_;
static TemperatureSensor temperaturesensor_;
static AnalogSensor analogsensor_;
static Shower shower_;
static RxService rxservice_;
static TxService txservice_;
static Preferences nvs_;
// web controllers
static ESP32React esp32React;
static WebSettingsService webSettingsService;
static WebStatusService webStatusService;
static WebActivityService webActivityService;
static WebDataService webDataService;
static WebAPIService webAPIService;
static WebLogService webLogService;
static WebCustomizationService webCustomizationService;
static WebSchedulerService webSchedulerService;
static WebCustomEntityService webCustomEntityService;
static WebModulesService webModulesService;
private:
static std::string device_tostring(const uint8_t device_id);
static void process_UBADevices(std::shared_ptr<const Telegram> telegram);
static void process_deviceName(std::shared_ptr<const Telegram> telegram);
static void process_version(std::shared_ptr<const Telegram> telegram);
static void publish_response(std::shared_ptr<const Telegram> telegram);
static void publish_all_loop();
void shell_prompt();
void start_serial_console();
static constexpr uint32_t EMS_FETCH_FREQUENCY = 60000; // check every minute
static constexpr uint8_t EMS_WAIT_KM_TIMEOUT = 60; // wait one minute
struct Device_record {
uint8_t product_id;
EMSdevice::DeviceType device_type;
const char * default_name;
uint8_t flags;
};
static std::vector<Device_record> device_library_;
static uint16_t watch_id_;
static uint8_t watch_;
static uint16_t read_id_;
static bool read_next_;
static uint16_t publish_id_;
static uint16_t response_id_;
static bool tap_water_active_;
static uint8_t publish_all_idx_;
static uint8_t unique_id_count_;
static bool trace_raw_;
static uint16_t wait_validate_;
static bool wait_km_;
static uint32_t last_fetch_;
// UUID stuff
static constexpr auto & serial_console_ = Serial;
static constexpr unsigned long SERIAL_CONSOLE_BAUD_RATE = 115200;
std::shared_ptr<EMSESPShell> shell_;
#ifndef EMSESP_STANDALONE
uuid::telnet::TelnetService telnet_;
#endif
protected:
static uuid::log::Logger logger_;
};
} // namespace emsesp
#endif

45
src/core/emsesp_stub.hpp Normal file
View File

@@ -0,0 +1,45 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_EMSESP_STUB_H
#define EMSESP_EMSESP_STUB_H
#include "system.h"
#include "mqtt.h"
#include "temperaturesensor.h"
// #include "../version.h"
#include "ESP32React/ESP32React.h"
#include <uuid/log.h>
using uuid::log::Logger;
// forward declarator
// used to bind EMS-ESP functions to external frameworks
namespace emsesp {
class EMSESP {
public:
static Mqtt mqtt_;
static System system_;
static TemperatureSensor temperaturesensor_;
static Logger logger();
static ESP32React esp32React;
};
} // namespace emsesp
#endif

101
src/core/emsfactory.h Normal file
View File

@@ -0,0 +1,101 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_EMSFACTORY_H_
#define EMSESP_EMSFACTORY_H_
#include <map>
#include <memory> // for unique_ptr
#include "emsdevice.h"
// Macro for class registration
// Anonymous namespace is used to make the definitions here private to the current
// compilation unit (current file). It is equivalent to the old C static keyword.
#define REGISTER_FACTORY(derivedClass, device_type) \
namespace { \
auto registry_##derivedClass = ConcreteEMSFactory<derivedClass>(device_type); \
}
namespace emsesp {
class EMSdevice; // forward declaration, for gcc linking
class EMSFactory {
public:
virtual ~EMSFactory() = default;
// Register factory object of derived class
// using the device_type as the unique identifier
static auto registerFactory(const uint8_t device_type, EMSFactory * factory) -> void {
auto & reg = EMSFactory::getRegister();
reg[device_type] = factory;
}
using FactoryMap = std::map<uint8_t, EMSFactory *>;
// returns all registered classes (really only for debugging)
static auto device_handlers() -> FactoryMap {
return EMSFactory::getRegister();
}
// Construct derived class returning an unique ptr
static auto add(const uint8_t device_type, uint8_t device_id, uint8_t product_id, const char * version, const char * default_name, uint8_t flags, uint8_t brand)
-> std::unique_ptr<EMSdevice> {
return std::unique_ptr<EMSdevice>(EMSFactory::makeRaw(device_type, device_id, product_id, version, default_name, flags, brand));
}
virtual auto construct(uint8_t device_type, uint8_t device_id, uint8_t product_id, const char * version, const char * name, uint8_t flags, uint8_t brand) const
-> EMSdevice * = 0;
private:
// Force global variable to be initialized, thus it avoids the "initialization order fiasco"
static auto getRegister() -> FactoryMap & {
static FactoryMap classRegister{};
return classRegister;
}
// Construct derived class returning a raw pointer
// find which EMS device it is and use that class
static auto makeRaw(const uint8_t device_type, uint8_t device_id, uint8_t product_id, const char * version, const char * name, uint8_t flags, uint8_t brand)
-> EMSdevice * {
auto it = EMSFactory::getRegister().find(device_type);
if (it != EMSFactory::getRegister().end()) {
return it->second->construct(device_type, device_id, product_id, version, name, flags, brand);
}
return nullptr;
}
};
template <typename DerivedClass>
class ConcreteEMSFactory : EMSFactory {
public:
// Register this global object on the EMSFactory register
ConcreteEMSFactory(const uint8_t device_type) {
EMSFactory::registerFactory(device_type, this);
}
auto construct(uint8_t device_type, uint8_t device_id, uint8_t product_id, const char * version, const char * name, uint8_t flags, uint8_t brand) const
-> EMSdevice * {
return new DerivedClass(device_type, device_id, product_id, version, name, flags, brand);
}
};
} // namespace emsesp
#endif

842
src/core/helpers.cpp Normal file
View File

@@ -0,0 +1,842 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "helpers.h"
#include "emsesp.h"
namespace emsesp {
// like itoa but for hex, and quicker
// note: only for single byte hex values
char * Helpers::hextoa(char * result, const uint8_t value) {
char * p = result;
uint8_t nib1 = (value >> 4) & 0x0F;
uint8_t nib2 = (value >> 0) & 0x0F;
*p++ = nib1 < 0xA ? '0' + nib1 : 'A' + nib1 - 0xA;
*p++ = nib2 < 0xA ? '0' + nib2 : 'A' + nib2 - 0xA;
*p = '\0'; // null terminate just in case
return result;
}
// same as above but to a hex string
std::string Helpers::hextoa(const uint8_t value, bool prefix) {
char buf[3];
if (prefix) {
return std::string("0x") + hextoa(buf, value);
}
return std::string(hextoa(buf, value));
}
// same for 16 bit values
char * Helpers::hextoa(char * result, const uint16_t value) {
if (value <= 0xFF) {
return hextoa(result, (uint8_t)value);
}
hextoa(result, (uint8_t)(value >> 8));
hextoa(&result[2], (uint8_t)(value & 0xFF));
return result;
}
// same as above but to a hex string
std::string Helpers::hextoa(const uint16_t value, bool prefix) {
char buf[5];
if (prefix) {
return std::string("0x") + hextoa(buf, value);
}
return std::string(hextoa(buf, value));
}
#ifdef EMSESP_STANDALONE
// special function to work outside of ESP's libraries
char * Helpers::ultostr(char * ptr, uint32_t value, const uint8_t base) {
if (nullptr == ptr) {
return nullptr;
}
unsigned long t = 0;
unsigned long tmp = value;
int count = 0;
if (tmp == 0) {
count++;
}
while (tmp > 0) {
tmp = tmp / base;
count++;
}
ptr += count;
*ptr = '\0';
do {
unsigned long res = value - base * (t = value / base);
if (res < 10) {
*--ptr = '0' + res;
} else if (res < 16) {
*--ptr = 'A' - 10 + res;
}
} while ((value = t) != 0);
return (ptr);
}
#endif
// fast itoa returning a std::string
// http://www.strudel.org.uk/itoa/
std::string Helpers::itoa(int16_t value) {
std::string buf;
buf.reserve(25); // Pre-allocate enough space.
int quotient = value;
do {
buf += "0123456789abcdef"[std::abs(quotient % 10)];
quotient /= 10;
} while (quotient);
// Append the negative sign
if (value < 0)
buf += '-';
std::reverse(buf.begin(), buf.end());
return buf;
}
/*
* fast itoa
* written by Lukás Chmela, Released under GPLv3. http://www.strudel.org.uk/itoa/ version 0.4
* optimized for ESP32
*/
char * Helpers::itoa(int32_t value, char * result, const uint8_t base) {
// check that the base if valid
if (base < 2 || base > 36) {
*result = '\0';
return result;
}
char * ptr = result, *ptr1 = result;
int32_t tmp_value;
do {
tmp_value = value;
value /= base;
*ptr++ = "zyxwvutsrqponmlkjihgfedcba9876543210123456789abcdefghijklmnopqrstuvwxyz"[35 + (tmp_value - value * base)];
} while (value);
// Apply negative sign
if (tmp_value < 0) {
*ptr++ = '-';
}
*ptr-- = '\0';
while (ptr1 < ptr) {
char tmp_char = *ptr;
*ptr-- = *ptr1;
*ptr1++ = tmp_char;
}
return result;
}
// for decimals 0 to 99, printed as a 2 char string
char * Helpers::smallitoa(char * result, const uint8_t value) {
result[0] = ((value / 10) == 0) ? '0' : (value / 10) + '0';
result[1] = (value % 10) + '0';
result[2] = '\0';
return result;
}
// for decimals 0 to 999, printed as a string
char * Helpers::smallitoa(char * result, const uint16_t value) {
result[0] = ((value / 100) == 0) ? '0' : (value / 100) + '0';
result[1] = (((value % 100) / 10) == 0) ? '0' : ((value % 100) / 10) + '0';
result[2] = (value % 10) + '0';
result[3] = '\0';
return result;
}
// work out how to display booleans
// for strings only
char * Helpers::render_boolean(char * result, const bool value, const bool dashboard) {
uint8_t bool_format_ = dashboard ? EMSESP::system_.bool_dashboard() : EMSESP::system_.bool_format();
if (bool_format_ == BOOL_FORMAT_ONOFF_STR) {
strlcpy(result, value ? translated_word(FL_(on)) : translated_word(FL_(off)), 12);
} else if (bool_format_ == BOOL_FORMAT_ONOFF_STR_CAP) {
strlcpy(result, value ? translated_word(FL_(ON)) : translated_word(FL_(OFF)), 12);
} else if ((bool_format_ == BOOL_FORMAT_10) || (bool_format_ == BOOL_FORMAT_10_STR)) {
strlcpy(result, value ? "1" : "0", 2);
} else {
strlcpy(result, value ? "true" : "false", 7); // default
}
return result;
}
// convert unsigned int (single byte) to text value and returns it
// format: 255(0xFF)=boolean, 0=no formatting, otherwise divide by format
char * Helpers::render_value(char * result, uint8_t value, int8_t format, const uint8_t fahrenheit) {
// special check if its a boolean
if ((uint8_t)format == EMS_VALUE_BOOL) {
if (value == EMS_VALUE_BOOL_OFF) {
render_boolean(result, false);
} else if (value == EMS_VALUE_BOOL_NOTSET) {
return nullptr;
} else {
render_boolean(result, true); // assume on. could have value 0x01 or 0xFF
}
return result;
}
if (!hasValue(value)) {
return nullptr;
}
int16_t new_value = fahrenheit ? format ? value * 1.8 + 32 * format * (fahrenheit - 1) : value * 1.8 + 32 * (fahrenheit - 1) : value;
if (!format) {
itoa(new_value, result, 10); // format = 0
return result;
}
char s2[10];
// special case for / 2
if (format == 2) {
strlcpy(result, itoa(new_value >> 1, s2, 10), 5);
strlcat(result, ".", 5);
strlcat(result, ((new_value & 0x01) ? "5" : "0"), 7);
return result;
} else if (format == 4) {
strlcpy(result, itoa(new_value >> 2, s2, 10), 5);
strlcat(result, ".", 5);
new_value = (new_value & 0x03) * 25;
strlcat(result, itoa(new_value, s2, 10), 7);
return result;
} else if (format > 0) {
strlcpy(result, itoa(new_value / format, s2, 10), 5);
strlcat(result, ".", 5);
strlcat(result, itoa(new_value % format, s2, 10), 7);
} else {
strlcpy(result, itoa(new_value * format * -1, s2, 10), 5);
}
return result;
}
// float: convert float to char
// format is the precision, 0 to 8
char * Helpers::render_value(char * result, const double value, const int8_t format) {
if (format > 8) {
return nullptr;
}
uint32_t p[] = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
char * ret = result;
double v = value < 0 ? value - 1.0 / (2 * p[format]) : value + 1.0 / (2 * p[format]);
auto whole = (long long)v;
if (whole <= 0 && v < 0) {
result[0] = '-';
result++;
whole = -whole;
v = -v;
}
#ifndef EMSESP_STANDALONE
lltoa(whole, result, 10);
#else
ultostr(result, whole, 10);
#endif
while (*result != '\0') {
result++;
}
*result++ = '.';
auto decimal = abs((int32_t)((v - whole) * p[format]));
for (int8_t i = 1; i < format; i++) {
if (decimal < p[i]) {
*result++ = '0'; // add leading zeros
}
}
itoa(decimal, result, 10);
return ret;
}
// int32: convert signed 32bit to text string and returns string
// format: 0=no division, other divide by the value given and render with a decimal point
char * Helpers::render_value(char * result, const int32_t value, const int8_t format, const uint8_t fahrenheit) {
int32_t new_value = fahrenheit ? format ? value * 1.8 + 32 * format * (fahrenheit - 1) : value * 1.8 + 32 * (fahrenheit - 1) : value;
char s[13] = {0};
// just print it if no conversion required (format = 0)
if (!format) {
strlcpy(result, itoa(new_value, s, 10), sizeof(s)); // format is 0
return result;
}
result[0] = '\0';
// check for negative values
if (new_value < 0) {
strlcpy(result, "-", sizeof(s));
new_value *= -1; // convert to positive
} else {
strlcpy(result, "", sizeof(s));
}
// do floating point
if (format == 2) {
// divide by 2
strlcat(result, itoa(new_value / 2, s, 10), sizeof(s));
strlcat(result, ".", sizeof(s));
strlcat(result, ((new_value & 0x01) ? "5" : "0"), sizeof(s));
} else if (format > 0) {
strlcat(result, itoa(new_value / format, s, 10), sizeof(s));
strlcat(result, ".", sizeof(s));
strlcat(result, itoa(((new_value % format) * 10) / format, s, 10), sizeof(s));
} else {
strlcat(result, itoa(new_value * format * -1, s, 10), sizeof(s));
}
return result;
}
// int16: convert short (two bytes) to text string and prints it
char * Helpers::render_value(char * result, const int16_t value, const int8_t format, const uint8_t fahrenheit) {
if (!hasValue(value)) {
return nullptr;
}
return (render_value(result, (int32_t)value, format, fahrenheit)); // use same code, force it to a signed int
}
// uint16: convert unsigned short (two bytes) to text string and prints it
char * Helpers::render_value(char * result, const uint16_t value, const int8_t format, const uint8_t fahrenheit) {
if (!hasValue(value)) {
return nullptr;
}
return (render_value(result, (int32_t)value, format, fahrenheit)); // use same code, force it to a signed int
}
// int8: convert signed byte to text string and prints it
char * Helpers::render_value(char * result, const int8_t value, const int8_t format, const uint8_t fahrenheit) {
if (!hasValue(value)) {
return nullptr;
}
return (render_value(result, (int32_t)value, format, fahrenheit)); // use same code, force it to a signed int
}
// uint32: render long (4 byte) unsigned values
char * Helpers::render_value(char * result, const uint32_t value, const int8_t format, const uint8_t fahrenheit) {
if (!hasValue(value)) {
return nullptr;
}
result[0] = '\0';
uint32_t new_value = fahrenheit ? format ? value * 1.8 + 32 * format * (fahrenheit - 1) : value * 1.8 + 32 * (fahrenheit - 1) : value;
char s[14] = {0};
#ifndef EMSESP_STANDALONE
if (!format) {
strlcpy(result, lltoa(new_value, s, 10), sizeof(s)); // format is 0
} else if (format > 0) {
strlcpy(result, lltoa(new_value / format, s, 10), sizeof(s));
strlcat(result, ".", sizeof(s));
strlcat(result, itoa(((new_value % format) * 10) / format, s, 10), sizeof(s));
if (format == 100) {
strlcat(result, itoa(new_value % 10, s, 10), sizeof(s));
}
} else {
strlcpy(result, lltoa(new_value * format * -1, s, 10), sizeof(s));
}
#else
if (!format) {
strlcpy(result, ultostr(s, new_value, 10), sizeof(s)); // format is 0
} else {
strlcpy(result, ultostr(s, new_value / format, 10), sizeof(s));
strlcat(result, ".", sizeof(s));
strncat(result, ultostr(s, new_value % format, 10), sizeof(s));
}
#endif
return result;
}
// creates string of hex values from an array of bytes
std::string Helpers::data_to_hex(const uint8_t * data, const uint8_t length) {
if (length == 0) {
return "<empty>";
}
char str[length * 3];
memset(str, 0, sizeof(str));
char buffer[4];
char * p = &str[0];
for (uint8_t i = 0; i < length; i++) {
Helpers::hextoa(buffer, data[i]);
*p++ = buffer[0];
*p++ = buffer[1];
*p++ = ' '; // space
}
*--p = '\0'; // null terminate just in case, loosing the trailing space
return std::string(str);
}
// takes a hex string and convert it to an unsigned 32bit number (max 8 hex digits)
// works with only positive numbers
uint32_t Helpers::hextoint(const char * hex) {
if (hex == nullptr) {
return 0;
}
uint32_t val = 0;
// skip leading '0x'
if (hex[0] == '0' && hex[1] == 'x') {
hex += 2;
}
while (*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')
byte = byte - 'a' + 10;
else if (byte >= 'A' && byte <= 'F')
byte = byte - 'A' + 10;
else
return 0; // error
// shift 4 to make space for new digit, and add the 4 bits of the new digit
val = (val << 4) | (byte & 0xF);
}
return val;
}
// quick char to long
int Helpers::atoint(const char * value) {
int x = 0;
char s = value[0];
if (s == '-') {
++value;
}
while (*value >= '0' && *value <= '9') {
x = (x * 10) + (*value - '0');
++value;
}
if (s == '-') {
return (-x);
}
return x;
}
// rounds a number to 2 decimal places
// example: round2(3.14159) -> 3.14
// The conversion to Fahrenheit is different for absolute temperatures and relative temperatures like hysteresis.
// fahrenheit=0 - off, no conversion
// fahrenheit=1 - relative, 1.8t
// fahrenheit=2 - absolute, 1.8t + 32(fahrenheit-1)
double Helpers::transformNumFloat(double value, const int8_t numeric_operator, const uint8_t fahrenheit) {
double val;
if (numeric_operator == 0) { // DV_NUMOP_NONE
val = value * 100;
} else if (numeric_operator > 0) { // DV_NUMOP_DIVxx
val = value * 100 / numeric_operator;
} else { // DV_NUMOP_MULxx
val = value * -100 * numeric_operator;
}
if (fahrenheit) {
val = val * 1.8 + 3200 * (fahrenheit - 1);
}
return (round(val)) / 100.0;
}
// abs of a signed 32-bit integer
uint32_t Helpers::abs(const int32_t i) {
return (i < 0 ? -i : i);
}
// for booleans, use isBool true (EMS_VALUE_BOOL)
bool Helpers::hasValue(const uint8_t & value, const uint8_t isBool) {
if (isBool == EMS_VALUE_BOOL) {
return (value != EMS_VALUE_BOOL_NOTSET);
}
return (value != EMS_VALUE_UINT8_NOTSET);
}
bool Helpers::hasValue(const int8_t & value) {
return (value != EMS_VALUE_INT8_NOTSET);
}
bool Helpers::hasValue(const char * value) {
if ((value == nullptr) || (strlen(value) == 0)) {
return false;
}
return (value[0] != '\0');
}
// for short these are typically 0x8300, 0x7D00 and sometimes 0x8000
bool Helpers::hasValue(const int16_t & value) {
return (abs(value) < EMS_VALUE_UINT16_NOTSET);
}
bool Helpers::hasValue(const uint16_t & value) {
return (value < EMS_VALUE_UINT16_NOTSET);
}
bool Helpers::hasValue(const uint32_t & value) {
return (value != EMS_VALUE_UINT24_NOTSET && value != EMS_VALUE_UINT32_NOTSET);
}
// checks if we can convert a char string to an int value
bool Helpers::value2number(const char * value, int & value_i, const int min, const int max) {
if ((value == nullptr) || (strlen(value) == 0)) {
value_i = 0;
return false;
}
value_i = atoi(value);
if (value_i >= min && value_i <= max) {
return true;
}
return false;
}
// checks if we can convert a char string to a float value
bool Helpers::value2float(const char * value, float & value_f) {
value_f = 0;
if ((value == nullptr) || (strlen(value) == 0)) {
return false;
}
if (value[0] == '-' || value[0] == '.' || (value[0] >= '0' && value[0] <= '9')) {
value_f = atof(value);
return true;
}
if (value[0] == '+' && (value[1] == '.' || (value[1] >= '0' && value[1] <= '9'))) {
value_f = atof(value + 1);
return true;
}
return false;
}
bool Helpers::value2temperature(const char * value, float & value_f, bool relative) {
if (value2float(value, value_f)) {
if (EMSESP::system_.fahrenheit()) {
value_f = relative ? (value_f / 1.8) : (value_f - 32) / 1.8;
}
return true;
}
return false;
}
bool Helpers::value2temperature(const char * value, int & value_i, const bool relative, const int min, const int max) {
if (value2number(value, value_i, min, max)) {
if (EMSESP::system_.fahrenheit()) {
value_i = relative ? (value_i / 1.8) : (value_i - 32) / 1.8;
}
return true;
}
return false;
}
// checks if we can convert a char string to a lowercase string
bool Helpers::value2string(const char * value, std::string & value_s) {
if ((value == nullptr) || (strlen(value) == 0)) {
value_s = std::string{};
return false;
}
value_s = toLower(value);
return true;
}
// checks to see if a string (usually a command or payload cmd) looks like a boolean
// on, off, true, false, 1, 0
bool Helpers::value2bool(const char * value, bool & value_b) {
if ((value == nullptr) || (strlen(value) == 0)) {
return false;
}
std::string bool_str = toLower(value);
if ((bool_str == std::string(Helpers::translated_word(FL_(on)))) || (bool_str == toLower(Helpers::translated_word(FL_(ON)))) || (bool_str == "on")
|| (bool_str == "1") || (bool_str == "true")) {
value_b = true;
return true; // is a bool
}
if ((bool_str == std::string(Helpers::translated_word(FL_(off)))) || (bool_str == toLower(Helpers::translated_word(FL_(OFF)))) || (bool_str == "off")
|| (bool_str == "0") || (bool_str == "false")) {
value_b = false;
return true; // is a bool
}
#ifdef EMSESP_STANDALONE
emsesp::EMSESP::logger().debug("Error. value2bool: %s is not a boolean", value);
#endif
return false; // not a bool
}
// checks to see if a string is member of a vector and return the index, also allow true/false for on/off
// this for a list of lists, when using translated strings
bool Helpers::value2enum(const char * value, uint8_t & value_ui, const char * const ** strs) {
if ((value == nullptr) || (strlen(value) == 0)) {
return false;
}
std::string str = toLower(value);
for (value_ui = 0; strs[value_ui]; value_ui++) {
std::string str1 = toLower(std::string(Helpers::translated_word(strs[value_ui])));
std::string str2 = toLower((strs[value_ui][0])); // also check for default language
if ((str1 != "")
&& ((str2 == "off" && str == "false") || (str2 == "on" && str == "true") || (str == str1) || (str == str2)
|| (value[0] == ('0' + value_ui) && value[1] == '\0'))) {
return true;
}
}
value_ui = 0;
return false;
}
// finds the string (value) of a list vector (strs)
// returns true if found, and sets the value_ui to the index, else false
// also allow true/false for on/off
bool Helpers::value2enum(const char * value, uint8_t & value_ui, const char * const * strs) {
if ((value == nullptr) || (strlen(value) == 0)) {
return false;
}
std::string str = toLower(value);
std::string s_on = Helpers::translated_word(FL_(on));
std::string s_off = Helpers::translated_word(FL_(off));
// stops when a nullptr is found, which is the end delimeter of a MAKE_TRANSLATION()
// could use count_items() to avoid buffer over-run but this works
for (value_ui = 0; strs[value_ui]; value_ui++) {
std::string enum_str = toLower((strs[value_ui]));
if ((enum_str != "")
&& ((enum_str == "off" && (str == s_off || str == "false")) || (enum_str == "on" && (str == s_on || str == "true")) || (str == enum_str)
|| (value[0] == ('0' + value_ui) && value[1] == '\0'))) {
return true;
}
}
return false;
}
// https://stackoverflow.com/questions/313970/how-to-convert-stdstring-to-lower-case
std::string Helpers::toLower(std::string const & s) {
std::string lc = s;
std::transform(lc.begin(), lc.end(), lc.begin(), [](unsigned char c) { return std::tolower(c); });
return lc;
}
std::string Helpers::toLower(const char * s) {
return toLower(std::string(s));
}
std::string Helpers::toUpper(std::string const & s) {
std::string lc = s;
std::transform(lc.begin(), lc.end(), lc.begin(), [](unsigned char c) { return std::toupper(c); });
return lc;
}
// capitalizes one UTF-8 character in char array
// works with Latin1 (1 byte), Polish amd some other (2 bytes) characters
// TODO add special characters that occur in other supported languages
#if defined(EMSESP_STANDALONE)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wtype-limits"
#endif
void Helpers::CharToUpperUTF8(char * c) {
auto p = (c + 1); // pointer to 2nd char of 2-byte unicode char
char p_v = *p; // value of 2nd char in 2-byte unicode char
switch (*c) {
case (char)0xC3:
// grave, acute, circumflex, diaeresis, etc.
if ((p_v >= (char)0xA0) && (p_v <= (char)0xBE)) {
*p -= 0x20;
}
break;
case (char)0xC4:
switch (p_v) {
case (char)0x85: //ą (0xC4,0x85) -> Ą (0xC4,0x84)
case (char)0x87: //ć (0xC4,0x87) -> Ć (0xC4,0x86)
case (char)0x99: //ę (0xC4,0x99) -> Ę (0xC4,0x98)
*p -= 1;
break;
}
break;
case (char)0xC5:
switch (p_v) {
case (char)0x82: //ł (0xC5,0x82) -> Ł (0xC5,0x81)
case (char)0x84: //ń (0xC5,0x84) -> Ń (0xC5,0x83)
case (char)0x9B: //ś (0xC5,0x9B) -> Ś (0xC5,0x9A)
case (char)0xBA: //ź (0xC5,0xBA) -> Ź (0xC5,0xB9)
case (char)0xBC: //ż (0xC5,0xBC) -> Ż (0xC5,0xBB)
*p -= 1;
break;
}
break;
default:
*c = toupper(*c); // works on Latin1 letters
break;
}
}
#if defined(EMSESP_STANDALONE)
#pragma GCC diagnostic pop
#endif
// replace char in char string
void Helpers::replace_char(char * str, char find, char replace) {
if (str == nullptr) {
return;
}
int i = 0;
while (str[i] != '\0') {
// Replace the matched character...
if (str[i] == find)
str[i] = replace;
i++;
}
}
// count number of items in a list
// the end of a list has a nullptr
uint8_t Helpers::count_items(const char * const * list) {
uint8_t list_size = 0;
if (list != nullptr) {
while (list[list_size]) {
list_size++;
}
}
return list_size;
}
// count number of items in a list of lists
// the end of a list has a nullptr
uint8_t Helpers::count_items(const char * const ** list) {
uint8_t list_size = 0;
if (list != nullptr) {
while (list[list_size]) {
list_size++;
}
}
return list_size;
}
// returns char pointer to translated description or fullname
// if force_en is true always take the EN non-translated word
const char * Helpers::translated_word(const char * const * strings, const bool force_en) {
uint8_t language_index = EMSESP::system_.language_index();
uint8_t index = 0; // default en
if (!strings) {
return ""; // no translations
}
// see how many translations we have for this entity. if there is no translation for this, revert to EN
if (!force_en && (Helpers::count_items(strings) >= language_index + 1 && strlen(strings[language_index]))) {
index = language_index;
}
return strings[index];
}
uint16_t Helpers::string2minutes(const std::string & str) {
uint8_t i = 0;
uint16_t res = 0;
uint16_t tmp = 0;
uint8_t state = 0;
while (str[i] != '\0') {
// If we got a digit
if (str[i] >= '0' && str[i] <= '9') {
tmp = tmp * 10 + str[i] - '0';
}
// Or if we got a colon
else if (str[i] == ':') {
// If we were reading the hours
if (state == 0) {
res = 60 * tmp;
}
// Or if we were reading the minutes
else if (state == 1) {
if (tmp > 60) {
return 0;
}
res += tmp;
}
// Or we got an extra colon
else {
return 0;
}
state++;
tmp = 0;
}
// Or we got something wrong
else {
return 0;
}
i++;
}
if (state == 1 && tmp < 60) {
return res + tmp;
} else if (state == 0) { // without : it's only minutes
return tmp;
} else {
return 0; // Or if we were not, something is wrong in the given string
}
}
float Helpers::numericoperator2scalefactor(int8_t numeric_operator) {
if (numeric_operator == 0)
return 1.0f;
else if (numeric_operator > 0)
return 1.0f / numeric_operator;
else
return -numeric_operator;
}
} // namespace emsesp

91
src/core/helpers.h Normal file
View File

@@ -0,0 +1,91 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_HELPERS_H
#define EMSESP_HELPERS_H
#include "telegram.h" // for EMS_VALUE_* settings
#include "common.h"
namespace emsesp {
class Helpers {
public:
static char * render_value(char * result, const double value, const int8_t format); // format is the precision
static char * render_value(char * result, const uint8_t value, const int8_t format, const uint8_t fahrenheit = 0);
static char * render_value(char * result, const int8_t value, const int8_t format, const uint8_t fahrenheit = 0);
static char * render_value(char * result, const uint16_t value, const int8_t format, const uint8_t fahrenheit = 0);
static char * render_value(char * result, const uint32_t value, const int8_t format, const uint8_t fahrenheit = 0);
static char * render_value(char * result, const int16_t value, const int8_t format, const uint8_t fahrenheit = 0);
static char * render_value(char * result, const int32_t value, const int8_t format, const uint8_t fahrenheit = 0);
static char * render_boolean(char * result, const bool value, const bool dashboard = false);
static char * hextoa(char * result, const uint8_t value);
static char * hextoa(char * result, const uint16_t value);
static std::string hextoa(const uint8_t value, bool prefix = true); // default prefix with 0x
static std::string hextoa(const uint16_t value, bool prefix = true); // default prefix with 0x
static std::string data_to_hex(const uint8_t * data, const uint8_t length);
static char * smallitoa(char * result, const uint8_t value);
static char * smallitoa(char * result, const uint16_t value);
static char * itoa(int32_t value, char * result, const uint8_t base = 10);
static std::string itoa(int16_t value);
static uint32_t hextoint(const char * hex);
static int atoint(const char * value);
static bool check_abs(const int32_t i);
static uint32_t abs(const int32_t i);
static uint16_t string2minutes(const std::string & str);
static float numericoperator2scalefactor(int8_t numeric_operator);
static double transformNumFloat(double value, const int8_t numeric_operator, const uint8_t fahrenheit = 0);
static std::string toLower(std::string const & s);
static std::string toUpper(std::string const & s);
static std::string toLower(const char * s);
static void CharToUpperUTF8(char * c);
static void replace_char(char * str, char find, char replace);
static bool hasValue(const uint8_t & value, const uint8_t isBool = 0);
static bool hasValue(const int8_t & value);
static bool hasValue(const int16_t & value);
static bool hasValue(const uint16_t & value);
static bool hasValue(const uint32_t & value);
static bool hasValue(const char * value);
static bool value2number(const char * value, int & value_i, const int min = -2147483648, const int max = 2147483647);
static bool value2float(const char * value, float & value_f);
static bool value2bool(const char * value, bool & value_b);
static bool value2string(const char * value, std::string & value_s);
static bool value2enum(const char * value, uint8_t & value_ui, const char * const ** strs);
static bool value2enum(const char * value, uint8_t & value_ui, const char * const * strs);
static bool value2temperature(const char * value, float & value_f, bool relative = false);
static bool value2temperature(const char * value, int & value_i, const bool relative = false, const int min = -2147483648, const int max = 2147483647);
static uint8_t count_items(const char * const ** list);
static uint8_t count_items(const char * const * list);
static const char * translated_word(const char * const * strings, const bool force_en = false);
#ifdef EMSESP_STANDALONE
static char * ultostr(char * ptr, uint32_t value, const uint8_t base);
#endif
};
} // namespace emsesp
#endif

396
src/core/locale_common.h Normal file
View File

@@ -0,0 +1,396 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
// clang-format off
/*
* THIS FILE CONTAINS STANDARD STRING LITERALS THAT DON'T NEED LANGUAGE TRANSLATIONS
*/
// common words
MAKE_WORD(exit)
MAKE_WORD(help)
MAKE_WORD(log)
MAKE_WORD(set)
MAKE_WORD(show)
MAKE_WORD(su)
MAKE_WORD(scan)
MAKE_WORD(password)
MAKE_WORD(read)
MAKE_WORD(values)
MAKE_WORD(system)
MAKE_WORD(fetch)
MAKE_WORD(restart)
MAKE_WORD(format)
MAKE_WORD(raw)
MAKE_WORD(watch)
MAKE_WORD(syslog)
MAKE_WORD(send)
MAKE_WORD(telegram)
MAKE_WORD(bus_id)
MAKE_WORD(tx_mode)
MAKE_WORD(showertimer)
MAKE_WORD(showeralert)
MAKE_WORD(ems)
MAKE_WORD(devices)
MAKE_WORD(shower)
MAKE_WORD(mqtt)
MAKE_WORD(modbus)
MAKE_WORD(emsesp)
MAKE_WORD(connected)
MAKE_WORD(disconnected)
MAKE_WORD(passwd)
MAKE_WORD(hostname)
MAKE_WORD(wifi)
MAKE_WORD(reconnect)
MAKE_WORD(ssid)
MAKE_WORD(heartbeat)
MAKE_WORD(users)
MAKE_WORD(publish)
MAKE_WORD(board_profile)
MAKE_WORD(setvalue)
MAKE_WORD(service)
MAKE_WORD(message)
// for commands
MAKE_WORD(call)
MAKE_WORD(cmd)
MAKE_WORD(id)
MAKE_WORD(hc)
MAKE_WORD(dhw)
MAKE_WORD(device)
MAKE_WORD(data)
MAKE_WORD(command)
MAKE_WORD(commands)
MAKE_WORD(info)
MAKE_WORD(settings)
MAKE_WORD(value)
MAKE_WORD(entities)
MAKE_WORD(coldshot)
// device types - lowercase, used in MQTT
MAKE_WORD(boiler)
MAKE_WORD(thermostat)
MAKE_WORD_CUSTOM(switcher, "switch")
MAKE_WORD(solar)
MAKE_WORD(mixer)
MAKE_WORD(gateway)
MAKE_WORD(controller)
MAKE_WORD(connect)
MAKE_WORD(heatpump)
MAKE_WORD(generic)
MAKE_WORD(analogsensor)
MAKE_WORD(temperaturesensor)
MAKE_WORD(alert)
MAKE_WORD(pump)
MAKE_WORD(extension)
MAKE_WORD(heatsource)
MAKE_WORD(scheduler)
MAKE_WORD(custom)
MAKE_WORD(ventilation)
MAKE_WORD(water)
MAKE_WORD(pool)
// brands
MAKE_WORD_CUSTOM(bosch, "Bosch")
MAKE_WORD_CUSTOM(junkers, "Junkers")
MAKE_WORD_CUSTOM(buderus, "Buderus")
MAKE_WORD_CUSTOM(nefit, "Nefit")
MAKE_WORD_CUSTOM(sieger, "Sieger")
MAKE_WORD_CUSTOM(worcester, "Worcester")
MAKE_WORD_CUSTOM(ivt, "IVT")
MAKE_WORD_CUSTOM(no_brand, "")
// types
MAKE_WORD_CUSTOM(number, "number")
MAKE_WORD_CUSTOM(enum, "enum")
MAKE_WORD_CUSTOM(text, "text")
// Console
MAKE_WORD_CUSTOM(EMSESP, "EMS-ESP")
MAKE_WORD_CUSTOM(host_fmt, "Host: %s")
MAKE_WORD_CUSTOM(port_fmt, "Port: %d")
MAKE_WORD_CUSTOM(hostname_fmt, "Hostname: %s")
MAKE_WORD_CUSTOM(mark_interval_fmt, "Mark interval: %lus")
MAKE_WORD_CUSTOM(wifi_ssid_fmt, "WiFi SSID: %s")
MAKE_WORD_CUSTOM(wifi_password_fmt, "WiFi Password: %S")
MAKE_WORD_CUSTOM(tx_mode_fmt, "Tx mode: %d")
MAKE_WORD_CUSTOM(bus_id_fmt, "Bus ID: %02X")
MAKE_WORD_CUSTOM(log_level_fmt, "Log level: %s")
MAKE_WORD_CUSTOM(cmd_optional, "[cmd]")
MAKE_WORD_CUSTOM(ha_optional, "[ha]")
MAKE_WORD_CUSTOM(deep_optional, "[deep]")
MAKE_WORD_CUSTOM(watchid_optional, "[ID]")
MAKE_WORD_CUSTOM(watch_format_optional, "[off | on | raw | unknown]")
MAKE_WORD_CUSTOM(invalid_watch, "Invalid watch type")
MAKE_WORD_CUSTOM(data_mandatory, "\"XX XX ...\"")
MAKE_WORD_CUSTOM(asterisks, "********")
MAKE_WORD_CUSTOM(n_mandatory, "<n>")
MAKE_WORD_CUSTOM(sensorid_optional, "[sensor ID]")
MAKE_WORD_CUSTOM(id_optional, "[id|hc]")
MAKE_WORD_CUSTOM(partitionname_optional, "[partitionname]")
MAKE_WORD_CUSTOM(data_optional, "[data]")
MAKE_WORD_CUSTOM(offset_optional, "[offset]")
MAKE_WORD_CUSTOM(length_optional, "[length]")
MAKE_WORD_CUSTOM(typeid_mandatory, "<type ID>")
MAKE_WORD_CUSTOM(deviceid_mandatory, "<deviceID>")
MAKE_WORD_CUSTOM(device_type_optional, "[device]")
MAKE_WORD_CUSTOM(invalid_log_level, "Invalid log level")
MAKE_WORD_CUSTOM(log_level_optional, "[level]")
MAKE_WORD_CUSTOM(show_commands, "[system | users | devices | log | ems | values | mqtt | commands")
MAKE_WORD_CUSTOM(name_mandatory, "<name>")
MAKE_WORD_CUSTOM(name_optional, "[name]")
MAKE_WORD_CUSTOM(new_password_prompt1, "Enter new password: ")
MAKE_WORD_CUSTOM(new_password_prompt2, "Retype new password: ")
MAKE_WORD_CUSTOM(password_prompt, "Password: ")
MAKE_WORD_CUSTOM(unset, "<unset>")
MAKE_WORD_CUSTOM(enable_mandatory, "<enable | disable>")
MAKE_WORD_CUSTOM(service_mandatory, "<ap | mqtt | ntp>")
// more common names that don't need translations
MAKE_NOTRANSLATION(1x3min, "1x3min")
MAKE_NOTRANSLATION(2x3min, "2x3min")
MAKE_NOTRANSLATION(3x3min, "3x3min")
MAKE_NOTRANSLATION(4x3min, "4x3min")
MAKE_NOTRANSLATION(5x3min, "5x3min")
MAKE_NOTRANSLATION(6x3min, "6x3min")
MAKE_NOTRANSLATION(auto, "auto")
MAKE_NOTRANSLATION(rc3x, "RC3x")
MAKE_NOTRANSLATION(rc20, "RC20")
MAKE_NOTRANSLATION(fb10, "FB10")
MAKE_NOTRANSLATION(fb100, "FB100")
MAKE_NOTRANSLATION(rc310, "RC310")
MAKE_NOTRANSLATION(rc200, "RC200")
MAKE_NOTRANSLATION(rc100, "RC100")
MAKE_NOTRANSLATION(rc100h, "RC100H")
MAKE_NOTRANSLATION(tc100, "TC100")
MAKE_NOTRANSLATION(rc120rf, "RC120RF")
MAKE_NOTRANSLATION(rc220, "RC220/RT800")
MAKE_NOTRANSLATION(single, "single")
MAKE_NOTRANSLATION(dash, "-")
MAKE_NOTRANSLATION(BLANK, "")
MAKE_NOTRANSLATION(pwm, "pwm")
MAKE_NOTRANSLATION(pwm_invers, "pwm inverse")
MAKE_NOTRANSLATION(mpc, "mpc")
MAKE_NOTRANSLATION(tempauto, "temp auto")
MAKE_NOTRANSLATION(bypass, "bypass")
MAKE_NOTRANSLATION(mixer, "mixer")
MAKE_NOTRANSLATION(monovalent, "monovalent")
MAKE_NOTRANSLATION(bivalent, "bivalent")
MAKE_NOTRANSLATION(n_o, "n_o")
MAKE_NOTRANSLATION(n_c, "n_c")
MAKE_NOTRANSLATION(prog1, "prog 1")
MAKE_NOTRANSLATION(prog2, "prog 2")
MAKE_NOTRANSLATION(proga, "prog a")
MAKE_NOTRANSLATION(progb, "prog b")
MAKE_NOTRANSLATION(progc, "prog c")
MAKE_NOTRANSLATION(progd, "prog d")
MAKE_NOTRANSLATION(proge, "prog e")
MAKE_NOTRANSLATION(progf, "prog f")
MAKE_NOTRANSLATION(rc35, "RC35")
MAKE_NOTRANSLATION(0kW, "0 kW")
MAKE_NOTRANSLATION(2kW, "2 kW")
MAKE_NOTRANSLATION(3kW, "3 kW")
MAKE_NOTRANSLATION(4kW, "4 kW")
MAKE_NOTRANSLATION(6kW, "6 kW")
MAKE_NOTRANSLATION(9kW, "9 kW")
MAKE_NOTRANSLATION(L1, "L1")
MAKE_NOTRANSLATION(L2, "L2")
MAKE_NOTRANSLATION(L3, "L3")
MAKE_NOTRANSLATION(L4, "L4")
// templates - this are not translated and will be saved under options_single
MAKE_NOTRANSLATION(tpl_datetime, "NTP | dd.mm.yyyy-hh:mm:ss-day(0-6)-dst(0/1)")
MAKE_NOTRANSLATION(tpl_switchtime, "<nn> [ not_set | day hh:mm on|off ]")
MAKE_NOTRANSLATION(tpl_switchtime1, "<nn> [ not_set | day hh:mm Tn ]")
MAKE_NOTRANSLATION(tpl_holidays, "dd.mm.yyyy-dd.mm.yyyy")
MAKE_NOTRANSLATION(tpl_date, "dd.mm.yyyy")
MAKE_NOTRANSLATION(tpl_input, "<inv>[<evu1><evu2><evu3><comp><aux><cool><heat><dhw><pv>]")
MAKE_NOTRANSLATION(tpl_input4, "<inv>[<comp><aux><cool><heat><dhw><pv>]")
#if defined(EMSESP_TEST)
MAKE_NOTRANSLATION(test_cmd, "run a test")
#endif
// TAG mapping - maps to DeviceValueTAG_s in emsdevicevalue.cpp
// use empty string if want to suppress showing tags
// mqtt tags must not have spaces
MAKE_NOTRANSLATION(tag_none, "")
MAKE_NOTRANSLATION(tag_heartbeat, "")
MAKE_NOTRANSLATION(tag_device_data, "")
// Unit Of Measurement mapping - maps to DeviceValueUOM_s in emsdevice.cpp
// Translating hours/minute/seconds are done in emsdevice.cpp (uom_to_string())
MAKE_WORD_CUSTOM(uom_blank, " ")
MAKE_WORD_CUSTOM(uom_percent, "%")
MAKE_WORD_CUSTOM(uom_degrees, "°C")
MAKE_WORD_CUSTOM(uom_kwh, "kWh")
MAKE_WORD_CUSTOM(uom_wh, "Wh")
MAKE_WORD_CUSTOM(uom_bar, "bar")
MAKE_WORD_CUSTOM(uom_ua, "µA")
MAKE_WORD_CUSTOM(uom_lmin, "l/min")
MAKE_WORD_CUSTOM(uom_kw, "kW")
MAKE_WORD_CUSTOM(uom_w, "W")
MAKE_WORD_CUSTOM(uom_kb, "KB")
MAKE_WORD_CUSTOM(uom_dbm, "dBm")
MAKE_WORD_CUSTOM(uom_fahrenheit, "°F")
MAKE_WORD_CUSTOM(uom_mv, "mV")
MAKE_WORD_CUSTOM(uom_sqm, "")
MAKE_WORD_CUSTOM(uom_m3, "")
MAKE_WORD_CUSTOM(uom_l, "l")
MAKE_WORD_CUSTOM(uom_kmin, "K*min")
MAKE_WORD_CUSTOM(uom_k, "K")
MAKE_WORD_CUSTOM(uom_volts, "V")
MAKE_WORD_CUSTOM(uom_mbar, "mbar")
MAKE_WORD_CUSTOM(uom_lh, "l/h")
MAKE_WORD_CUSTOM(uom_ctkwh, "ct/kWh")
// MQTT topics and prefixes
MAKE_WORD_CUSTOM(heating_active, "heating_active")
MAKE_WORD_CUSTOM(tapwater_active, "tapwater_active")
MAKE_WORD_CUSTOM(response, "response")
// syslog
MAKE_ENUM_FIXED(list_syslog_level, "off", "emerg", "alert", "crit", "error", "warn", "notice", "info", "debug", "trace", "all")
// sensors
MAKE_ENUM_FIXED(counter, "counter")
MAKE_ENUM_FIXED(digital_out, "digital_out")
MAKE_ENUM_FIXED(list_sensortype, "disabled", "digital in", "counter", "adc", "timer", "rate", "digital out", "pwm 0", "pwm 1", "pwm 2")
// watch
MAKE_ENUM_FIXED(list_watch, "off", "on", "raw", "unknown")
/*
* The rest below are Enums and generated from translations lists
*/
MAKE_ENUM(enum_cylprio, FL_(cyl1), FL_(cyl2))
MAKE_ENUM(enum_progMode, FL_(prog1), FL_(prog2))
MAKE_ENUM(enum_progMode4, FL_(proga), FL_(progb), FL_(progc), FL_(progd), FL_(proge), FL_(progf))
MAKE_ENUM(enum_climate, FL_(seltemp), FL_(roomtemp))
MAKE_ENUM(enum_charge, FL_(chargepump), FL_(3wayvalve))
MAKE_ENUM(enum_freq, FL_(off), FL_(1x3min), FL_(2x3min), FL_(3x3min), FL_(4x3min), FL_(5x3min), FL_(6x3min), FL_(continuous))
MAKE_ENUM(enum_off_time_date_manual, FL_(off), FL_(time), FL_(date), FL_(manual))
MAKE_ENUM(enum_comfort, FL_(hot), FL_(eco), FL_(intelligent))
MAKE_ENUM(enum_comfort1, FL_(high_comfort), FL_(eco))
MAKE_ENUM(enum_comfort2, FL_(eco), FL_(high_comfort))
MAKE_ENUM(enum_flow, FL_(off), FL_(flow), FL_(bufferedflow), FL_(buffer), FL_(layeredbuffer))
MAKE_ENUM(enum_reset, FL_(dash), FL_(maintenance), FL_(error), FL_(history), FL_(message))
MAKE_ENUM(enum_maxHeat, FL_(0kW), FL_(2kW), FL_(3kW), FL_(4kW), FL_(6kW), FL_(9kW))
MAKE_ENUM(enum_pumpMode, FL_(proportional), FL_(deltaP1), FL_(deltaP2), FL_(deltaP3), FL_(deltaP4))
MAKE_ENUM(enum_pumpCharacter, FL_(proportional), FL_(pressure1), FL_(pressure2), FL_(pressure3), FL_(pressure4), FL_(pressure5), FL_(pressure6))
MAKE_ENUM(enum_hpPumpMode, FL_(auto), FL_(continuous))
// thermostat lists
MAKE_ENUM(enum_ibaMainDisplay, FL_(internal_temperature), FL_(internal_setpoint), FL_(external_temperature), FL_(burner_temperature), FL_(ww_temperature), FL_(functioning_mode), FL_(time), FL_(date), FL_(smoke_temperature))
MAKE_ENUM(enum_ibaLanguage, FL_(german), FL_(dutch), FL_(french), FL_(italian))
MAKE_ENUM(enum_ibaLanguage_RC30, FL_(german), FL_(dutch))
MAKE_ENUM(enum_floordrystatus, FL_(off), FL_(start), FL_(heat), FL_(hold), FL_(cool), FL_(end))
MAKE_ENUM(enum_ibaBuildingType, FL_(light), FL_(medium), FL_(heavy))
MAKE_ENUM(enum_PID, FL_(fast), FL_(medium), FL_(slow))
MAKE_ENUM(enum_heatup, FL_(slow), FL_(medium), FL_(fast))
MAKE_ENUM(enum_wwMode, FL_(off), FL_(normal), FL_(comfort), FL_(auto), FL_(own_prog))
MAKE_ENUM(enum_wwCircMode, FL_(off), FL_(on), FL_(auto), FL_(own_prog))
MAKE_ENUM(enum_wwMode2, FL_(off), FL_(on), FL_(auto))
MAKE_ENUM(enum_wwMode3, FL_(on), FL_(off), FL_(auto))
MAKE_ENUM(enum_wwMode4, FL_(off), FL_(ecoplus), FL_(eco), FL_(comfort), FL_(auto))
MAKE_ENUM(enum_wwMode5, FL_(normal), FL_(comfort), FL_(ecoplus)) // Rego3000
MAKE_ENUM(enum_wwMode6, FL_(off), FL_(comfort), FL_(auto)) // CR120
MAKE_ENUM(enum_heatingtype, FL_(off), FL_(radiator), FL_(convector), FL_(floor))
MAKE_ENUM(enum_heatingtype1, FL_(off), FL_(curve), FL_(radiator), FL_(convector), FL_(floor))
MAKE_ENUM(enum_heatingtype2, FL_(off), FL_(radiator), FL_(convector), FL_(floor), FL_(roomflow), FL_(roomload))
MAKE_ENUM(enum_summermode, FL_(summer), FL_(auto), FL_(winter))
MAKE_ENUM(enum_hpoperatingmode, FL_(off), FL_(auto), FL_(heating), FL_(cooling))
MAKE_ENUM(enum_summer, FL_(winter), FL_(summer))
MAKE_ENUM(enum_operatingstate, FL_(heating), FL_(off), FL_(cooling))
MAKE_ENUM(enum_hpmode, FL_(heating), FL_(cooling), FL_(heatandcool))
MAKE_ENUM(enum_mode, FL_(manual), FL_(auto)) // RC100, RC300, RC310
MAKE_ENUM(enum_mode2, FL_(off), FL_(manual), FL_(auto)) // RC20, RC30, BC400
MAKE_ENUM(enum_mode3, FL_(night), FL_(day), FL_(auto)) // RC35, RC30_N, RC25, RC20_N
MAKE_ENUM(enum_mode4, FL_(nofrost), FL_(eco), FL_(heat), FL_(auto)) // JUNKERS
MAKE_ENUM(enum_mode5, FL_(auto), FL_(off)) // CRF
MAKE_ENUM(enum_mode6, FL_(nofrost), FL_(night), FL_(day)) // RC10
MAKE_ENUM(enum_mode_ha, FL_(off), FL_(heat), FL_(auto)) // HA climate
MAKE_ENUM(enum_modetype, FL_(eco), FL_(comfort))
MAKE_ENUM(enum_modetype3, FL_(night), FL_(day))
MAKE_ENUM(enum_modetype4, FL_(nofrost), FL_(eco), FL_(heat))
MAKE_ENUM(enum_modetype5, FL_(off), FL_(on))
MAKE_ENUM(enum_reducemode, FL_(nofrost), FL_(reduce), FL_(room), FL_(outdoor))
MAKE_ENUM(enum_reducemode1, FL_(outdoor), FL_(room), FL_(reduce)) // RC310 values: 1-3
MAKE_ENUM(enum_nofrostmode, FL_(off), FL_(outdoor), FL_(room))
MAKE_ENUM(enum_nofrostmode1, FL_(room), FL_(outdoor), FL_(room_outdoor))
MAKE_ENUM(enum_controlmode, FL_(optimized), FL_(simple), FL_(na), FL_(room), FL_(power))
MAKE_ENUM(enum_controlmode1, FL_(weather_compensated), FL_(outside_basepoint), FL_(na), FL_(room), FL_(power), FL_(constant)) // RC310 1-4
MAKE_ENUM(enum_controlmode2, FL_(outdoor), FL_(room))
MAKE_ENUM(enum_controlmode3, FL_(off), FL_(unmixed), FL_(unmixedIPM), FL_(mixed))
MAKE_ENUM(enum_control, FL_(off), FL_(rc20), FL_(rc3x))
MAKE_ENUM(enum_j_control, FL_(off), FL_(fb10), FL_(fb100))
MAKE_ENUM(enum_roomsensor, FL_(extern), FL_(intern), FL_(auto))
MAKE_ENUM(enum_roominfluence, FL_(off), FL_(intern), FL_(extern), FL_(auto))
MAKE_ENUM(enum_control1, FL_(rc310), FL_(rc200), FL_(rc100), FL_(rc100h), FL_(tc100))
MAKE_ENUM(enum_control2, FL_(off), FL_(dash), FL_(rc100), FL_(rc100h), FL_(dash), FL_(rc120rf), FL_(rc220), FL_(single)) // BC400
MAKE_ENUM(enum_switchmode, FL_(off), FL_(eco), FL_(comfort), FL_(heat))
MAKE_ENUM(enum_switchProgMode, FL_(level), FL_(absolute))
MAKE_ENUM(enum_dayOfWeek, FL_(day_mo), FL_(day_tu), FL_(day_we), FL_(day_th), FL_(day_fr), FL_(day_sa), FL_(day_su), FL_(all))
MAKE_ENUM(enum_progMode2, FL_(own_1), FL_(family), FL_(morning), FL_(evening), FL_(am), FL_(pm), FL_(midday), FL_(singles), FL_(seniors), FL_(new), FL_(own_2))
MAKE_ENUM(enum_progMode3, FL_(family), FL_(morning), FL_(evening), FL_(am), FL_(pm), FL_(midday), FL_(singles), FL_(seniors))
MAKE_ENUM(enum_hybridStrategy, FL_(co2_optimized), FL_(cost_optimized), FL_(outside_temp_switched), FL_(co2_cost_mix))
MAKE_ENUM(enum_hybridStrategy1, FL_(cost_optimized), FL_(co2_optimized), FL_(outside_temp_alt), FL_(outside_temp_par), FL_(hp_preferred), FL_(boiler_only))
MAKE_ENUM(enum_lowNoiseMode, FL_(off), FL_(reduced_output), FL_(switchoff), FL_(perm))
// heat pump
MAKE_ENUM(enum_hpactivity, FL_(off), FL_(heating), FL_(cooling), FL_(hot_water), FL_(pool), FL_(pool_heating), FL_(defrost), FL_(compressor_alarm)) // BV name COMPRESSOR_E21_STATUS
MAKE_ENUM(enum_silentMode, FL_(off), FL_(auto), FL_(on))
MAKE_ENUM(enum_4way, FL_(cool_defrost), FL_(heat_ww))
// solar
MAKE_ENUM(enum_solarmode, FL_(constant), FL_(pwm), FL_(analog))
MAKE_ENUM(enum_collectortype, FL_(flat), FL_(vacuum))
MAKE_ENUM(enum_wwStatus2, FL_(BLANK), FL_(BLANK), FL_(BLANK), FL_(no_heat), FL_(BLANK), FL_(BLANK), FL_(heatrequest), FL_(BLANK), FL_(disinfecting), FL_(hold))
// mixer
MAKE_ENUM(enum_shunt, FL_(stopped), FL_(opening), FL_(closing), FL_(open), FL_(close))
MAKE_ENUM(enum_wwProgMode, FL_(std_prog), FL_(own_prog))
// AM200 lists
MAKE_ENUM(enum_vr2Config, FL_(off), FL_(bypass))
MAKE_ENUM(enum_aPumpSignal, FL_(off), FL_(pwm), FL_(pwm_invers))
MAKE_ENUM(enum_bufBypass, FL_(no), FL_(mixer), FL_(valve))
MAKE_ENUM(enum_blockMode, FL_(off), FL_(auto), FL_(blocking))
MAKE_ENUM(enum_bufConfig, FL_(off), FL_(monovalent), FL_(bivalent))
MAKE_ENUM(enum_blockTerm, FL_(n_o), FL_(n_c))
// Ventilation
MAKE_ENUM(enum_ventMode, FL_(auto), FL_(off), FL_(L1), FL_(L2), FL_(L3), FL_(L4), FL_(demand), FL_(sleep), FL_(intense), FL_(bypass), FL_(partymode), FL_(fireplace))
// water
MAKE_ENUM(enum_errorDisp, FL_(off), FL_(normal), FL_(inverted))
#pragma GCC diagnostic pop
// clang-format on

View File

@@ -0,0 +1,941 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY, without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
// clang-format off
// Define language codes here and add to languages[] in system.cpp
#define EMSESP_LOCALE_EN "en"
#define EMSESP_LOCALE_DE "de"
#define EMSESP_LOCALE_NL "nl"
#define EMSESP_LOCALE_SV "sv"
#define EMSESP_LOCALE_PL "pl"
#define EMSESP_LOCALE_NO "no"
#define EMSESP_LOCALE_FR "fr"
#define EMSESP_LOCALE_TR "tr"
#define EMSESP_LOCALE_IT "it"
#define EMSESP_LOCALE_SK "sk"
#define EMSESP_LOCALE_CZ "cz"
// IMPORTANT! translations are in the order:,en, de, nl, sv, pl, no, fr, tr, it, sk, cz
//
// if there is no translation, it will default to en
//
// device types, as display in Web and Console
MAKE_WORD_TRANSLATION(boiler_hp_device, "Boiler/HP", "Kessel/WP", "CV ketel/WP", "Värmepanna/VP", "Kocioł/PC", "Varmekjele/VP", "", "Kazan/IP", "Caldaia/PC", "Kotol/TČ", "Kotel/TČ") // TODO translate
MAKE_WORD_TRANSLATION(boiler_device, "Boiler", "Kessel", "CV ketel", "Värmepanna", "Kocioł", "Varmekjele", "", "Kazan", "Caldaia", "Kotol", "Kotel") // TODO translate
MAKE_WORD_TRANSLATION(thermostat_device, "Thermostat", "Thermostat", "Thermostaat", "Termostat", "Termostat", "Termostat", "", "Termostat", "Termostato", "Termostat", "Termostat") // TODO translate
MAKE_WORD_TRANSLATION(heatpump_device, "Heat Pump", "Wärmepumpe", "Warmtepomp", "Värmepump", "Pompa ciepła", "Varmepumpe", "", "Isı Pompası", "Pompa di Calore", "Tepelné čerpadlo", "Tepelné čerpadlo") // TODO translate
MAKE_WORD_TRANSLATION(solar_device, "Solar Module", "Solarmodul", "Solar Module", "Solmodul", "Moduł solarny", "Solmodul", "", "Güneş Enerjisi Cihazı", "Modulo Solare", "Solárny modul", "Solární modul") // TODO translate
MAKE_WORD_TRANSLATION(connect_device, "Connect Module", "Verbindungsmodul", "Connect Module", "Uppkopplingsmodul", "Moduł przyłączeń", "Sammenkoblingsmodul", "", "Güneş Enerjisi Cihazı", "Modulo connessione", "Pripojte modul", "Modul připojení") // TODO translate
MAKE_WORD_TRANSLATION(mixer_device, "Mixer Module", "Mischermodul", "Mixer Module", "Blandarmodul", "Moduł mieszacza", "Miksermodul", "", "Karışım Cihazı", "Modulo Miscela", "Modul mixera", "Směšovací modul") // TODO translate
MAKE_WORD_TRANSLATION(controller_device, "Controller Module", "Regelmodul", "Controller Module", "Styrmodul", "Moduł sterujący", "Styremodul", "", "Kontrol Ünitesi", "Modulo Controllo", "Modul ovládača", "Řídicí modul") // TODO translate
MAKE_WORD_TRANSLATION(switch_device, "Switch Module", "Schaltmodul", "Switch Module", "Relämodul", "Moduł przełączający", "Switch modul", "", "Anahtar", "Modulo Switch", "Spínací modul", "Spínací modul") // TODO translate
MAKE_WORD_TRANSLATION(gateway_device, "Gateway Module", "Gateway-Modul", "Gateway Module", "Gateway", "Moduł IP", "Gateway", "", "Ağ Geçidi", "Modulo Gateway", "Modul brány", "Modul brány") // TODO translate
MAKE_WORD_TRANSLATION(alert_device, "Alert Module", "Alarmmodul", "Alert Module", "Larmmodul", "Moduł alarmowy", "Alarmmodul", "", "Alarm Cihazı", "Module Avviso", "Modul upozornení", "Modul upozornění") // TODO translate
//MAKE_WORD_TRANSLATION(pump_device, "Pump Module", "Pumpenmodul", "Pump Module", "Pumpmodul", "Moduł pompy", "Pumpemodul", "", "Pompa", "Module Pompa", "Modul čerpadla", "Modul čerpadla") // TODO translate
MAKE_WORD_TRANSLATION(extension_device, "Extension Module", "Erweiterungsnmodul", "Module", "Utökningsmodul", "Moduł rozszerzeń", "Modul", "", "", "Module", "Rozširujúci modul", "Rozšiřující modul") // TODO translate
MAKE_WORD_TRANSLATION(heatsource_device, "Heatsource", "Wärmequelle", "Heatsource", "Värmekälla", "Źródło ciepła", "Varmekilde", "", "Isı Kaynağı", "Fonte di calore", "Tepelný zdroj", "Zdroj tepla") // TODO translate
MAKE_WORD_TRANSLATION(sensors_device, "Sensors", "Sensoren", "Sensoren", "Sensorer", "Czujniki", "Sensorer", "Capteurs", "Sensör Cihazı", "Sensori", "Snímače", "Senzory")
MAKE_WORD_TRANSLATION(unknown_device, "Unknown", "Unbekannt", "Onbekend", "Okänt", "Nieznane urządzenie", "Ukjent", "Inconnu", "Bilinmeyen", "Sconosciuto", "Neznámy", "Neznámé")
MAKE_WORD_TRANSLATION(custom_device, "Custom", "Nutzerdefiniert", "Aangepast", "Anpassad", "Niestandardowe", "", "", "Özel", "Personalizzato", "Vlastné", "Vlastní") // TODO translate
MAKE_WORD_TRANSLATION(custom_device_name, "Custom Entities", "Nutzer deklarierte Entitäten", "Gebruiker gedefineerd", "Egendefinierade entiteter", "Encje zdefiniowane przez użytkownika", "", "", "Kullanıcı tarafından tanımlanmış varlıklar", "Entità definita da utente", "Používateľom definované entity", "Vlastní entity") // TODO translate
MAKE_WORD_TRANSLATION(ventilation_device, "Ventilation", "Belüftung", "Ventilatie", "Ventilation", "Wentylacja", "", "", "Havalandırma", "Ventilazione", "Vetranie", "Větrání") // TODO translate
MAKE_WORD_TRANSLATION(water_device, "Water Module", "Wassermodul", "", "Vattenmodul", "Moduł wodny", "", "", "", "", "Modul vody", "Modul vody") // TODO translate
MAKE_WORD_TRANSLATION(pool_device, "Pool Module", "Poolmodul", "", "Poolmodul", "Moduł basenu", "", "", "", "", "Modul bazéna", "Modul bazénu") // TODO translate
// commands
MAKE_WORD_TRANSLATION(info_cmd, "list all values (verbose)", "Liste aller Werte", "lijst van alle waardes", "lista alla värden", "wyświetl wszystkie wartości", "Viser alle verdier", "", "Tüm değerleri listele", "elenca tutti i valori", "zobraziť všetky hodnoty", "vypsat všechny hodnoty (podrobně)") // TODO translate
MAKE_WORD_TRANSLATION(commands_cmd, "list all commands", "Liste aller Kommandos", "lijst van alle commando's", "lista alla kommandon", "wyświetl wszystkie komendy", "Viser alle kommandoer", "", "Tüm komutları listele", "elencaa tutti i comandi", "zobraziť všetky príkazy", "vypsat všechny příkazy") // TODO translate
MAKE_WORD_TRANSLATION(entities_cmd, "list all entities", "Liste aller Entitäten", "lijst van alle entiteiten", "lista all entiteter", "wyświetl wszsytkie encje", "Viser alle enheter", "", "Tüm varlıkları listele", "elenca tutte le entità", "zobraziť všetky entity", "vypsat všechny entity") // TODO translate
MAKE_WORD_TRANSLATION(send_cmd, "send a telegram", "Sende EMS-Telegramm", "stuur een telegram", "skicka ett telegram", "wyślij telegram", "send et telegram", "", "Bir telegram gönder", "invia un telegramma", "poslať telegram", "odeslat telegram") // TODO translate
MAKE_WORD_TRANSLATION(read_cmd, "send read request", "", "", "skicka en läsförfrågan", "", "", "", "", "", "odoslať žiadosť o prečítanie", "") // TODO translate
MAKE_WORD_TRANSLATION(setiovalue_cmd, "set I/O value", "Setze Werte E/A", "instellen standaardwaarde", "sätt ett I/O-värde", "ustaw wartość", "sett en io verdi", "", "Giriş/Çıkış değerlerini ayarla", "imposta valore io", "nastaviť hodnotu io", "nastavit hodnotu I/O") // TODO translate
MAKE_WORD_TRANSLATION(changeloglevel_cmd, "change log level", "Ändere Protokollebene", "aanpassen log niveau", "ändra logg-nivå", "zmień poziom log-u", "endre loggnivå", "", "Kayıt seviyesini değiştir", "cambia livello registrazione", "zmeniť úroveň protokolu", "změnit úroveň protokolování") // TODO translate
MAKE_WORD_TRANSLATION(fetch_cmd, "refresh all EMS values", "Aktualisiere alle EMS-Werte", "Verversen alle EMS waardes", "uppdatera alla EMS-värden", "odśwież wszystkie wartości EMS", "oppfrisk alle EMS verdier", "", "Bütün EMS değerlerini yenile", "aggiornare tutti i valori EMS", "obnoviť všetky hodnoty EMS", "aktualizovat všechny EMS hodnoty") // TODO translate
MAKE_WORD_TRANSLATION(restart_cmd, "restart EMS-ESP", "Neustart", "opnieuw opstarten", "starta om EMS-ESP", "uruchom ponownie EMS-ESP", "restart EMS-ESP", "redémarrer EMS-ESP", "EMS-ESPyi yeniden başlat", "riavvia EMS-ESP", "reštart EMS-ESP", "restartovat EMS-ESP") // TODO translate
MAKE_WORD_TRANSLATION(format_cmd, "factory reset EMS-ESP", "EMS-ESP auf Werkseinstellungen zurücksetzen", "fabriksåterställ EMS-ESP", "", "", "", "", "", "", "továrenske nastavenie EMS-ESP", "tovární nastavení EMS-ESP") // TODO translate
MAKE_WORD_TRANSLATION(watch_cmd, "watch incoming telegrams", "Beobachte eingehende Telegramme", "inkomende telegrammen bekijken", "visa inkommande telegram", "obserwuj przyczodzące telegramy", "se innkommende telegrammer", "", "Gelen telegramları", "guardare i telegrammi in arrivo", "sledovať prichádzajúce telegramy", "sledovat příchozí telegramy") // TODO translate
MAKE_WORD_TRANSLATION(publish_cmd, "publish all to MQTT", "Publiziere MQTT", "publiceer alles naar MQTT", "publicera allt till MQTT", "opublikuj wszystko na MQTT", "Publiser alt til MQTT", "", "Hepsini MQTTye gönder", "pubblica tutto su MQTT", "zverejniť všetko na MQTT", "publikovat vše do MQTT") // TODO translate
MAKE_WORD_TRANSLATION(system_info_cmd, "show system info", "Zeige Systeminformationen", "toon systeemstatus", "visa systeminformation", "pokaż status systemu", "vis system status", "", "Sistem Durumunu Göster", "visualizza stati di sistema", "zobraziť stav systému", "zobrazit informace o systému") // TODO translate
MAKE_WORD_TRANSLATION(schedule_cmd, "enable schedule item", "Aktiviere Zeitplanelemente", "activeer tijdschema item", "aktivera schemalagt objekt", "aktywuj wybrany harmonogram", "", "", "program öğesini etkinleştir", "abilitare l'elemento programmato", "povoliť položku plánovania", "povolit položku plánování") // TODO translate
MAKE_WORD_TRANSLATION(entity_cmd, "set custom value on ems", "Sende eigene Entitäten zu EMS", "verstuur custom waarde naar EMS", "sätt ett eget värde i EMS", "wyślij własną wartość na EMS", "", "", "emp üzerinde özel değer ayarla", "imposta valori personalizzati su EMS", "nastaviť vlastnú hodnotu na ems", "nastavit vlastní hodnotu na ems") // TODO translate
MAKE_WORD_TRANSLATION(commands_response, "get response", "Hole Antwort", "Verzoek om antwoord", "hämta svar", "uzyskaj odpowiedź", "", "", "gelen cevap", "", "získať odpoveď", "získat odpověď") // TODO translate
MAKE_WORD_TRANSLATION(coldshot_cmd, "send a cold shot of water", "Zugabe einer Menge kalten Wassers", "", "sckicka en liten mängd kallvatten", "uruchom tryśnięcie zimnej wody", "", "", "soğuk su gönder", "", "pošlite studenú dávku vody", "poslat studenou vodu") // TODO translate
MAKE_WORD_TRANSLATION(message_cmd, "send a message", "Eine Nachricht senden", "", "skicka ett meddelande", "", "", "", "", "", "poslať správu", "odeslat zprávu") // TODO translate
MAKE_WORD_TRANSLATION(values_cmd, "list all values", "Liste alle Werte auf", "", "lista alla värden", "", "", "", "", "", "vypísať všetky hodnoty", "vypsat všechny hodnoty") // TODO translate
MAKE_WORD_TRANSLATION(system_cmd, "system setting", "System Einstellung", "", "systeminställning", "", "", "", "", "", "vypísať všetky hodnoty", "vypsat všechny hodnoty") // TODO translate
MAKE_WORD_TRANSLATION(showertimer_cmd, "enable shower timer", "aktiviere Duschzeitmessung", "", "aktivera duschtimer", "", "", "", "", "", "povoliť časovač sprchovania", "") // TODO translate
MAKE_WORD_TRANSLATION(showeralert_cmd, "enable shower alert", "aktiviere Duschzeitwarnung", "", "aktivera duschvarning", "", "", "", "", "", "povoliť upozornenie na sprchu", "") // TODO translate
// tags
MAKE_WORD_TRANSLATION(tag_hc1, "hc1", "HK1", "hc1", "VK1", "OG1", "hc1", "hc1", "ID1", "hc1", "hc1", "hc1")
MAKE_WORD_TRANSLATION(tag_hc2, "hc2", "HK2", "hc2", "VK2", "OG2", "hc2", "hc2", "ID2", "hc2", "hc2", "hc2")
MAKE_WORD_TRANSLATION(tag_hc3, "hc3", "HK3", "hc3", "VK3", "OG3", "hc3", "hc3", "ID3", "hc3", "hc3", "hc3")
MAKE_WORD_TRANSLATION(tag_hc4, "hc4", "HK4", "hc4", "VK4", "OG4", "hc4", "hc4", "ID4", "hc4", "hc4", "hc4")
MAKE_WORD_TRANSLATION(tag_hc5, "hc5", "HK5", "hc5", "VK5", "OG5", "hc5", "hc5", "ID5", "hc5", "hc5", "hc5")
MAKE_WORD_TRANSLATION(tag_hc6, "hc6", "HK6", "hc6", "VK6", "OG6", "hc6", "hc6", "ID6", "hc6", "hc6", "hc6")
MAKE_WORD_TRANSLATION(tag_hc7, "hc7", "HK7", "hc7", "VK7", "OG7", "hc7", "hc7", "ID7", "hc7", "hc7", "hc7")
MAKE_WORD_TRANSLATION(tag_hc8, "hc8", "HK8", "hc8", "VK8", "OG8", "hc8", "hc8", "ID8", "hc8", "hc8", "hc8")
MAKE_WORD_TRANSLATION(tag_dhw1, "dhw", "WWK", "dhw", "VVK", "CWU", "dhw", "ecs", "SKS", "dhw", "TÚV", "TUV")
MAKE_WORD_TRANSLATION(tag_dhw2, "dhw2", "WWK2", "dhw2", "VVK2", "CWU2", "dhw2", "ecs2", "SKS2", "dhw2", "TÚV2", "TUV2")
MAKE_WORD_TRANSLATION(tag_dhw3, "dhw3", "WWK3", "dhw3", "VVK3", "CWU3", "dhw3", "ecs3", "SKS3", "dhw3", "TÚV3", "TUV3")
MAKE_WORD_TRANSLATION(tag_dhw4, "dhw4", "WWK4", "dhw4", "VVK4", "CWU4", "dhw4", "ecs4", "SKS4", "dhw4", "TÚV4", "TUV4")
MAKE_WORD_TRANSLATION(tag_dhw5, "dhw5", "WWK5", "dhw5", "VVK5", "CWU5", "dhw5", "ecs5", "SKS5", "dhw5", "TÚV5", "TUV5")
MAKE_WORD_TRANSLATION(tag_dhw6, "dhw6", "WWK6", "dhw6", "VVK6", "CWU6", "dhw6", "ecs6", "SKS6", "dhw6", "TÚV6", "TUV6")
MAKE_WORD_TRANSLATION(tag_dhw7, "dhw7", "WWK7", "dhw7", "VVK7", "CWU7", "dhw7", "ecs7", "SKS7", "dhw7", "TÚV7", "TUV7")
MAKE_WORD_TRANSLATION(tag_dhw8, "dhw8", "WWK8", "dhw8", "VVK8", "CWU8", "dhw8", "ecs8", "SKS8", "dhw8", "TÚV8", "TUV8")
MAKE_WORD_TRANSLATION(tag_dhw9, "dhw9", "WWK9", "dhw9", "VVK9", "CWU9", "dhw9", "ecs9", "SKS9", "dhw9", "TÚV9", "TUV9")
MAKE_WORD_TRANSLATION(tag_dhw10, "dhw10", "WWK10", "dhw10", "VVK10", "CWU10", "dhw10", "ecs10", "SKS10", "dhw10", "TÚV10", "TUV10")
MAKE_WORD_TRANSLATION(tag_ahs1, "ahs1", "AHQ1", "ahs1", "AVK1", "AŹC1", "ahs1", "ahs1", "ahs1", "ahs1", "ahs1", "ahs1")
MAKE_WORD_TRANSLATION(tag_hs1, "hs1", "hs1", "hs1", "VK1", "ŹC1", "hs1", "hs1", "hs1", "hs1", "hs1", "hs1")
MAKE_WORD_TRANSLATION(tag_hs2, "hs2", "hs2", "hs2", "VK2", "ŹC2", "hs2", "hs2", "hs2", "hs2", "hs2", "hs2")
MAKE_WORD_TRANSLATION(tag_hs3, "hs3", "hs3", "hs3", "VK3", "ŹC3", "hs3", "hs3", "hs3", "hs3", "hs3", "hs3")
MAKE_WORD_TRANSLATION(tag_hs4, "hs4", "hs4", "hs4", "VK4", "ŹC4", "hs4", "hs4", "hs4", "hs4", "hs4", "hs4")
MAKE_WORD_TRANSLATION(tag_hs5, "hs5", "hs5", "hs5", "VK5", "ŹC5", "hs5", "hs5", "hs5", "hs5", "hs5", "hs5")
MAKE_WORD_TRANSLATION(tag_hs6, "hs6", "hs6", "hs6", "VK6", "ŹC6", "hs6", "hs6", "hs6", "hs6", "hs6", "hs6")
MAKE_WORD_TRANSLATION(tag_hs7, "hs7", "hs7", "hs7", "VK7", "ŹC7", "hs7", "hs7", "hs7", "hs7", "hs7", "hs7")
MAKE_WORD_TRANSLATION(tag_hs8, "hs8", "hs8", "hs8", "VK8", "ŹC8", "hs8", "hs8", "hs8", "hs8", "hs8", "hs8")
MAKE_WORD_TRANSLATION(tag_hs9, "hs9", "hs9", "hs9", "VK9", "ŹC9", "hs9", "hs9", "hs9", "hs9", "hs9", "hs9")
MAKE_WORD_TRANSLATION(tag_hs10, "hs10", "hs10", "hs10", "VK10", "ŹC10", "hs10", "hs10", "hs10", "hs10", "hs10", "hs10")
MAKE_WORD_TRANSLATION(tag_hs11, "hs11", "hs11", "hs11", "VK11", "ŹC11", "hs11", "hs11", "hs11", "hs11", "hs11", "hs11")
MAKE_WORD_TRANSLATION(tag_hs12, "hs12", "hs12", "hs12", "VK12", "ŹC12", "hs12", "hs12", "hs12", "hs12", "hs12", "hs12")
MAKE_WORD_TRANSLATION(tag_hs13, "hs13", "hs13", "hs13", "VK13", "ŹC13", "hs13", "hs13", "hs13", "hs13", "hs13", "hs13")
MAKE_WORD_TRANSLATION(tag_hs14, "hs14", "hs14", "hs14", "VK14", "ŹC14", "hs14", "hs14", "hs14", "hs14", "hs14", "hs14")
MAKE_WORD_TRANSLATION(tag_hs15, "hs15", "hs15", "hs15", "VK15", "ŹC15", "hs15", "hs15", "hs15", "hs15", "hs15", "hs15")
MAKE_WORD_TRANSLATION(tag_hs16, "hs16", "hs16", "hs16", "VK16", "ŹC16", "hs16", "hs16", "hs16", "hs16", "hs16", "hs16")
MAKE_WORD_TRANSLATION(tag_hcx, "hc<n>", "HK<n>", "hc<n>", "VK<n>", "OG<n>", "hc<n>", "hc<n>", "ID<n>", "hc<n>", "hc<n>", "hc<n>")
MAKE_WORD_TRANSLATION(tag_dhwx, "dhw[n]", "WWK[n]", "dhw[n]", "VVK[n]", "CWU[n]", "dhw[n]", "ecs[n]", "SKS[n]", "dhw[n]", "TÚV[n]", "TUV[n]")
MAKE_WORD_TRANSLATION(tag_ahsx, "ahs<n>", "AHQ<n>", "ahs<n>", "AVK<n>", "AŹC<n>", "ahs<n>", "ahs<n>", "ahs<n>", "ahs<n>", "ahs<n>", "ahs<n>")
MAKE_WORD_TRANSLATION(tag_hsx, "hs<n>", "HQ<n>", "hs<n>", "VK<n>", "ŹC<n>", "hs<n>", "hs<n>", "hs<n>", "hs<n>", "hs<n>", "hs<n>")
// General
MAKE_WORD_TRANSLATION(on, "on", "an", "aan", "", "włączono", "", "on", "ık", "on", "zap", "zap")
MAKE_WORD_TRANSLATION(off, "off", "aus", "uit", "av", "wyłączono", "av", "off", "kapalı", "off", "vyp", "vyp")
MAKE_WORD_TRANSLATION(ON, "ON", "AN", "AAN", "", "wł.", "", "ON", "AÇIK", "ON", "ZAP", "ZAP")
MAKE_WORD_TRANSLATION(OFF, "OFF", "AUS", "UIT", "AV", "wył.", "AV", "OFF", "KAPALI", "OFF", "VYP", "VYP")
// Unit Of Measurement mapping - maps to DeviceValueUOM_s in emsdevice.cpp
// uom - also used with HA see https://github.com/home-assistant/core/blob/d7ac4bd65379e11461c7ce0893d3533d8d8b8cbf/homeassistant/const.py#L384
MAKE_WORD_TRANSLATION(seconds, "seconds", "Sekunden", "Seconden", "Sekunder", "sekund", "Sekunder", "secondes", "saniye", "Secondi", "sekundy", "sekundy")
MAKE_WORD_TRANSLATION(minutes, "minutes", "Minuten", "Minuten", "Minuter", "minut", "Minutter", "minutes", "dakika", "Minuti", "minúty", "minuty")
MAKE_WORD_TRANSLATION(hours, "hours", "Stunden", "Uren", "Timmar", "godzin", "Timer", "heures", "saat", "ore", "hodiny", "hodiny")
MAKE_WORD_TRANSLATION(days, "days", "Tage", "Dagen", "Dagar", "dni", "Dager", "jours", "gün", "giorni", "dni", "dny")
// Enum translations
// general
MAKE_WORD_TRANSLATION(day_mo, "mo", "Mo", "Mo", "", "poniedziałek", "ma", "lun", "Pzt", "lu", "po", "po")
MAKE_WORD_TRANSLATION(day_tu, "tu", "Di", "Di", "Ti", "wtorek", "ti", "mar", "Sal", "ma", "ut", "út")
MAKE_WORD_TRANSLATION(day_we, "we", "Mi", "Wo", "On", "środa", "on", "mer", "Çar", "me", "st", "st")
MAKE_WORD_TRANSLATION(day_th, "th", "Do", "Do", "To", "czwartek", "to", "jeu", "Per", "gio", "št", "čt")
MAKE_WORD_TRANSLATION(day_fr, "fr", "Fr", "Vr", "Fr", "piątek", "fr", "ven", "Cum", "ve", "pi", "")
MAKE_WORD_TRANSLATION(day_sa, "sa", "Sa", "Za", "", "sobota", "", "sam", "Cts", "sa", "so", "so")
MAKE_WORD_TRANSLATION(day_su, "su", "So", "Zo", "", "niedziela", "", "dim", "Paz", "do", "ne", "ne")
MAKE_WORD_TRANSLATION(all, "all", "Alle", "Alle", "Alla", "codziennie", "alle", "tous", "tüm", "tutti", "všetko", "vše")
MAKE_WORD_TRANSLATION(own_1, "own 1", "Eigen 1", "Eigen 1", "Egen 1", "własny 1", "egen 1", "propre 1", "kendi 1", "proprio 1", "vlastné 1", "vlastní 1")
MAKE_WORD_TRANSLATION(family, "family", "Familie", "Familie", "Familj", "rodzina", "familie", "famille", "aile", "famiglia", "rodina", "rodina")
MAKE_WORD_TRANSLATION(morning, "morning", "Morgens", "'s ochtends", "Morgon", "zmiana 1", "morgen", "matin", "sabah", "mattina", "ráno", "ráno")
MAKE_WORD_TRANSLATION(evening, "evening", "Abends", "'s avonds", "Kväll", "zmiana 2", "kveld", "soir", "akşam", "sera", "večer", "večer")
MAKE_WORD_TRANSLATION(seniors, "seniors", "Senioren", "senioren", "Seniorer", "senior", "seniorer", "séniors", "yaşlılar", "vecchi", "seniori", "senioři")
MAKE_WORD_TRANSLATION(no, "no", "nein", "nee", "nej", "nie", "nei", "non", "hayır", "no", "nie", "ne")
MAKE_WORD_TRANSLATION(new, "new", "Neu", "Nieuw", "Ny", "nowy", "ny", "nouveau", "yeni", "nuovo", "nové", "nový")
MAKE_WORD_TRANSLATION(own_2, "own 2", "Eigen 2", "Eigen 2", "Egen 2", "własny 2", "egen 2", "propre 2", "kendi 2", "proprio 2", "vlastné 2", "vlastní 2")
MAKE_WORD_TRANSLATION(singles, "singles", "Einzeln", "singles", "Singlar", "osoba samotna", "single", "seuls", "tekliler", "singoli", "slobodní", "singl")
MAKE_WORD_TRANSLATION(am, "am", "Vormittag", "ochtend", "Förmiddag", "do południa", "formiddag", "matin", "sabah", "mattina", "doobeda", "dopoledne")
MAKE_WORD_TRANSLATION(pm, "pm", "Nachmittag", "namiddag", "Eftermiddag", "po południu", "ettermiddag", "après-midi", "akşam", "pomeriggio", "poobede", "odpoledne")
MAKE_WORD_TRANSLATION(midday, "midday", "Mittag", "middag", "Middag", "południe", "middag", "midi", "öğlen", "mezzogiorno", "poludnie", "poledne")
MAKE_WORD_TRANSLATION(unknown, "unknown", "Unbekannt", "onbekend", "Okänt", "nieznany", "ukjent", "inconnu", "bilinmeyen", "sconosciuto", "neznáme", "neznámé")
MAKE_WORD_TRANSLATION(flat, "flat", "flach", "vlak", "Platt", "płaski", "flat", "plat", "düz", "piatto", "plochý", "plochý")
MAKE_WORD_TRANSLATION(vacuum, "vacuum", "Vakuum", "vacuum", "Vakuum", "próżnia", "vakum", "vide", "vakum", "vacuum", "vákuum", "vakuum")
MAKE_WORD_TRANSLATION(co2_optimized, "co2 optimized", "CO2 optimiert", "CO2 geoptimaliseerd", "CO2-optimerad", "optymalizacja CO2", "co2 optimalisert", "optimisé en CO2", "CO2 verimli", "CO2 ottimizzato", "co2 optimalizované", "optimalizace CO2")
MAKE_WORD_TRANSLATION(cost_optimized, "cost optimized", "kostenoptimiert", "kosten geoptimaliseerd", "kostnadsoptimerad", "optymalizacja kosztów", "kostnadsoptimalisert", "optimisé en coût", "maliyet odaklı", "costo ottimizzato", "nákladovo optimalizované", "optimalizace nákladů")
MAKE_WORD_TRANSLATION(outside_temp_switched, "outside temp switched", "Außentemp. gesteuert", "buitentemp. gestuurd", "Utomhustemp korrigerad", "temperatura zewn. przeł.", "utetemp optimalisert", "contrôle par temp. ext.", "dış hava sıcaklığına bağlı", "temperatura esterna cambiata", "prepnuta vonkajsia teplota", "venkovní teplota přepnuta")
MAKE_WORD_TRANSLATION(co2_cost_mix, "co2 cost mix", "CO2-Kostenmix", "CO2-kostenmix", "CO2-Kostnadsmix", "mieszany koszt CO2", "", "coût mixte CO2", "karışık maliyet", "co2 cost mix", "co2 náklady mix", "mix nákladů CO2") // TODO translate
MAKE_WORD_TRANSLATION(analog, "analog", "analog", "analoog", "analog", "analogowy", "analog", "analogique", "analog", "analogico", "analógový", "analogový")
MAKE_WORD_TRANSLATION(normal, "normal", "normal", "normaal", "normal", "normalny", "normal", "normal", "normal", "normale", "normálny", "normální")
MAKE_WORD_TRANSLATION(blocking, "blocking", "Blockierung", "blokkering", "Blockering", "blokowanie", "blokkering", "bloquant", "engelleme", "bloccaggio", "blokovanie", "blokování")
MAKE_WORD_TRANSLATION(extern, "extern", "extern", "extern", "extern", "zewnętrzny", "ekstern", "externe", "dış", "eesterno", "externý", "externí")
MAKE_WORD_TRANSLATION(intern, "intern", "intern", "intern", "intern", "wewnętrzny", "intern", "interne", "", "interno", "interný", "interní")
MAKE_WORD_TRANSLATION(lower, "lower", "niedriger", "lager", "lägre", "mniejszy", "nedre", "inférieur", "daha düşük", "basso", "nízky", "nízký")
MAKE_WORD_TRANSLATION(error, "error", "Fehler", "error", "Fel", "błąd", "feil", "erreur", "Hata", "errore", "error", "chyba")
MAKE_WORD_TRANSLATION(history, "history", "Fehlerspeicher", "", "historik", "historia", "", "", "", "", "história", "historie") // TODO translate
MAKE_WORD_TRANSLATION(message, "message", "Meldung", "melding", "meddelande", "komunikat", "melding", "message", "mesajı", "messaggio", "správa", "zpráva")
MAKE_WORD_TRANSLATION(na, "n/a", "n/a", "n/a", "n/a", "nd.", "n/a", "n/c", "mevcut değil", "n/a", "n/a", "n/a")
MAKE_WORD_TRANSLATION(inverted, "inverted", "invertiert", "", "inverterad", "odwrócony", "", "", "", "", "invertovaný", "invertovaný") // TODO translate
// boiler
MAKE_WORD_TRANSLATION(time, "time", "Zeit", "tijd", "Tid", "godzina", "tid", "heure", "zaman", "ora", "čas", "čas")
MAKE_WORD_TRANSLATION(date, "date", "Datum", "datum", "Datum", "data", "dato", "date", "tarih", "data", "dátum", "datum")
MAKE_WORD_TRANSLATION(continuous, "continuous", "kontinuierlich", "continue", "kontinuerlig", "ciągły", "kontinuerlig", "continu", "devam eden", "continuo", "nepretržité", "nepretržitě")
MAKE_WORD_TRANSLATION(3wayvalve, "3-way valve", "3-Wege-Ventil", "3-weg klep", "trevägsventil", "zawór 3-drogowy", "treveisventil", "vanne 3 voies", "3 yollu vana", "valvola 3 vie", "3-cestný ventil", "3-cestný ventil")
MAKE_WORD_TRANSLATION(chargepump, "chargepump", "Ladepumpe", "laadpomp", "laddpump", "pompa ładująca", "ladepumpe", "pompe de charge", "besleme pompası", "pompa di carica", "nabíjacie čerpadlo", "nabíjecí čerpadlo")
MAKE_WORD_TRANSLATION(hot, "hot", "Heiß", "heet", "Het", "gorący", "het", "chaud", "sıcak", "caldo", "horúci", "horký")
MAKE_WORD_TRANSLATION(high_comfort, "high comfort", "gehobener Komfort", "verhoogd comfort", "Förhöjd komfort", "wysoki komfort", "høy komfort", "comfort", "komfor", "comfort alto", "vysoký komfort", "vysoký komfort")
MAKE_WORD_TRANSLATION(eco, "eco", "Eco", "Eco", "Eko", "eko", "øko", "éco", "eko", "Eco", "eko", "eko")
MAKE_WORD_TRANSLATION(ecoplus, "eco+", "Eco+", "Eco+", "Eko+", "eko+", "øko+", "éco+", "eko+", "Eco+", "eko+")
MAKE_WORD_TRANSLATION(intelligent, "intelligent", "Intelligent", "intelligent", "Intelligent", "inteligentny", "intelligent", "intelligent", "akıllı", "intelligente", "inteligentný", "inteligentní")
MAKE_WORD_TRANSLATION(flow, "flow", "Durchfluss", "volumestroom", "Flöde", "przepływ", "strømme", "débit", "akım", "flusso", "tok", "průtok")
MAKE_WORD_TRANSLATION(manual, "manual", "Manuell", "handmatig", "Manuell", "ręczny", "manuell", "manuel", "manuel", "manuale", "manuálny", "manuální")
MAKE_WORD_TRANSLATION(buffer, "buffer", "Speicher", "buffer", "Buffert", "bufor", "buffer", "buffer", "tampon", "Buffer", "zásobník", "zásobník")
MAKE_WORD_TRANSLATION(bufferedflow, "buffered flow", "Durchlaufspeicher", "doorstroombuffer", "Buffertflöde", "przepływ buforowany", "bufret strømning", "", "tampon akım", "memoria flusso", "zásobníkový prietok", "zásobníkový průtok") // TODO translate
MAKE_WORD_TRANSLATION(layeredbuffer, "layered buffer", "Schichtspeicher", "gelaagde buffer", "Lagrad buffert", "bufor warstwowy", "lagdelt buffer", "", "katmanlı akım", "strato memoria", "vrstvený zásobník", "vrstvený zásobník") // TODO translate
MAKE_WORD_TRANSLATION(maintenance, "maintenance", "Wartung", "onderhoud", "Underhåll", "przegląd", "vedlikehold", "maintenance", "bakım", "servizio", "údržba", "údržba")
MAKE_WORD_TRANSLATION(heating, "heating", "Heizen", "verwarmen", "Uppvärmning", "ogrzewanie", "oppvarming", "chauffage", "ısıtma", "riscaldamento", "kúrenie", "topení")
MAKE_WORD_TRANSLATION(cooling, "cooling", "Kühlen", "koelen", "Kyler", "chłodzenie", "kjøling", "refroidissement", "soğuma", "raffreddamento", "chladenie", "chlazení")
MAKE_WORD_TRANSLATION(heatandcool, "heating & cooling", "Heizen & Kühlen", "verwarmen & koelen", "Uppvärmning & Kyla", "ogrzewanie i chłodzenie", "", "", "ısıtma & soğutma", "", "kúrenie a chladenie", "topení a chlazení") // TODO translate
MAKE_WORD_TRANSLATION(disinfecting, "disinfecting", "Desinfizieren", "desinfecteren", "Desinficerar", "dezynfekcja termiczna", "desinfisering", "désinfection", "dezenfeksiyon", "disinfezione", "dezinfekcia", "dezinfekce")
MAKE_WORD_TRANSLATION(no_heat, "no heat", "keine Wärme", "geen warmte", "Ingen värme", "brak ciepła", "ingen varme", "pas de chauffage", "ısınma yok", "nessun calore", "žiadne teplo", "bez tepla")
MAKE_WORD_TRANSLATION(heatrequest, "heat request", "Wärmeanforderung", "verwarmingsverzoek", "Värmeförfrågan", "zapotrzebowanie na ciepło", "varmeforespørsel", "demande de chauffage", "ısınma ihtiyacı", "richiesta calore", "požiadavka na teplo", "požadavek na teplo")
MAKE_WORD_TRANSLATION(valve, "valve", "Ventil", "klep", "Ventil", "zawór", "ventil", "valve", "vana", "valvola", "ventil", "ventil")
MAKE_WORD_TRANSLATION(proportional, "proportional", "proportional", "proportioneel", "proportionell", "proporcjonalny", "proposjonal", "", "oransal", "proporzionale", "proporcionálne", "proporcionální") // TODO translate
MAKE_WORD_TRANSLATION(deltaP1, "deltaP-1", "deltaP-1", "deltaP-1", "deltaP-1", "delta P-1", "deltaP-1", "", "deltaP-1", "deltaP-1", "deltaP-1", "deltaP-1") // TODO translate
MAKE_WORD_TRANSLATION(deltaP2, "deltaP-2", "deltaP-2", "deltaP-2", "deltaP-2", "delta P-2", "deltaP-2", "", "deltaP-2", "deltaP-2", "deltaP-2", "deltaP-2") // TODO translate
MAKE_WORD_TRANSLATION(deltaP3, "deltaP-3", "deltaP-3", "deltaP-3", "deltaP-3", "delta P-3", "deltaP-3", "", "deltaP-3", "deltaP-3", "deltaP-3", "deltaP-3") // TODO translate
MAKE_WORD_TRANSLATION(deltaP4, "deltaP-4", "deltaP-4", "deltaP-4", "deltaP-4", "delta P-4", "deltaP-4", "", "deltaP-4", "deltaP-4", "deltaP-4", "deltaP-4") // TODO translate
MAKE_WORD_TRANSLATION(pressure1, "150mbar", "150mbar", "150mbar", "150mbar", "150mbar", "150mbar", "150mbar", "150mbar", "150mbar", "150mbar", "150mbar") // TODO translate
MAKE_WORD_TRANSLATION(pressure2, "200mbar", "200mbar", "200mbar", "200mbar", "200mbar", "200mbar", "200mbar", "200mbar", "200mbar", "200mbar", "200mbar") // TODO translate
MAKE_WORD_TRANSLATION(pressure3, "250mbar", "250mbar", "250mbar", "250mbar", "250mbar", "250mbar", "250mbar", "250mbar", "250mbar", "250mbar", "250mbar") // TODO translate
MAKE_WORD_TRANSLATION(pressure4, "300mbar", "300mbar", "300mbar", "300mbar", "300mbar", "300mbar", "300mbar", "300mbar", "300mbar", "300mbar", "300mbar") // TODO translate
MAKE_WORD_TRANSLATION(pressure5, "350mbar", "350mbar", "350mbar", "350mbar", "350mbar", "350mbar", "350mbar", "350mbar", "350mbar", "350mbar", "350mbar") // TODO translate
MAKE_WORD_TRANSLATION(pressure6, "400mbar", "400mbar", "400mbar", "400mbar", "400mbar", "400mbar", "400mbar", "400mbar", "400mbar", "400mbar", "400mbar") // TODO translate
// heatpump
MAKE_WORD_TRANSLATION(none, "none", "keine", "geen", "ingen", "brak", "ingen", "aucun", "hiçbiri", "nessuno", "žiadny", "žádné")
MAKE_WORD_TRANSLATION(hot_water, "hot water", "Warmwasser", "warm water", "varmvatten", "c.w.u.", "varmtvann", "eau chaude", "sıcak su", "acqua calda", "horúca voda", "horká voda")
MAKE_WORD_TRANSLATION(pool, "pool", "Pool", "zwembad", "pool", "basen", "basseng", "piscine", "havuz", "piscina", "bazén", "bazén")
MAKE_WORD_TRANSLATION(pool_heating, "pool heating", "Pool beheizen", "", "", "", "", "", "", "", "ohrev bazéna", "")
MAKE_WORD_TRANSLATION(outside_temp_alt, "outside temperature alt.", "Außentemp. alternativ", "alternatieve buitentemperatuur", "Alternativ utomhustemp.", "temp. zewn. alternat.", "alternativ utendørstemp.", "température extérieure alternative", "alternatif dış sıcaklık", "temperatura esterna alternativa", "vonkajšia teplota altern.", "venkovní teplota alt.")
MAKE_WORD_TRANSLATION(outside_temp_par, "outside temperature parallel", "Außentemp. parallel", "buitentemperatuur parallel", "Parallell utomhustemp.", "temp. zewn. równoległa", "parallell utendørstemp.", "température extérieure parallèle", "paralel dış sıcaklık", "temperatura esterna parallela", "paralelne s vonkajšou teplotou", "paralelně s venkovní teplotou")
MAKE_WORD_TRANSLATION(hp_preferred, "heatpump preferred", "Wärmepumpe bevorzugt", "voorkeur warmtepomp", "Värmepump föredraget", "preferowana pompa ciepła", "varmepumpe prioritert", "pompe à chaleur préférée", "tercih edilen pompa", "pompa di calore preferita", "preferované tepelné čerpadlo", "preferované tepelné čerpadlo")
MAKE_WORD_TRANSLATION(boiler_only, "boiler only", "nur Kessel", "uitsluitend cv ketel", "Värmepanna enbart", "tylko kocioł", "kun kjele", "chaudière uniquement", "sadece kazan", "solo caldaia", "len kotol", "pouze kotel")
MAKE_WORD_TRANSLATION(reduced_output, "reduced output", "Reduzierte Leistung", "gereduceerde output", "Reducerad produktion", "zmniejszona wydajność", "redusert ytelse", "sortie réduite", "düşürülmüş çıkış", "riduzione uscita", "znížený výkon", "snížený výkon")
MAKE_WORD_TRANSLATION(switchoff, "switch off hp", "WP ausschalten", "WP uitschakelen", "Värmepump avstängd", "wyłącz pompę ciepła", "slå av varmepumpe", "éteindre la PAC", "ısı pompasını kapat", "spegnimento pompa calore", "vypnúť tep. čerpadlo", "vypnout tepelné čerpadlo")
MAKE_WORD_TRANSLATION(perm, "perm. reduced", "perm. reduziert", "permanent gereduceerd", "Permanent reducerad", "stale zmniejszona wydajność", "permanent redusert", "réduction permanente", "sürekli azaltılmış", "riduzione permanente", "trvalo znížené", "trvale sníženo")
MAKE_WORD_TRANSLATION(heat_ww, "heating & dhw", "Heizen & Warmwasser", "", "Uppvärmning & Varmvatten", "ogrzewanie i c.w.u.", "", "", "", "", "kúrenie a TÚV", "topení a TUV")
MAKE_WORD_TRANSLATION(cool_defrost, "cooling & defrost", "Kühlen & Abtauen", "", "Kyla & Avfrostning", "chłodzenie i odladzanie", "", "", "", "", "chladenie a rozmrazovanie", "chlazení a odmrazování")
MAKE_WORD_TRANSLATION(compressor_alarm, "compressor alarm", "Alarm Kompressor", "", "", "", "", "", "", "", "alarm kompresora", "")
// thermostat
MAKE_WORD_TRANSLATION(seltemp, "selTemp", "Solltemperatur", "doeltemperatuur", "Börtemperatur", "temperatura zadana", "innstilt temperatur", "consigne température", "ayarlanmış sıcaklık", "temperatura di consegna", "zadaná teplota", "zvolená teplota")
MAKE_WORD_TRANSLATION(roomtemp, "roomTemp", "Raumtemperatur", "kamertemperatuur", "Rumstemperatur", "temperatura w pomieszczeniu", "romstemperatur", "température de la pièce", "oda sıcaklığı", "temperatura camera", "izbová teplota", "teplota místnosti")
MAKE_WORD_TRANSLATION(own_prog, "own prog", "Eigenprog.", "eigen prog.", "Egen prog.", "program własny", "eget prog.", "programme propre", "isteğe göre ayarlanmış program", "proprio prog.", "vlastný prog.", "vlastní program")
MAKE_WORD_TRANSLATION(std_prog, "std prog", "Standardprog.", "standaard prog.", "Standardprog.", "program standardowy", "standardprog.", "programme standard", "sandart pogram", "programma standard", "Štandar.prog.", "standardní program")
MAKE_WORD_TRANSLATION(light, "light", "Leicht", "licht", "Lätt", "lekki", "lett", "léger", "düşük", "luce", "ľahký", "lehký")
MAKE_WORD_TRANSLATION(medium, "medium", "Mittel", "middel", "Medel", "średni", "medium", "medium", "orta", "medio", "stredný", "střední")
MAKE_WORD_TRANSLATION(heavy, "heavy", "Schwer", "zwaar", "Tung", "ciężki", "tung", "lourd", "yüksek", "pesante", "ťažký", "těžký")
MAKE_WORD_TRANSLATION(start, "start", "Start", "start", "Start", "start", "start", "début", "başlat", "avvia", "štart", "start")
MAKE_WORD_TRANSLATION(heat, "heat", "Heizen", "verwarmen", "Värme", "ciepło", "varmer", "chaleur", "ısıtma", "caldo", "kúrenie", "topení")
MAKE_WORD_TRANSLATION(hold, "hold", "Halten", "pauzeren", "Paus", "pauza", "pause", "pause", "durdur", "pausa", "pauza", "pausa")
MAKE_WORD_TRANSLATION(cool, "cool", "Kühlen", "koelen", "Kyla", "zimno", "kjøler", "froid", "soğutma", "freddo", "chladenie", "chladit")
MAKE_WORD_TRANSLATION(end, "end", "Ende", "einde", "Slut", "koniec", "slutt", "fin", "bitti", "fine", "koniec", "konec")
MAKE_WORD_TRANSLATION(german, "german", "Deutsch", "Duits", "Tyska", "niemiecki", "tysk", "allemand", "Almanca", "Tedesco", "nemecky", "němčina")
MAKE_WORD_TRANSLATION(dutch, "dutch", "Niederländisch", "Nederlands", "Nederländska", "niderlandzki", "nederlandsk", "néerlandais", "Flemenkçe", "Olandese", "holandsky", "nizozemština")
MAKE_WORD_TRANSLATION(french, "french", "Französisch", "Frans", "Franska", "francuski", "fransk", "français", "Fransızca", "Francese", "francúzsky", "francouzština")
MAKE_WORD_TRANSLATION(italian, "italian", "Italienisch", "Italiaans", "Italienska", "włoski", "italiensk", "italien", "İtalyanca", "Italiano", "taliansky", "italština")
MAKE_WORD_TRANSLATION(high, "high", "hoch", "hoog", "Hög", "wysoki", "høy", "haut", "yüksek", "alto", "vysoký", "vysoký")
MAKE_WORD_TRANSLATION(low, "low", "niedrig", "laag", "Låg", "niski", "lav", "bas", "düşük", "basso", "nízky", "nízký")
MAKE_WORD_TRANSLATION(curve, "heatingcurve", "Heizkurve", "", "värmekurva", "krzywa grzania", "", "", "", "", "vykurovacia krivka", "křivka topení") // TODO translate
MAKE_WORD_TRANSLATION(radiator, "radiator", "Heizkörper", "radiator", "Radiator", "grzejniki", "radiator", "radiateur", "radyatör", "radiatore", "radiátor", "radiátor")
MAKE_WORD_TRANSLATION(convector, "convector", "Konvektor", "convector", "Konvektor", "konwektory", "konvektor", "convecteur", "convector", "convettore", "konvektor", "konvektor")
MAKE_WORD_TRANSLATION(floor, "floor", "Fussboden", "vloer", "Golv", "podłoga", "gulv", "sol", "yer", "pavimento", "podlaha", "podlaha")
MAKE_WORD_TRANSLATION(roomflow, "roomflow", "Raum Fluß", "", "Rumsflöde", "", "", "", "", "", "prúdenie miestnosti", "") // TODO translate
MAKE_WORD_TRANSLATION(roomload, "roomload", "Raum Bedarf", "", "Rumsbehov", "", "", "", "", "", "izbová zaťaž", "") // TODO translate
MAKE_WORD_TRANSLATION(summer, "summer", "Sommer", "zomer", "Sommar", "lato", "sommer", "été", "yaz", "estate", "leto", "léto")
MAKE_WORD_TRANSLATION(winter, "winter", "Winter", "winter", "Vinter", "zima", "vinter", "hiver", "kış", "inverno", "zima", "zima")
MAKE_WORD_TRANSLATION(outdoor, "outdoor", "Außen", "buiten", "Utomhus", "temp. zewnętrzna", "utendørs", "extérieur", "dış", "esterno", "vonku", "venkovní")
MAKE_WORD_TRANSLATION(room, "room", "Raum", "kamer", "Rum", "temp. w pomieszczeniu", "", "pièce", "oda", "camera", "izba", "dle teploty prostoru") // TODO translate
MAKE_WORD_TRANSLATION(room_outdoor, "room outdoor", "Raum + Außen", "kamer + buiten", "Rum + Ute", "temp. w pom. i zewn.", "rom utendørs", "pièce extérieure", "oda ve dış", "camera esterna", "miestnosť vonku", "místnost venku")
MAKE_WORD_TRANSLATION(power, "power", "Leistung", "vermogen", "Effekt", "moc", "effekt", "puissance", "güç", "potenza", "výkon", "výkon")
MAKE_WORD_TRANSLATION(constant, "constant", "konstant", "constant", "Konstant", "stały", "konstant", "constant", "sabit", "costante", "konštantný", "konstantní")
MAKE_WORD_TRANSLATION(simple, "simple", "einfach", "simpel", "enkel", "prosty", "enkel", "simple", "basit", "semplice", "jednoduchý", "jednoduchý")
MAKE_WORD_TRANSLATION(optimized, "optimized", "optimiert", "geoptimaliseerd", "optimerad", "zoptymalizowany", "optimalisert", "optimisé", "optimize", "ottimizzato", "optimalizovaný", "optimalizovaný")
MAKE_WORD_TRANSLATION(nofrost, "nofrost", "Frostschutz", "vorstbescherming", "Frostskydd", "ochrona przed zamarzaniem", "frostsikring", "protection gel", "Donma koruması", "protezione gelo", "bez námrazy", "bez mrazu")
MAKE_WORD_TRANSLATION(defrost, "defrost", "Abtauen", "ontdooien", "avfrostning", "odladzanie", "tine", "dégivrage", "buz çözücü", "scongelamento", "odmrazenie", "odmrazování")
MAKE_WORD_TRANSLATION(comfort, "comfort", "Komfort", "comfort", "Komfort", "komfort", "komfort", "comfort", "konfor", "comfort", "komfortný", "komfort")
MAKE_WORD_TRANSLATION(night, "night", "Nacht", "nacht", "Natt", "noc", "natt", "nuit", "gece", "notte", "noc", "noc")
MAKE_WORD_TRANSLATION(day, "day", "Tag", "dag", "Dag", "dzień", "dag", "jour", "gün", "giorno", "deň", "den")
MAKE_WORD_TRANSLATION(holiday, "holiday", "Urlaub", "vakantie", "Helgdag", "urlop", "ferie", "vacances", "tatil", "vacanza", "dovolenka", "dovolená")
MAKE_WORD_TRANSLATION(reduce, "reduce", "reduziert", "gereduceerd", "Reducera", "zredukowany", "redusere", "réduit", "düşür", "riduzione", "znížený", "snížený")
MAKE_WORD_TRANSLATION(noreduce, "no reduce", "unreduziert", "niet gereduceerd", "oreducerad", "bez redukcji", "ingen reduksjon", "pas de réduction", "düşürme", "non ridurre", "bez zníženia", "nesnížený")
MAKE_WORD_TRANSLATION(offset, "offset", "Anhebung", "offset", "Förskjutning", "przesunięcie", "kompensasjon", "offset", "kompansasyon", "offset", "ofset", "offset")
MAKE_WORD_TRANSLATION(design, "design", "Auslegung", "ontwerp", "Design", "projekt", "design", "design", "tasarım", "disegno", "design", "design")
MAKE_WORD_TRANSLATION(minflow, "min flow", "min. Durchfluss", "min. doorstroom", "Min. flöde", "minimalny przepływ", "min. strømming", "flux min.", "minimum akış", "flusso minimo", "min. prietok", "minimální průtok")
MAKE_WORD_TRANSLATION(maxflow, "max flow", "max. Durchfluss", "max. doorstroom", "Max. flöde", "maksymalny przepływ", "maks. strømming", "flux max.", "maksimum akış", "flusso massimo", "max. prietok", "maximální průtok")
MAKE_WORD_TRANSLATION(fast, "fast", "schnell", "snel", "snabb", "szybkie", "hurtig", "rapide", "hızlı", "veloce", "rýchly", "rychlý")
MAKE_WORD_TRANSLATION(slow, "slow", "langsam", "langzaam", "långsam", "powolne", "langsom", "lent", "yavaş", "lento", "pomalý", "pomalý")
MAKE_WORD_TRANSLATION(internal_temperature, "internal temperature", "Interne Temperatur", "interne temperatuur", "Interntemperatur", "temperatura wewnętrzna", "interntemperatur", "température interne", "oda sıcaklığı", "temperatura interna", "vnútorná teplota", "vnitřní teplota")
MAKE_WORD_TRANSLATION(internal_setpoint, "internal setpoint", "Interner Sollwert", "interne streeftemperatuur", "Internt börvärde", "nastawa wewnętrzna", "internt settpunkt", "consigne interne", "istenen oda sıcaklığı", "setpoint interno", "vnútorná pož. hodnota", "vnitřní požadovaná hodnota")
MAKE_WORD_TRANSLATION(external_temperature, "external temperature", "Externe Temperatur", "externe temperatuur", "Extern temperatur", "temperatura zewnętrzna", "ekstern temperatur", "température externe", "dış sıcaklık", "temperatura esterna", "vonkajšie teplota", "vnější teplota")
MAKE_WORD_TRANSLATION(burner_temperature, "burner temperature", "Brennertemperatur", "brander temperatuur", "Brännartemperatur", "temperatura palnika", "brennertemperatur", "température du brûleur", "kazan sıcaklığı", "temperatura bruciatore", "teplota horáka", "teplota hořáku")
MAKE_WORD_TRANSLATION(ww_temperature, "dhw temperature", "Wassertemperatur", "watertemperatuur", "Vattentemperatur", "temperatura c.w.u.", "vanntemperatur", "température de l'eau", "Kullanım suyu sıcaklığı", "temperatura acqua", "teplota vody", "teplota TUV")
MAKE_WORD_TRANSLATION(smoke_temperature, "smoke temperature", "Abgastemperatur", "rookgastemperatuur", "Rökgastemperatur", "temperatura dymu", "røykgasstemperatur", "température des gaz d'échappement", "baca gazı sıcaklığı", "temperatura fumo", "teplota dymu", "teplota spalin")
MAKE_WORD_TRANSLATION(weather_compensated, "weather compensated", "Wetter kompensiert", "weer gecompenseerd", "Väderkompenserad", "skompensow. pogodą", "værkompensert", "compensation par l'extérieur", "hava durumuna göre dengelenmiş", "acqua compensata", "kompenzácia počasia", "dle venkovní teploty")
MAKE_WORD_TRANSLATION(outside_basepoint, "outside basepoint", "Basispunkt Außentemp.", "buiten basispunt", "Utomhus baspunkt", "temp. zewn. z pkt. pocz.", "utendørs basispunkt", "point de base temp. ext.", "dış hava sıcaklığı taban noktası", "basepoint esterno", "vonkajší základný bod", "venkovní teplota s patním bodem")
MAKE_WORD_TRANSLATION(functioning_mode, "functioning mode", "Funktionsweise", "functiemodus", "Driftläge", "tryb pracy", "driftsmodus", "mode de fonctionnement", "işletme konumu", "modalità di funzionamento", "funkčný režim", "provozní režim")
MAKE_WORD_TRANSLATION(unmixed, "unmixed", "ungemischt", "", "oshuntad", "niezmieszany", "", "", "", "", "nezmiešaný", "nemíchaný") // TODO translate
MAKE_WORD_TRANSLATION(unmixedIPM, "unmixed IPM", "ungemischt IPM", "", "oshuntad IPM", "niezmieszany IPM", "", "", "", "", "nezmiešaný IPM", "nemíchaný IPM") // TODO translate
MAKE_WORD_TRANSLATION(mixed, "mixed IPM", "gemischt IPM", "", "shuntad IPM", "zmieszany IPM", "", "", "", "", "zmiešaný IPM", "míchaný IPM") // TODO translate
MAKE_WORD_TRANSLATION(level, "level", "Level", "", "nivå", "", "", "", "", "", "úroveň", "úroveň") // TODO translate
MAKE_WORD_TRANSLATION(absolute, "absolute", "Absolut", "", "absolut", "", "", "", "", "", "absolútny", "absolutní") // TODO translate
// mixer
MAKE_WORD_TRANSLATION(stopped, "stopped", "gestoppt", "gestopt", "stoppad", "zatrzymany", "stoppet", "arrêté", "durdu", "fermato", "zastavený", "zastavené")
MAKE_WORD_TRANSLATION(opening, "opening", "öffnen", "openen", "öppnar", "otwieranie", "åpner", "ouverture", "ılıyor", "aperto", "otvorenie", "otevřené")
MAKE_WORD_TRANSLATION(closing, "closing", "schließen", "sluiten", "stänger", "zamykanie", "stenger", "fermeture", "kapanıyor", "chiuso", "zatvorenie", "zavřené")
MAKE_WORD_TRANSLATION(open, "open", "offen", "open", "Öppen", "otwórz", "åpen", "ouvert", "ık", "aprire", "otvoriť", "otevřít")
MAKE_WORD_TRANSLATION(close, "close", "geschlossen", "Gesloten", "Stängd", "zamknij", "stengt", "fermé", "kapalı", "chiudere", "zatvoriť", "zavřít")
// solar dhw
MAKE_WORD_TRANSLATION(cyl1, "cyl 1", "Zyl_1", "Cil 1", "Cyl 1", "cyl 1", "cyl 1", "cyl 1", "cly 1", "Cil 1", "cyl 1", "")
MAKE_WORD_TRANSLATION(cyl2, "cyl 2", "Zyl_2", "Cil 2", "Cyl 2", "cyl 2", "cyl 2", "cyl 2", "cly 1", "Cil 2", "cyl 2", "")
// ventilation
MAKE_WORD_TRANSLATION(demand, "demand", "Bedarf", "vereist", "behov", "zapotrzebowanie", "", "", "talep", "richiesta", "požiadavka", "") // TODO translate
MAKE_WORD_TRANSLATION(intense, "intense", "Intensiv", "intensief", "intensiv", "intensywne", "", "", "yoğun", "intensivo", "intenzívne", "") // TODO translate
MAKE_WORD_TRANSLATION(sleep, "sleep", "Einschlafen", "slaapmodus", "sova", "sen", "", "", "uyku", "notturno", "spiace", "") // TODO translate
MAKE_WORD_TRANSLATION(partymode, "party", "Party", "party", "party", "impreza", "", "", "parti", "festa", "párty režim", "") // TODO translate
MAKE_WORD_TRANSLATION(fireplace, "fireplace", "Kamin", "haard", "Kamin", "kominek", "", "", "şömine", "camino", "krb", "") // TODO translate
// MQTT Discovery - this is special device entity for 'climate'
MAKE_TRANSLATION(haclimate, "haclimate", "mqtt discovery current room temperature", "Discovery aktuelle Raumtemperatur", "Discovery huidige kamertemperatuur", "MQTT Discovery för aktuell rumstemperatur", "termostat w HA", "HA Avlest temp", "", "Güncel osa sıcaklığı", "verifica temperatura ambiente attuale", "mqtt discovery aktuálna teplota v miestnosti", "mqtt discovery aktuální pokojová teplota") // TODO translate
// Entity translations: tag, mqtt, en, de, nl, sv, pl, no, fr, tr, it, sk, cz
// Boiler
MAKE_TRANSLATION(reset, "reset", "reset", "Reset", "Reset", "Återställ", "kasowanie komunikatu", "nullstill", "reset", "Sıfırla", "Reset", "reset", "reset")
MAKE_TRANSLATION(forceHeatingOff, "heatingoff", "force heating off", "Heizen abschalten", "", "Uppvärmning avstängd", "wymuś wyłączenie grzania", "", "", "", "", "vynútené vypnutie kúrenia", "vynutit vypnutí vytápění") // TODO translate
MAKE_TRANSLATION(wwtapactivated, "tapactivated", "turn on/off", "Durchlauferhitzer aktiv", "zet aan/uit", "på/av", "system przygotowywania", "Varmtvann active", "ecs activée", "aç/kapa", "commuta on/off", "zapnúť/vypnúť", "zapnout/vypnout")
MAKE_TRANSLATION(oilPreHeat, "oilpreheat", "oil preheating", "Ölvorwärmung", "Olie voorverwarming", "Förvärmning olja", "podgrzewanie oleju", "oljeforvarming", "préchauffage de l'huile", "Yakıt Ön ısıtma devrede", "preriscaldamento olio", "predohrev oleja", "předehřev oleje")
MAKE_TRANSLATION(heatingActive, "heatingactive", "heating active", "Heizen aktiv", "Verwarming actief", "Uppvärmning aktiv", "c.o. aktywne", "oppvarming aktiv", "chauffage actif", "ısıtma devrede", "riscaldamento attivo", "vykurovanie aktívne", "Vytápění aktivní")
MAKE_TRANSLATION(heatingOn, "heating", "heating", "Heizen", "verwarmen", "Uppvärmning", "ogrzewanie", "oppvarming", "chauffage", "ısıtma", "riscaldamento", "vykurovanie", "Vytápění")
MAKE_TRANSLATION(tapwaterActive, "tapwateractive", "tapwater active", "Warmwasser aktiv", "Warm water actief", "Varmvatten aktiv", "c.w.u. aktywne", "varmtvann aktiv", "eau chaude active", "sıcak kullanım suyu devrede", "acqua calda attiva", "TÚV aktívna", "teplá voda aktivní")
MAKE_TRANSLATION(selBurnPow, "selburnpow", "burner selected max power", "Eingestellte maximale Brennerleistung", "Ingestelde maximale brandervermogen", "Brännare vald maxeffekt", "wybrana moc źródła ciepła", "settpunkt brennerkapasitet", "puissance max du brûleur selectionnée", "seçili kazan maksimum güç", "Setpoint potenza bruciatore", "horák zvolený maximálny výkon", "zvolený maximální výkon hořáku")
MAKE_TRANSLATION(absBurnPow, "absburnpow", "burner current power (absolute)", "Aktuelle Brennerleistung (absolut)", "Brandervermogen (abs)", "Värmepanna aktuell effekt (abs)", "aktualna moc źródła ciepła (absolutna)", "brennereffekt", "puissance du brûleur actuelle (abs)", "kazan anlık akış gücü(mutlak)", "potenza attuale del bruciatore (assoluta)", "aktuálny výkon horáka (absolútny)", "aktuální výkon hořáku (absolutní)")
MAKE_TRANSLATION(heatingPumpMod, "heatingpumpmod", "heating pump modulation", "Modulation Heizungspumpe", "Modulatie verwarmingspomp", "Modulering Värmepump", "wysterowanie pompy c.o.", "varmepumpemodulering", "modulation de la pompe à chaleur", "ısı pompası modülasyonu", "modulazione pompa di calore", "modulácia tepelného čerpadla", "modulace čerpadla topení")
MAKE_TRANSLATION(outdoorTemp, "outdoortemp", "outside temperature", "Außentemperatur", "Buitentemperatuur", "Utomhustemperatur", "temperatura zewnętrzna", "utetemperatur", "température extérieure", "dış ortam sıcaklığı", "tempertura esterna", "vonkajšia teplota", "venkovní teplota")
MAKE_TRANSLATION(selFlowTemp, "selflowtemp", "selected flow temperature", "Gewählte Vorlauftemperatur", "Ingestelde aanvoertemperatuur", "Börvärde Flödestemperatur", "wybrana temperatura zasilania", "valgt turtemperatur", "température de flux selectionnée", "seçili akış sıcaklığı", "flusso temperatura selezionato", "zvolená teplota na výstupe", "zvolená výstupní teplota ")
MAKE_TRANSLATION(curFlowTemp, "curflowtemp", "current flow temperature", "Aktuelle Vorlauftemperatur", "Huidige aanvoertemperatuur", "Flödestemperatur", "temperatura zasilania", "aktuell strømmetemperatur", "température actuelle du flux", "akış sıcaklığı", "temperatura di mandata attuale", "aktuálna výstupná teplota", "naměřená výstupní teplota")
MAKE_TRANSLATION(retTemp, "rettemp", "return temperature", "Rücklauftemperatur", "Retourtemperatuur", "Returtemperatur", "temperatura powrotu", "returtemperatur", "température de retour", "dönüş sıcaklığı", "temperatura di ritorno attuale", "teplota spiatočky", "teplota zpátečky")
MAKE_TRANSLATION(switchTemp, "switchtemp", "mixing switch temperature", "Mischerschalttemperatur", "Mixer temperatuur", "Blandartemperatur", "temperatura przełączania mieszacza", "Blandertemperatur", "température de bascule du mélangeur", "karışım hücresi sıcaklığı", "Temperatura di commutazione del miscelatore", "teplota zmiešavacieho spínača", "teplota směšovacího rozdělovače")
MAKE_TRANSLATION(sysPress, "syspress", "system pressure", "Systemdruck", "Systeemdruk", "Systemtryck", "ciśnienie w systemie", "systemtrykk", "pression du système", "sistem basıncı", "pressione sistema", "tlak v systéme", "tlak v systému")
MAKE_TRANSLATION(boilTemp, "boiltemp", "actual boiler temperature", "Kesseltemperatur", "Keteltemperatuur", "Temperatur Värmepanna", "temperatura zasobnika", "varmepumpetemp.", "température de la chaudière", "gerçek boyler sıcaklığı", "temperatura attuale caldaia", "skutočná teplota kotla", "aktuální teplota kotle")
MAKE_TRANSLATION(exhaustTemp, "exhausttemp", "exhaust temperature", "Abgastemperatur", "Uitlaattemperatuur", "Avgastemperatur", "temperatura spalin", "røykgasstemp", "température des gaz d'échappement", "baca sıcaklığı", "temperatura di scarico", "teplota výfukových plynov", "teplota spalin")
MAKE_TRANSLATION(burnGas, "burngas", "gas", "Gas", "Gas", "Gas", "gaz", "gass", "gaz", "gaz", "Gas", "plyn", "plyn")
MAKE_TRANSLATION(burnGas2, "burngas2", "gas stage 2", "Gas Stufe 2", "gas fase 2", "Gas Fas 2", "gaz 2 stopień", "gass fase 2", "gaz état 2", "gaz 2.seviye", "gas fase 2", "plynový stupeň 2", "plyn stupeň 2")
MAKE_TRANSLATION(flameCurr, "flamecurr", "flame current", "Flammenstrom", "Vlammenstroom", "Lågström", "prąd palnika", "flammetemp", "courrant de flamme", "alev akımı", "corrente di fiamma", "prúd plameňa", "proud plamene")
MAKE_TRANSLATION(heatingPump, "heatingpump", "heating pump", "Heizungspumpe", "Verwarmingspomp", "Värmepump", "pompa ciepła", "varmepumpe", "pompe à chaleur", "ısı pompası", "pompa di calore", "tepelné čerpadlo", "Čerpadlo topení")
MAKE_TRANSLATION(fanWork, "fanwork", "fan", "Gebläse", "Ventilator", "Fläkt", "wentylator", "vifte", "ventilateur", "fan", "Ventilatore", "ventilátor", "ventilátor")
MAKE_TRANSLATION(ignWork, "ignwork", "ignition", "Zündung", "Ontsteking", "Tändning", "zapłon", "tenning", "ignition", "ateşleme", "accensione", "zapálenie", "zapalování")
MAKE_TRANSLATION(heatingActivated, "heatingactivated", "heating activated", "Heizbetrieb aktiviert", "Verwarmen geactiveerd", "Uppvärmning aktiv", "system c.o.", "oppvarming aktivert", "chauffage activé", "ısıtma başladı", "riscaldamento attivato", "kúrenie aktivované", "topení aktivováno")
MAKE_TRANSLATION(heatingTemp, "heatingtemp", "heating temperature", "Heiztemperatur", "Verwarmingstemperatuur", "Uppvärmningstemperatur", "temperatura grzania", "oppvarmingstemperatur", "température de chauffage", "ısıtma sıcaklığı", "temperatura riscaldamento", "teplota vykurovania", "teplota topení")
MAKE_TRANSLATION(pumpModMax, "pumpmodmax", "boiler pump max power", "Maximale Kesselpumpenleistung", "Ketelpomp max vermogen", "Värmepannepump max effekt", "maksymalna moc pompy zasobnika", "varmepumpe maks effekt", "puissance max pompe à chaleur", "boyler pompası maksimum güç", "max potenza pompa caldaia", "maximálny výkon čerpadla kotla", "maximální výkon čerpadla kotle")
MAKE_TRANSLATION(pumpModMin, "pumpmodmin", "boiler pump min power", "Minimale Kesselpumpenleistung", "Ketelpomp min vermogen", "Värmepannepump min effekt", "minimalna moc pompy zasobnika", "varmepumpe min effekt", "puissance min pompe à chaleur", "boyler pompası minimum güç", "min potenza pompa caldaia", "min. výkon čerpadla kotla", "minimální výkon čerpadla kotle")
MAKE_TRANSLATION(pumpDelay, "pumpdelay", "pump delay", "Pumpennachlaufzeit", "Pomp nalooptijd", "Pumpfördröjning", "opóźnienie pompy", "pumpeforsinkelse", "délai d'attente pompe", "pompa gecikmesi", "ritardo pompa", "oneskorenie čerpadla", "zpoždění čerpadla")
MAKE_TRANSLATION(burnMinPeriod, "burnminperiod", "burner min period", "Antipendelzeit", "Antipendeltijd", "Värmepanna Min Period", "minimalny czas pracy palnika", "varmekjele min periode", "délai d'attente du brûleur", "kazan minmum periyod", "periodo minimo del bruciatore", "minimálne obdobie horáka", "minimální doba hoření")
MAKE_TRANSLATION(burnMinPower, "burnminpower", "burner min power", "Minimale Brennerleistung", "Minimaal brandervermogen", "Värmepanna Min Effekt", "minimalna moc źródła ciepła", "varmekjele min effekt", "puissance min brûleur", "kazan minimum güç", "potenza minima bruciatore", "min. výkon horáka", "minimální výkon hořáku")
MAKE_TRANSLATION(burnMaxPower, "burnmaxpower", "burner max power", "Maximale Brennerleistung", "Maximaal brandervermogen", "Värmepanna Max Effekt", "maksymalna moc źródła ciepła", "varmekjele maks effekt", "puissance max brûleur", "kazan maksimum güç", "potenza massima bruciatore", "max. výkon horáka", "maximální výkon hořáku")
MAKE_TRANSLATION(boilHystOn, "boilhyston", "hysteresis on temperature", "Einschaltdifferenz", "ketel aan hysterese verschil", "Hysteres aktiveringstemperatur", "histereza załączania", "hysterese på temperatur", "hysteresis température d'allumage", "gecikme sıcaklığı devrede", "isteresi sulla temperatura", "hysterézia teploty pri zapnutí", "hystereze teploty při zapnutí")
MAKE_TRANSLATION(boilHystOff, "boilhystoff", "hysteresis off temperature", "Ausschaltdifferenz", "ketel uit hysterese verschil", "Hysteres inaktiveringstemperatur", "histereza wyłączania", "hysterese av temperatur", "hysteresis température d'extinction", "gecikme sıcaklığı kapalı", "isteresi fuori temperatura", "hysterézia teploty pri vypnutí", "hystereze teploty při vypnutí")
MAKE_TRANSLATION(boil2HystOn, "boil2hyston", "hysteresis stage 2 on temperature", "Einschaltdifferenz Stufe 2", "ketel aan hysterese verschil 2", "Hysteres aktiveringstemperatur 2", "histereza załączania stopnia 2", "", "hysteresis état 2 température d'allumage", "2. seviye gecikme sıcaklığı devrede", "stadio di isteresi 2 sulla temperatura", "2. stupeň hysterézie pri teplote", "hystereze teploty při zapnutí 2.stupeň") // TODO translate
MAKE_TRANSLATION(boil2HystOff, "boil2hystoff", "hysteresis stage 2 off temperature", "Ausschaltdifferenz Stufe 2", "ketel uit hysterese verschil 2", "Hysteres inaktiveringstemperatur 2", "histereza wyłączania stopnia 2", "hysterese inaktiveringstemperatur 2", "hysteresis état 2 température d'extinction", "2. seviye gecikme sıcaklığı kapalı", "isteresi stadio 2 fuori temperatura", "teplota vypnutia hysterézneho stupňa 2", "hystereze teploty při vypnutí 2.stupeň")
MAKE_TRANSLATION(setFlowTemp, "setflowtemp", "set flow temperature", "Sollwert Vorlauftemperatur", "Ingestelde aanvoertemperatuur", "Börvärde Flödestemperatur", "zadana temperatura zasilania", "innstilt turtemperatur", "température du flux définie", "akış sıcaklığını ayarla", "impostare temperatura di mandata", "nastavená teplota výstupu", "nastavená teplota výstupu")
MAKE_TRANSLATION(setBurnPow, "setburnpow", "burner set power", "Sollwert Brennerleistung", "Ingesteld brandervermogen", "Värmepanna vald Effekt", "zadana moc palnika", "varmekjele valgt effekt", "puissance du brûleur définie", "kazan gücünü ayarla", "impostare potenza bruciatore", "nastavenie výkonu horáka", "nastavený výkon hořáku")
MAKE_TRANSLATION(curBurnPow, "curburnpow", "burner current power", "Aktuelle Brennerleistung", "Brandervermogen", "Värmepanna aktuell effekt", "aktualna moc źródła ciepła", "brennereffekt", "puissance du brûleur actuelle", "kazan güncel gücü", "potenza attuale bruciatore", "aktuálny výkon horáka", "aktuální výkon hořáku")
MAKE_TRANSLATION(burnStarts, "burnstarts", "burner starts", "Brennerstarts", "Aantal brander starts", "Värmepanna antal starter", "liczba uruchomień palnika", "antall varmepumpe starter", "démarrages du brûleur", "kazan başlıyor", "avvii bruciatore", "štarty horáka", "starty hořáku")
MAKE_TRANSLATION(burnWorkMin, "burnworkmin", "total burner operating time", "Brennerlaufzeit", "Totale branderlooptijd", "Värmepanna aktiva timmar", "łączny czas pracy palnika", "brennersteg tid i min", "durée de fonctionnement totale du brûleur", "toplam kazan çalışma süresi", "tempo totale di funzionamento del bruciatore", "celkový prevádzkový čas horáka", "celkový provozní čas hořáku")
MAKE_TRANSLATION(burn2WorkMin, "burn2workmin", "burner stage 2 operating time", "Brennerlaufzeit Stufe 2", "Totale looptijd brander fase 2", "Värmepanna steg 2 aktiva timmar", "łączny czas pracy palnika 2 stopnia", "brennersteg2 tid i min", "durée de fonctionnement totale du brûleur état 2", "2. seviye toplam kazan çalışma süresi", "tempo di funzionamento del bruciatore 2° stadio", "doba prevádzky 2. stupňa horáka", "provozní čas hořáku 2.stupeň")
MAKE_TRANSLATION(heatWorkMin, "heatworkmin", "total heat operating time", "Heizlaufzeit", "Totale looptijd verwarming", "Uppvärmning aktiva timmar", "łączny czas grzania", "varmetid i min", "durée de fonctionnement du chauffage", "toplam ısıtma çalışma süresi", "tempo totale di funzionamento in riscaldamento", "celkový prevádzkový čas kúrenia", "celkový provozní čas topení")
MAKE_TRANSLATION(heatStarts, "heatstarts", "burner starts heating", "Brennerstarts Heizen", "Aantal brander starts verwarming", "Uppvärmning antal starter", "liczba uruchomień palnika na ogrzewanie", "antall oppvarmninger", "démarrages du chauffage", "kazan ısıtmaya başlıyor", "preriscaldamento bruciatore", "štarty horáka na kúrenie", "starty hořáku topení")
MAKE_TRANSLATION(UBAuptime, "ubauptime", "total UBA operating time", "Anlagengesamtlaufzeit", "totale looptijd branderautomaat (UBA)", "Total Tid", "łączny czas pracy układu sterowania", "totaltid", "durée de fonctionnement totale de l'appareil (UBA)", "kazanın toplam işletme süresi", "Tempo di funzionamento totale del sistema", "Celkový čas chodu systému", "celkový provozní čas UBA")
MAKE_TRANSLATION(lastCode, "lastcode", "last error code", "Letzter Fehler", "Laatste foutcode", "Senaste Felkod", "ostatni błąd", "siste feilkode", "dernier code d'erreur", "son hata kodu", "ultimo codice errore", "posledný chybový kód", "poslední kód chyby")
MAKE_TRANSLATION(serviceCode, "servicecode", "service code", "Statusmeldung", "Statuscode", "Servicekod", "kod serwisowy", "servicekode", "code de service", "servis kodu", "codice messaggio di stato", "servisný kód", "servisní kód")
MAKE_TRANSLATION(serviceCodeNumber, "servicecodenumber", "service code number", "Statusmeldungsnummer", "Status codenummer", "Servicekodnummer", "numer kodu serwisowego", "servicekodenummer", "numéro du code de service", "servis kod numarası", "numero del messaggio di stato", "servisný kód - číslo", "číslo servisního kódu")
MAKE_TRANSLATION(maintenanceMessage, "maintenancemessage", "maintenance message", "Wartungsmeldung", "Onderhoudsmelding", "Servicemeddelande", "komunikat przeglądu", "vedlikeholdsmelding", "message de maintenance", "bakım mesajı", "messaggio di manutenzione", "správa o údržbe", "zpráva o údržbě")
MAKE_TRANSLATION(maintenanceDate, "maintenancedate", "next maintenance date", "Wartungsdatum", "Onderhoudsdatum", "Datum nästa Service", "termin następnego przeglądu", "vedlikeholdsdato", "prochaine date de maintenance", "bakım tarihi", "prossima data di manutenzione", "dátum ďalšej údržby", "datum další údržby")
MAKE_TRANSLATION(maintenanceType, "maintenance", "maintenance scheduled", "Wartungsplan", "Onderhoud gepland", "Underhall schemlagt", "rodzaj przeglądu", "vedlikeholdstype", "maintenance prévue", "planlı bakım", "manutenzione programmata", "plánovaná údržba", "naplánovaná údržba")
MAKE_TRANSLATION(maintenanceTime, "maintenancetime", "time to next maintenance", "Wartung in", "Onderhoud in", "Tid till nästa underhall", "czas do kolejnego przeglądu", "vedlikeholdstid", "durée avant la prochaine maintenance", "bakıma kalan süre", "tempo alla prossima manutenzione", "čas do ďalšej údržby", "čas do další údržby")
MAKE_TRANSLATION(emergencyOps, "emergencyops", "emergency operation", "Notbetrieb", "Noodoperatie", "Nöddrift", "praca w trybie awaryjnym", "nøddrift", "opération d'urgence", "acil durum çalışması", "operazione di emergenza", "núdzová prevádzka", "nouzový provoz")
MAKE_TRANSLATION(emergencyTemp, "emergencytemp", "emergency temperature", "Notfalltemperatur", "Noodtemperatuur", "Nöddrift temperatur", "temperatura w trybie awaryjnym", "nødtemperatur", "température d'urgence", "acil durum sıcaklığı", "temperatura di emergenza", "núdzová teplota", "nouzová teplota")
MAKE_TRANSLATION(pumpMode, "pumpmode", "boiler pump mode", "Kesselpumpenmodus", "Ketelpomp modus", "", "tryb pracy pompy kotła", "pumpemodus", "", "pompa modu", "modalità pompa caldaia", "režim kotlového čerpadla", "režim čerpadla kotle") // TODO translate
MAKE_TRANSLATION(pumpCharacter, "pumpcharacter", "boiler pump characteristic", "Charakteristik der Kesselpumpe", "karakteristiek ketelpomp", "pannpumpsegenskaper", "charakterystyka pompy kotłowej", "kjelepumpekarakteristikk", "caractéristique de la pompe de la chaudière", "gazan nasosy", "caratteristica della pompa della caldaia", "charakteristika kotlového čerpadla", "charakteristika čerpadla kotle") // TODO translate
MAKE_TRANSLATION(pumpOnTemp, "pumpontemp", "pump logic temperature", "Pumpenlogiktemperatur", "", "Pumplogiktemperatur", "", "", "", "", "", "teplota logiky čerpadla", "") // TODO translate
MAKE_TRANSLATION(headertemp, "headertemp", "low loss header", "Hydr. Weiche", "open verdeler", "Fördelare", "sprzęgło hydrauliczne", "", "bouteille de déc. hydr.", "isı bloğu gidiş suyu sıc.", "comp. idr.", "nízkostratová hlavica", "") // TODO translate
MAKE_TRANSLATION(heatblock, "heatblock", "heating block", "Wärmezelle", "Aanvoertemp. warmtecel", "Värmeblock", "blok grzewczy", "", "départ corps de chauffe", "Hid.denge kabı sıcaklığı", "mandata scamb. pr.", "vykurovací blok", "blok topení") // TODO translate
MAKE_TRANSLATION(curveOn, "curveon", "heatingcurve on", "Heizkurve an", "", "Värmekurva På", "", "", "", "", "", "vykurovacia krivka zapnutá", "topná křivka zapnutá") // TODO translate
MAKE_TRANSLATION(curveBase, "curvebase", "heatingcurve base", "Heizkurve Basis", "", "Värmekurva Bas", "", "", "", "", "", "základňa vykurovacej krivky", "základ topné křivky") // TODO translate
MAKE_TRANSLATION(curveEnd, "curveend", "heatingcurve end", "Heizkurve Ende", "", "Värmekurva Slut", "", "", "", "", "", "koniec vykurovacej krivky", "konec topné křivky") // TODO translate
// heatpump/compress specific
MAKE_TRANSLATION(upTimeTotal, "uptimetotal", "heatpump total uptime", "Gesamtbetriebszeit Wärmepumpe", "", "Total tid värmepump", "łączny czas pracy pompy ciepła", "", "", "", "", "celková doba prevádzky tepelného čerpadla", "celková doba provozu tepelného čerpadla") // TODO translate
MAKE_TRANSLATION(upTimeControl, "uptimecontrol", "total operating time heat", "Gesamtbetriebszeit Heizen", "Totale bedrijfstijd", "Total tid uppvärmning", "łączny czas generowania ciepła", "total driftstid", "durée totale de fonctionnement chauffage", "ısınma toplam işletme süresi", "Tempo di funzionamento totale riscaldamento", "celkový prevádzkový čas tepla", "celková provozní doba topení")
MAKE_TRANSLATION(upTimeCompHeating, "uptimecompheating", "operating time compressor heating", "Betriebszeit Kompressor heizen", "Bedrijfstijd compressor verwarmingsbedrijf", "Total tid kompressor uppvärmning", "łączny czas ogrzewania (sprężarka)", "totaltid kompressor", "durée de fonctionnement compresseur chauffage", "ısı pompası ısınma işletme süresi", "tempo di funzionamento del compressore riscaldamento", "prevádzková doba vykurovania kompresora", "provozní doba kompresoru pro topení")
MAKE_TRANSLATION(upTimeCompCooling, "uptimecompcooling", "operating time compressor cooling", "Betriebszeit Kompressor kühlen", "Bedrijfstijd compressor koelbedrijf", "Total tid kompressor kyla", "łączny czas chłodzenia (sprężarka)", "Total tid kompressor kjøling", "durée de fonctionnement compresseur refroidissement", "ısı pompası soğuma işletme süresi", "tempo di funzionamento del compressore raffreddamento", "doba prevádzky chladenia kompresora", "provozní doba kompresoru pro chlazení")
MAKE_TRANSLATION(upTimeCompWw, "uptimecomp", "operating time compressor", "Betriebszeit Kompressor", "Bedrijfstijd compressor", "Total tid kompressor", "łączny czas grzania c.w.u. (sprężarka)", "Total tid kompressor varmvatten", "durée de fonctionnement compresseur", "ısı pompası sıcak kullanım suyu işletme süresi", "tempo di funzionamento del compressore", "prevádzková doba kompresora", "provozní doba kompresoru")
MAKE_TRANSLATION(upTimeCompPool, "uptimecomppool", "operating time compressor pool", "Betriebszeit Kompressor Pool", "Bedrijfstijd compressor voor zwembadbedrijf", "Total tid kompressor pool", "łączny czas podgrzewania basenu (sprężarka)", "Total tid kompressor basseng", "durée de fonctionnement compresseur piscine", "ısı pompası havuz işletme süresi", "tempo di funzionamento del compressore piscina", "prevádzková doba kompresorového bazéna", "provozní doba kompresoru pro bazén")
MAKE_TRANSLATION(totalCompStarts, "totalcompstarts", "total compressor control starts", "Gesamtkompressorstarts ", "Totaal compressorstarts", "Kompressorstarter Totalt", "liczba załączeń sprężarki", "kompressorstarter totalt", "nombre démarrages total contrôle compresseur", "ısı pompası kontrolü toplam başlatma", "avvii totali del compressore", "spustí sa celkové riadenie kompresora", "celkový počet startů řízení kompresoru")
MAKE_TRANSLATION(heatingStarts, "heatingstarts", "heating control starts", "Heizungsregelungstarts", "Starts verwarmingsbedrijf", "Kompressorstarter Uppvärmning", "liczba załączeń ogrzewania", "kompressorstarter oppvarming", "démarrages contrôle chauffage", "ısıtma kontrolü toplam başlatma", "avvii riscaldamento", "ovládanie vykurovania sa spustí", "počet startů řízení topení")
MAKE_TRANSLATION(coolingStarts, "coolingstarts", "cooling control starts", "Kühlregelungstarts", "Starts koelbedrijf", "Kompressorstarter Kyla", "liczba załączeń chłodzenia", "kompressorstarter kjøling", "démarrages contrôle refroidissement", "soğutma kontrolü toplam başlatma", "avvii raffreddamento", "ovládanie chladenia sa spustí", "počet startů řízení chlazení")
MAKE_TRANSLATION(poolStarts, "poolstarts", "pool control starts", "Poolsteuerungstarts", "Starts zwembadbedrijf", "Kompressorstarter Pool", "liczba załączeń podgrzewania basenu", "kompressorstarter basseng", "démarrages contrôle piscine", "havuz kontrolü toplam başlatma", "avvio controllato piscina", "riadenie bazéna sa spustí", "počet startů řízení bazénu")
MAKE_TRANSLATION(nrgConsTotal, "nrgconstotal", "total energy consumption", "Gesamtenergieverbrauch", "Energieverbrauch gesamt", "Energiförbrukning totalt", "energia pobrana (sumarycznie)", "energiforbruk totalt", "consommation totale énergie", "toplam enerji tüketimi", "totale energia consumata", "celková spotreba energie", "celková spotřeba energie")
MAKE_TRANSLATION(nrgConsCompTotal, "nrgconscomptotal", "total energy consumption compressor", "Gesamtenergieverbrauch Kompressor", "Energieverbruik compressor totaal", "Energiförbrukning kompressor", "energia pobrana przez sprężarkę", "energiforbruk kompressor", "consommation totale énergie compresseur", "ısı pompası toplam enerji tüketimi", "totale energia consumata compressore", "kompresor s celkovou spotrebou energie", "celková spotřeba energie kompresoru")
MAKE_TRANSLATION(nrgConsCompHeating, "nrgconscompheating", "energy consumption compressor heating", "Energieverbrauch Kompressor heizen", "Energieverbruik compressor verwarmingsbedrijf", "Energiförbrukning uppvärmning", "energia pobrana przez sprężarkę na ogrzewanie", "energiforbruk oppvarming", "consommation énergie compresseur chauffage", "ısı pompası ısıtma toplam enerji tüketimi", "consumo energia compressore riscaldamento", "spotreba energie vykurovanie kompresorom", "spotřeba energie kompresoru pro topení")
MAKE_TRANSLATION(nrgConsCompWw, "nrgconscomp", "energy consumption compressor", "Energieverbrauch Kompressor", "Energieverbruik compressor", "Energiförbrukning varmvatten", "energia pobrana przez sprężarkę na c.w.u.", "energiforbruk", "consommation énergie compresseur", "ısı pompası sıcak kullanım suyu toplam enerji tüketimi", "consumo energia compressore", "kompresor spotreby energie", "spotřeba energie kompresoru")
MAKE_TRANSLATION(nrgConsCompCooling, "nrgconscompcooling", "energy consumption compressor cooling", "Energieverbrauch Kompressor kühlen", "Energieverbruik compressor koelbedrijf", "Energiförbrukning kyla", "energia pobrana przez sprężarkę na chłodzenie", "energiforbruk kjøling", "consommation énergie compresseur refroidissement", "ısı pompası soğutma toplam enerji tüketimi", "consumo energia compressore raffreddamento", "spotreba energie kompresorové chladenie", "spotřeba energie kompresoru pro chlazení")
MAKE_TRANSLATION(nrgConsCompPool, "nrgconscomppool", "energy consumption compressor pool", "Energieverbrauch Kompressor Pool", "Energiebedrijf compressor zwembadbedrijf", "Energiförbrukning pool", "energia pobrana przez sprężarkę na podgrzewanie basenu", "energiforbruk basseng", "consommation énergie compresseur piscine", "ısı pompası havuz toplam enerji tüketimi", "consumo energia compressore piscina", "spotreba energie kompresorový bazén", "spotřeba energie kompresoru pro bazén")
MAKE_TRANSLATION(nrgSuppTotal, "nrgsupptotal", "total energy supplied", "gesamte Energieabgabe", "Totaal opgewekte energie", "Genererad energi", "energia oddana (sumarycznie)", "tilført energi", "énergie totale fournie", "sağlanan toplam enerji", "totale energia fornita", "celková dodaná energia", "celková dodaná energie")
MAKE_TRANSLATION(nrgSuppHeating, "nrgsuppheating", "total energy supplied heating", "gesamte Energieabgabe heizen", "Opgewekte energie verwarmingsbedrijf", "Genererad energi Uppvärmning", "energia oddana na ogrzewanie", "tilført energi oppvarming", "énergie totale fournie chauffage", "ısıtma sağlanan toplam enerji", "energia totale fornita - riscaldamento", "celková dodaná energia na vykurovanie", "celková dodaná energie pro topení")
MAKE_TRANSLATION(nrgSuppWw, "nrgsupp", "total energy warm supplied", "gesamte Energieabgabe Wärme", "Opgewekte energie", "Genererad energi varmvatten", "energia oddana na c.w.u.", "tilført energi", "énergie chaude totale fournie", "sıcak kullanım suyu sağlanan toplam enerji", "totale energia calorica fornita", "celková dodaná teplá energia", "celková dodaná teplá energie")
MAKE_TRANSLATION(nrgSuppCooling, "nrgsuppcooling", "total energy supplied cooling", "gesamte Energieabgabe kühlen", "Opgewekte energie koelbedrijf", "Genererad energi Kyla", "energia oddana na chłodzenie", "Tillført energi kjøling", "énergie totale fournie refroidissement", "soğutma sağlanan toplam enerji", "energia totale fornita - raffreddamento", "chladenie s celkovou dodanou energiou", "celková dodaná energie pro chlazení")
MAKE_TRANSLATION(nrgSuppPool, "nrgsupppool", "total energy supplied pool", "gesamte Energieabgabe Pool", "Opgewekte energie zwembadbedrijf", "Genererad energi Pool", "energia oddana na podgrzewanie basenu", "tilført energi basseng", "énergie totale fournie piscine", "havuz sağlanan toplam enerji", "totale di energia fornita- piscina", "celkový bazén dodanej energie", "celková dodaná energie pro bazén")
MAKE_TRANSLATION(auxElecHeatNrgConsTotal, "auxelecheatnrgconstotal", "total aux elec. heater energy consumption", "Energieverbrauch el. Zusatzheizung", "Totaal energieverbruik electrisch verwarmingselement", "Energiförbrukning Eltillkott", "energia pobrana przez grzałki", "energiforbruk varmekolbe", "consommation totale énergie electrique auxiliaire chauffage", "ilave elektrikli ısıtıcı toplam enerji tüketimi", "consumo energetico riscaldamento elettrico supplementare", "celková spotreba energie prídavného elektrického ohrievača", "celková spotřeba energie pomocného el. ohřívače")
MAKE_TRANSLATION(auxElecHeatNrgConsHeating, "auxelecheatnrgconsheating", "aux elec. heater energy consumption heating", "Energieverbrauch el. Zusatzheizung Heizen", "Energieverbruik electrisch verwarmingselement voor verwarmingsbedrijf", "Energiförbrukning Eltillskott Uppvärmning", "energia pobrana przez grzałki na ogrzewanie", "energiforbruk varmekolbe oppvarming", "consommation énergie electrique auxiliaire chauffage", "ilave elektrikli ısıtıcı ısınma toplam enerji tüketimi", "consumo di energia riscaldamento elettrico ausiliario", "pomocný elektrický ohrievač spotreba energie vykurovanie", "spotřeba energie pomocného el. ohřívače pro topení")
MAKE_TRANSLATION(auxElecHeatNrgConsWw, "auxelecheatnrgcons", "aux elec. heater energy consumption", "Energieverbrauch el. Zusatzheizung", "Energieverbruik electrisch verwarmingselement voor", "Energiförbrukning Eltillskott Varmvatten", "energia pobrana przez grzałki na c.w.u.", "energiförbruk varmekolbe", "consommation énergie electrique auxiliaire chauffage", "ilave elektrikli ısıtıcı sıcak kullanım suyu toplam enerji tüketimi", "consumo di energia riscaldamento elettrico ausiliario", "spotreba energie pomocného elektrického ohrievača", "spotřeba energie pomocného el. ohřívače")
MAKE_TRANSLATION(auxElecHeatNrgConsPool, "auxelecheatnrgconspool", "aux elec. heater energy consumption pool", "Energieverbrauch el. Zusatzheizung Pool", "Energieverbruik electrisch verwarmingselement voor zwembadbedrijf", "Energiförbrukning Eltillskott Pool", "energia pobrana przez grzałki na podgrzewanie basenu", "energiforbruk el. tilleggsvarme basseng", "consommation énergie electrique auxiliaire chauffage piscine", "ilave elektrikli ısıtıcı havuz toplam enerji tüketimi", "consumo di energia riscaldamento elettrico ausiliario piscina", "bazén spotreby energie pomocného elektrického ohrievača", "spotřeba energie pomocného el. ohřívače pro bazén")
MAKE_TRANSLATION(hpCompOn, "hpcompon", "hp compressor", "WP Kompressor", "WP compressor", "VP Kompressor", "sprężarka pompy ciepła", "vp kompressor", "compresseur pompe à chaleur", "hp ısı pompası", "compressore pompa calore", "tč kompresor", "tč kompresor")
MAKE_TRANSLATION(coolingOn, "coolingon", "cooling on", "Kühlung an", "koelbedrijf", "Kyla", "chłodzenie włączone", "kjøling", "refroidissement", "soğutma", "", "chladenie", "chlazení zapnuto") // TODO translate
// MAKE_TRANSLATION(hpHeatingOn, "hpheatingon", "hp heating", "WP Heizen", "WP verwarmingsbedrijf", "VP Uppvärmning", "pompa ciepła, ogrzewanie", "vp oppvarmning", "hp ısınıyor", "riscaldamento pompa calore", "vykurovanie hp", "topení hp")
// MAKE_TRANSLATION(hpCoolingOn, "hpcoolingon", "hp cooling", "WP Kühlen", "WP koelbedrijf", "VP Kyla", "pompa ciepła, chłodzenie", "vp kjøling", "hp soğuyor", "raffreddamento pompa calore", "chladenie hp", "chlazení hp")
// MAKE_TRANSLATION(hpWwOn, "hpdhwon", "hp", "WP", "WP", "VP Varmvatten", "pompa ciepła", "vp", "pompe à chaleur", "hp", "pompa calore", "hp", "hp")
// MAKE_TRANSLATION(hpPoolOn, "hppoolon", "hp pool", "WP Pool", "WP zwembadbedrijf", "VP Pool", "pompa ciepła, podgrzewanie basenu", "vp basseng", "tuzlu su pompası hızı", "pompa calore piscina", "hp bazén", "hp bazén")
MAKE_TRANSLATION(hpBrinePumpSpd, "hpbrinepumpspd", "brine pump speed", "Solepumpendrehzahl", "Snelheid pekelpomp", "Hastighet Brine-pump", "wysterowanie pompy glikolu", "hastighet brine-pumpe", "vitesse pompe à saumure", "ısı pompası hızı", "velocità pompa sbrinamento", "rýchlosť čerpadla soľanky", "rychlost čerpadla solanky")
MAKE_TRANSLATION(hpCompSpd, "hpcompspd", "compressor speed", "Kompressordrehzahl", "Snelheid compressor", "Kompressorhastighet", "wysterowanie sprężarki", "kompressorhastighet", "vitesse du compresseur", "sirkülasyon pompası hızı", "velocità compressore", "rýchlosť kompresora", "rychlost kompresoru")
MAKE_TRANSLATION(hpCircSpd, "hpcircspd", "circulation pump speed", "Zirkulationspumpendrehzahl", "Snelheid circulatiepomp", "Hastighet Cirkulationspump", "wysterowanie pompy obiegu grzewczego", "hastighet sirkulationspumpe", "vitesse pompe à circulation", "evaporatör tuzlu su giişi", "velocità pompa circolazione", "otáčky obehového čerpadla", "rychlost oběhového čerpadla")
MAKE_TRANSLATION(hpBrineIn, "hpbrinein", "brine in/evaporator", "Sole in/Verdampfer", "pekel in/verdamper", "Brine in (förångare)", "temperatura glikolu na wejściu kolektora (TB0)", "brine in/fordamper", "entrée saumure/évaporateur", "kondenser tuzlu su çıkışı", "salamoia nell evaporatore", "soľanka v/výparník", "solanka in/evaporátor")
MAKE_TRANSLATION(hpBrineOut, "hpbrineout", "brine out/condenser", "Sole aus/Kondensator", "pekel uit/condensor", "Brine ut (kondensor)", "temperatura glikolu na wyjściu kolektora (TB1)", "Brine ut/kondensor", "sortie saumure/condenseur", "anahtar valfi", "salamoia nell uscita evaporatore", "výstup soľanky/kondenzátor", "solanka out/kondenzátor")
MAKE_TRANSLATION(hpSwitchValve, "hpswitchvalve", "switch valve", "Schaltventil", "schakelklep", "Växelventil", "zawór przełączający", "skifteventil", "valve de commutation", "ısı pompası aktivitesi", "valvola commutazione pompa di calore", "prepínací ventil", "přepínací ventil")
MAKE_TRANSLATION(hpActivity, "hpactivity", "compressor activity", "Kompressoraktivität", "Compressoractiviteit", "Kompressoraktivitet", "pompa ciepła, aktywność sprężarki", "kompressoraktivitet", "hp ısı pompası", "attività compressore", "činnosť kompresora", "činnost kompresoru")
MAKE_TRANSLATION(hpMaxPower, "hpmaxpower", "compressor max power", "max. Kompressorleistung", "", "Max. Kompressoreffekt", "maksymalna wydajność sprężarki", "", "", "", "", "max výkon kompresora", "maximální výkon kompresoru") // TODO translate
MAKE_TRANSLATION(pvMaxComp, "pvmaxcomp", "pv compressor max power", "PV max. Kompressorleistung", "", "PV Max. Kompressoreffekt", "maksymalna wydajność sprężarki", "", "", "", "", "pv max výkon kompresora", "maximální výkon PV kompresoru") // TODO translate
MAKE_TRANSLATION(hpPower, "hppower", "compressor power output", "Kompressorleistung", "Compressorvermogen", "Kompressoreffekt", "moc wyjściowa sprężarki", "kompressoreffekt", "puissance de sortie compresseur", "ısı pompası güç çıkışı", "potenza uscita compressore", "výkon kompresora", "výstupní výkon kompresoru")
MAKE_TRANSLATION(hpTc0, "hptc0", "heat carrier return (TC0)", "Kältemittelrücklauf (TC0)", "Koudemiddel retour (TC0)", "Värmebärare Retur (TC0)", "temperatura nośnika ciepła na powrocie (TC0)", "kjølemiddel retur (TC0)", "retour caloporteur (TC0)", "sıcak su dönüşü (TC0)", "ritorno del refrigerante (TC0)", "návrat nosiča tepla (TC0)", "návrat teplonosné látky (TC0)")
MAKE_TRANSLATION(hpTc1, "hptc1", "heat carrier forward (TC1)", "Kältemittelvorlauf (TC1)", "Koudemiddel aanvoer (TC1)", "Värmebärare Framledning (TC1)", "temperatura nośnika ciepła pierwotna (TC1)", "kjølemiddel tur (TC1)", "avance caloporteur (TC1)", "sıcak su çıkışı (TC1)", "flusso di refrigerante (TC1)", "nosič tepla vpred (TC1)", "předání teplonosné látky (TC1)")
MAKE_TRANSLATION(hpTc3, "hptc3", "condenser temperature (TC3)", "Kondensatortemperatur (TC3)", "Condensortemperatuur (TC3)", "Kondensortemperatur (TC3)", "temperatura skraplacza/na wyjściu sprężarki (TC3)", "kondensortemperatur (TC3)", "température condensateur (TC3)", "kondenser sıcaklığı (TC3)", "temperatura condensatore (TC3)", "teplota kondenzátora (TC3)", "teplota kondenzátoru (TC3)")
MAKE_TRANSLATION(hpTr1, "hptr1", "compressor temperature (TR1)", "Kompressortemperatur (TR1)", "Compressor temperatuur (TR1)", "Kompressor temp (TR1)", "temperatura sprężarki (TR1)", "kompressor temperatur (TR1)", "température compresseur (TR1)", "ısı pompası sıcaklığı (TR1)", "temperatura compressore (TR1)", "teplota kompresora (TR1)", "teplota kompresoru (TR1)")
MAKE_TRANSLATION(hpTr3, "hptr3", "refrigerant temperature liquid side (condenser output) (TR3)", "Kältemittel (flüssig) (TR3)", "Temperatuur koudemiddel vloeibare zijde (TR3)", "Köldmedium temperatur (kondensorutlopp) (TR3)", "temperatura skraplacza ogrzew. (TR3)", "kjølemiddeltemperatur på væskesiden (TR3)", "température réfrigérant côté liquide (sortie condensateur) (TR3)", "ısı pompası çıkışı (TR3)", "temperatura refrigerante lato liquido (uscita condensatore) (TR3)", "teplota chladiva na strane kvapaliny (výstup kondenzátora) (TR3)", "teplota chladiva na kapalné straně (výstup kondenzátoru) (TR3)")
MAKE_TRANSLATION(hpTr4, "hptr4", "evaporator inlet temperature (TR4)", "Verdampfereingang (TR4)", "Verdamper ingangstemperatuur (TR4)", "Förångare inloppstemp (TR4)", "temperatura na wejściu parownika (TR4)", "innløpstemperatur for fordamperen (TR4)", "température entrée évaporateur (TR4)", "evaporatör giriş sıcaklığı (TR4)", "temperatura di ingresso dell'evaporatore (TR4)", "Vstupná teplota výparníka (TR4)", "teplota vstupu do evaporátoru (TR4)")
MAKE_TRANSLATION(hpTr5, "hptr5", "compressor inlet temperature (TR5)", "Kompressoreingang (TR5)", "Compressor ingangstemperatuur (TR5)", "Kompressor inloppstemp (TR5)", "temperatura na wejściu sprężarki (TR5)", "kompressor innløpstemp (TR5)", "température entrée compresseur (TR5)", "ısı pompası giriş sıcaklığı (TR5)", "temperatura di ingresso del compressore (TR5)", "vstupná teplota kompresora (TR5)", "teplota vstupu do kompresoru (TR5)")
MAKE_TRANSLATION(hpTr6, "hptr6", "compressor outlet temperature (TR6)", "Kompressorausgang (TR6)", "Compressor uitgangstemperatuur (TR6)", "Kompressor utloppstemp (TR6)", "temperatura na wyjściu sprężarki (TR6)", "kompressor utløpstemp (TR6)", "température sortie compresseur (TR6)", "ısı pompası çıkış sıcaklığı (TR6)", "temperatura di uscita del compressore (TR6)", "výstupná teplota kompresora (TR6)", "teplota výstupu z kompresoru (TR6)")
MAKE_TRANSLATION(hpTr7, "hptr7", "refrigerant temperature gas side (condenser input) (TR7)", "Kältemittel (gasförmig) (TR7)", "Temperatuur koudemiddel gasvormig (TR7)", "Köldmedium temperatur gassida (kondensorinlopp) (TR7)", "temperatura czynnika chłodniczego po stronie gazu (wejście skraplacza) (TR7)", "kjølemedium temperatur gassida (kondensatorinløp) (TR7)", "température réfrigérant côté gaz (sortie condensateur) (TR7)", "kondenser giriş sıcaklığı (TR7)", "temperatura refrigerante lato gas (ingresso condensatore) (TR7)", "teplota chladiva na strane plynu (vstup kondenzátora) (TR7)", "teplota chladiva na plynové straně (vstup kondenzátoru) (TR7)")
MAKE_TRANSLATION(hpTl2, "hptl2", "air inlet temperature (TL2)", "Außenlufteintrittstemperatur (TL2)", "Temperatuur luchtinlaat (TL2)", "Luftintagstemperatur (TL2)", "temperatura wlotu powietrza (TL2)", "luftinntakstemperatur (TL2)", "température entrée air (TL2)", "hava giriş sıcaklığı (TL2)", "temperatura ingresso aria (TL2)", "teplota prívodu vzduchu (TL2)", "teplota vzduchu na vstupu (TL2)")
MAKE_TRANSLATION(hpPl1, "hppl1", "low pressure side temperature (PL1)", "Niederdrucktemperatur (PL1)", "Temperatuur lage drukzijde (PL1)", "Temperatur Lågtryckssidan (PL1)", "temperatura po stronie niskiego ciśnienia (PL1)", "temperatur lavtrykksiden (PL1)", "température côté basse pression (PL1)", "düşük basınç tarafı sıcaklığı (PL1)", "temperatura lato bassa pressione (PL1)", "teplota na strane nízkeho tlaku (PL1)", "teplota na nízkotlaké straně (PL1)")
MAKE_TRANSLATION(hpPh1, "hpph1", "high pressure side temperature (PH1)", "Hochdrucktemperatur (PH1)", "Temperatuur hoge drukzijde (PH1)", "Temperatur Högtryckssidan (PH1)", "temperatura po stronie wysokiego ciśnienia (PH1)", "Temperatur Høytrykksiden (PH1)", "température côté bhauteasse pression (PH1)", "yüksek basınç tarafı sıcaklığı (PH1)", "temperatura lato alta pressione (PH1)", "teplota na strane vysokého tlaku (PH1)", "teplota na vysokotlaké straně (PH1)")
MAKE_TRANSLATION(hpTa4, "hpta4", "drain pan temp (TA4)", "Kondensatorwanne (TA4)", "Temperatuur condensorafvoerbak (TA4)", " (TA4)", "temperatura ociekacza (TA4)", "kondenstråg temperatur (TA4)", " (TA4)", "tahliye sıcaklığı (TA4)", "temperatura condensatore (TA4)", "teplota vypúšťacej misky (TA4)", "teplota odvodňovací vany (TA4)") // TODO translate
MAKE_TRANSLATION(hpTw1, "hptw1", "reservoir temp (TW1)", "DHW Reservoir (TW1)", "(TW1)", "Varmvattentank Temperatur (TW1)", "temperatura zbiornika (TW1)", "(TW1)", "(TW1)", "(TW1)", "(TW1)", "teplota zásobníka (TW1)", "teplota v nádrži (TW1)") // TODO translate
MAKE_TRANSLATION(hpInput1, "hpin1", "input 1 state", "Status Eingang 1", "Status input 1", "Status Ingång 1", "stan wejścia 1", "status inggang 1", "état entrée 1", "giriş 1 durumu", "stato ingresso 1", "stav vstupu 1", "stav vstupu 1")
MAKE_TRANSLATION(hpInput2, "hpin2", "input 2 state", "Status Eingang 2", "Status input 2", "Status Ingång 2", "stan wejścia 2", "status inggang 2", "état entrée 2", "giriş 2 durumu", "stato ingresso 2", "stav vstupu 2", "stav vstupu 2")
MAKE_TRANSLATION(hpInput3, "hpin3", "input 3 state", "Status Eingang 3", "Status input 3", "Status Ingång 3", "stan wejścia 3", "status inggang 3", "état entrée 3", "giriş 3 durumu", "stato ingresso 3", "stav vstupu 3", "stav vstupu 3")
MAKE_TRANSLATION(hpInput4, "hpin4", "input 4 state", "Status Eingang 4", "Status input 4", "Status Ingång 4", "stan wejścia 4", "status inggang 4", "état entrée 4", "giriş 4 durumu", "stato ingresso 4", "stav vstupu 4", "stav vstupu 4")
MAKE_TRANSLATION(hpIn1Opt, "hpin1opt", "input 1 options", "Einstellung Eingang 1", "Instelling input 1", "Inställningar Ingång 1", "opcje wejścia 1", "innstillinger inngang 1", "options entrée 1", "giriş 1 seçenekleri", "impostazioni ingresso 1", "možnosti vstupu 1", "možnosti vstupu 1")
MAKE_TRANSLATION(hpIn2Opt, "hpin2opt", "input 2 options", "Einstellung Eingang 2", "Instelling input 2", "Inställningar Ingång 2", "opcje wejścia 2", "innstillinger inngang 2", "options entrée 2", "giriş 2 seçenekleri", "impostazioni ingresso 2", "možnosti vstupu 2", "možnosti vstupu 2")
MAKE_TRANSLATION(hpIn3Opt, "hpin3opt", "input 3 options", "Einstellung Eingang 3", "Instelling input 3", "Inställningar Ingång 3", "opcje wejścia 3", "innstillinger inngang 3", "options entrée 3", "giriş 3 seçenekleri", "impostazioni ingresso 3", "možnosti vstupu 3", "možnosti vstupu 3")
MAKE_TRANSLATION(hpIn4Opt, "hpin4opt", "input 4 options", "Einstellung Eingang 4", "Instelling input 4", "Inställningar Ingång 4", "opcje wejścia 4", "innstillinger inngang 4", "options entrée 4", "giriş 4 seçenekleri", "impostazioni ingresso 4", "možnosti vstupu 4", "možnosti vstupu 4")
MAKE_TRANSLATION(maxHeatComp, "maxheatcomp", "heat limit compressor", "Heizstab Limit mit Kompressor", "heat limit compressor", "Max. Värmegräns Kompressor", "ograniczenie mocy sprężarki", "max varmegrense kompressor", "limite chaleur compresseur", "ısı pompası ısıtma sınırı", "limite riscaldamento compressore", "tepelný limit kompresora", "tepelný limit kompresoru")
MAKE_TRANSLATION(maxHeatHeat, "maxheatheat", "heat limit heating", "Heizstab Limit Leistung", "Max, Värmegräns Uppvärmning", "heat limit heating", "ograniczenie mocy w trybie ogrzewania", "maks varmegrense oppvarming", "limite chaleur chauffage", "ısınma ısıtma sınırı", "limite calore riscaldamento", "vyhrievanie limitu tepla", "tepelný limit topení")
MAKE_TRANSLATION(maxHeatDhw, "maxheat", "heat limit", "Heizstab Limit für WW", "heat limit", "Max. Värmegräns Varmvatten", "ograniczenie mocy w trybie c.w.u.", "varmegrense", "limite chaleur", "sıcak kullanım suyu ısınma sınırı", "limite calore", "tepelný limit", "tepelný limit")
MAKE_TRANSLATION(auxHeaterOff, "auxheateroff", "disable aux heater", "Zusatzheizer deaktivieren", "Bijverwarming uitsc", "Blockera eltillskott", "wyłącz dogrzewacz", "deaktiver tilleggsvarme", "Désactiver chauff. d'app", "ilave ısıtıcıyı kapat", "disattivare i riscaldatori addizionali", "vypnúť pomocný ohrievač", "zakázat pomocné topení")
MAKE_TRANSLATION(auxHeaterStatus, "auxheaterstatus", "aux heater status", "Zusatzheizerstatus", "Bijverwarming", "Eltillskott Status", "status dogrzewacza", "status el. tillegsvarme", "Chauffage auxiliaire", "ilave ısıtıcı durumu", "stato riscaldatori addizionali", "stav pomocného ohrievača", "stav pomocného topení")
MAKE_TRANSLATION(auxHeaterLevel, "auxheaterlevel", "aux heater level", "Zusatzheizer", "Bijverwarming", "Eltillskott", "dogrzewacza", "el. tillegsvarme", "Chauffage auxiliaire", "ilave ısıtıcı durumu", "riscaldatori addizionali", "pomocného ohrievača", "pomocného topení")
MAKE_TRANSLATION(auxHeaterOnly, "auxheateronly", "aux heater only", "nur Zusatzheizer", "Alleen bijverwarming", "Eltillskott Enbart", "tylko dogrzewacz", "kun el tilleggsvarme", "Que chauffage auxiliaire", "sadece ilave ısıtıvcı", "solo riscaldatori addizionali", "iba pomocný ohrievač", "pouze pomocné topení")
MAKE_TRANSLATION(auxHeaterDelay, "auxheaterdelay", "aux heater on delay", "Zusatzheizer verzögert ein", "Bijverw. vertraagd aan", "Eltillskottfördröjning på", "opóźnienie włączenia dogrzewacza", "Tilleggsvarmer forsinket på", "Chauff app tempo marche", "ilave ısıtıcı beklemede", "ritardo riscaldatori addizionali", "oneskorenie prídavného ohrievača", "zpoždění zapnutí pomocného topení")
MAKE_TRANSLATION(silentMode, "silentmode", "silent mode", "Silentmodus", "Stiller gebruik", "Tyst drift", "tryb cichy", "stille modus", "Fct silencieux", "sessiz mod", "modalità silenziosa", "tichý režim", "tichý režim")
MAKE_TRANSLATION(minTempSilent, "mintempsilent", "min outside temp for silent mode", "Minimale Außentemperatur Silentmodus", "Stiller gebruik min. buitentemp", "Tyst drift min temp", "minimalna temperatura zewnętrzna dla trybu cichego", "atille modus min temp", "Fct silencieux: Temp. extérieure min.", "sessiz mod için min. dış ortam sıcaklığı", "modalità silenziosa temperatura esterna minima", "min. vonkajšia teplota pre tichý režim", "minimální venkovní teplota pro tichý režim")
MAKE_TRANSLATION(tempParMode, "tempparmode", "outside temp parallel mode", "Heizstab Parallelbetrieb", "Buitentemp. parallelbedr", "Parallelläge Utomhustemp.", "maksymalna temperatura zewnętrzna dla dogrzewacza", "", "Temp. ext. fct parallèle", "paralel mod dış ortam sıcaklığı", "modalità parallela temperatura esterna", "paralelný režim mimo teploty", "venkovní teplota pro paralelní režim") // TODO translate
MAKE_TRANSLATION(auxHeatMixValve, "auxheatmix", "aux heater mixing valve", "Mischventil Zusatzheizer", "Bijverwarming menger", "Eltillskott Blandarventil", "mieszacz dogrzewacza", "eltilskudd blandeventil", "Chauffage auxiliaire mélangeur", "ilave ısıtıcı karışım vanası", "miscela riscaldatori addizionali", "zmiešavací ventil pomocného ohrievača", "směšovací ventil pomocného topení")
MAKE_TRANSLATION(hpHystHeat, "hphystheat", "on/off hyst heat", "Schalthysterese Heizen", "Aan/uit-hysteresis in verw. bedrijf", "Hysteres Uppvärmning", "histereza wł./wył. ogrzewania", "På/av hysterese Oppvar.", "Hystérésis Marche en mode chauffage", "ısıtma gecikmesi", "isteresi di commutazione riscaldamento", "zapnutie/vypnutie hyst ohrevu", "hystereze zapnutí/vypnutí pro topení")
MAKE_TRANSLATION(hpHystCool, "hphystcool", "on/off hyst cool", "Schalthysterese Kühlen", "Aan/uit-hysteresis in koelbedrijf", "Hysteres Kyla", "histereza wł./wył. chłodzenia", "hystrese kjøling", "Hystérésis Marche en mode refroidissement", "soğutma gecikmesi", "isteresi di commutazione raffreddamento", "zapnutie/vypnutie hyst chladenia", "hystereze zapnutí/vypnutí pro chlazení")
MAKE_TRANSLATION(hpHystPool, "hphystpool", "on/off hyst pool", "Schalthysterese Pool", "an/uit-hysteresis in zwembadbedri", "Hysteres Pool", "histereza wł./wył. podgrzewania basenu", "hystrese basseng", "Hystérésis Marche en mode piscine", "havuz gecikmesi", "isteresi di commutazione piscina", "zapnutie/vypnutie hyst bazénu", "hystereze zapnutí/vypnutí pro bazén")
MAKE_TRANSLATION(tempDiffHeat, "tempdiffheat", "temp diff TC3/TC0 heat", "Temp.diff. TC3/TC0 Heizen", "Temp.vers. TC3/TC0 verw", "Temperaturskillnad TC3/TC0 Uppvärmning", "różnica temperatur TC3/TC0 w trakcie ogrzewania", "temp. diff. TC3/TC0 oppvarm", "Delta T TC3/TC0 Chauff", "TC3-TC0 ısıtma sıcaklık farkı", "Delta T riscaldamento TC3/TC0", "teplotný rozdiel TC3/TC0 tepla", "rozdíl teplot TC3/TC0 pro topení")
MAKE_TRANSLATION(tempDiffCool, "tempdiffcool", "temp diff TC3/TC0 cool", "Temp.diff. TC3/TC0 Kühlen", "Temp.vers. TC3/TC0 koel.", "Temperaturskillnad TC3/TC0 Kyla", "różnica temperatur TC3/TC0 w trakcie chłodzenia", "temp. diff. TC3/TC0 kjøling", "Delta T TC3/TC0 Refroid.", "TC3-TC0 soğutma sıcaklık farkı", "Delta T raffreddamento TC3/TC0", "teplotný rozdiel TC3/TC0 chladenie", "rozdíl teplot TC3/TC0 pro chlazení")
MAKE_TRANSLATION(silentFrom, "silentfrom", "silent mode from", "Silentmodus Start", "Start stille modus", "Tyst drift starttid", "początek trybu cichego", "stillemodus starter", "", "sessiz mod başlangıcı", "avvio della modalità silenziosa", "tichý režim od", "tichý režim od") // TODO translate
MAKE_TRANSLATION(silentTo, "silentto", "silent mode to", "Silentmodus Ende", "Einde stille modus", "Tyst drift stopptid", "koniec trybu cichego", "komfortmodus av", "", "sessiz mod bitişi", "spegnere modalità silenziosa", "tichý režim do", "tichý režim do") // TODO translate
MAKE_TRANSLATION(wwComfOffTemp, "comfoff", "comfort switch off", "Komfort Ausschalttemp.", "Comfort Uitschakeltemp.", "Komfort frånkopplingstemperatur", "temperatura wyłączania w trybie komfort", "eco modus utkoblingstem", "Confort Temp. d'arrêt", "konfor kapalı", "spegnimento modalità comfort", "komfortné vypnutie", "komfortní vypnutí")
MAKE_TRANSLATION(wwEcoOffTemp, "ecooff", "eco switch off", "ECO Ausschalttemp.", "Eco Uitschakeltemp.", "Eko frånkopplingstemperatur", "temperatura wyłączania w trybie eko", "Øko avstengningstemp.", "Eco Temp. d'arrêt", "eko kapalı", "spegnimento modalità ECO", "eko vypínač", "eko vypnutí")
MAKE_TRANSLATION(wwEcoPlusOffTemp, "ecoplusoff", "eco+ switch off", "ECO+ Ausschalttemp.", "Eco+ Uitschakeltemp.", "Eko+ frånkopplingstemperatur", "temperatura wyłączania w trybie eko+", "Øko+ avstengningstemp.", "Eco+ Temp. d'arrêt", "eko+ kapalı", "spegnimento modalità ECO+", "eko+ vypnutie", "eko+ vypnutí")
MAKE_TRANSLATION(wwComfDiffTemp, "comfdiff", "comfort diff", "Komfort Differenztemp.", "", "Komfort temperaturskillnad", "", "", "", "", "", "Komfortný rozdiel teploty", "komfortní rozdíl") // TODO translate
MAKE_TRANSLATION(wwEcoDiffTemp, "ecodiff", "eco diff", "ECO Differenztemp.", "", "Eko temperaturskillnad", "", "", "", "", "", "ECO rozdiel teploty", "eko rozdíl") // TODO translate
MAKE_TRANSLATION(wwEcoPlusDiffTemp, "ecoplusdiff", "eco+ diff", "ECO+ Differenztemp.", "", "Eko+ temperaturskillnad", "", "", "", "", "", "ECO+ rozdiel teploty", "eko+ rozdíl") // TODO translate
MAKE_TRANSLATION(wwComfStopTemp, "comfstop", "comfort stop temp", "Komfort Stopptemp.", "", "Komfort stopptemperatur", "", "", "", "", "", "komfortná stop teplota", "komfortní teplota vypnutí") // TODO translate
MAKE_TRANSLATION(wwEcoStopTemp, "ecostop", "eco stop temp", "ECO Stopptemp.", "", "Eko stopptemperatur", "", "", "", "", "", "ECO stop teplota", "eko teplota vypnutí") // TODO translate
MAKE_TRANSLATION(wwEcoPlusStopTemp, "ecoplusstop", "eco+ stop temp", "ECO+ Stopptemp.", "", "Eko+ stopptemperatur", "", "", "", "", "", "ECO+ stop teplota", "eko+ teplota vypnutí") // TODO translate
MAKE_TRANSLATION(auxHeatMode, "auxheatrmode", "aux heater mode", "Zusatzheizungsmodus", "Modus bijverwarmer", "Eltillskott Läge", "tryb pracy dogrzewacza po blokadzie z Zakładu Energetycznego", "tilleggsvarmer modus", "", "ilave ısıtıcı modu", "modalità riscaldatore addizionale", "režim pomocného ohrievača", "režim pomocného topení") // TODO translate
MAKE_TRANSLATION(auxMaxLimit, "auxmaxlimit", "aux heater max limit", "Zusatzheizer max. Grenze", "Bijverwarmer grensinstelling maximaal", "Eltillskott max begränsning", "dogrzewacz, maksymalny limit", "tillegsvarme maksgrense", "ilave ısıtıcı maks limit", "limite massimo riscaldatore addizionale", "maximálny limit pomocného ohrievača", "maximální limit pomocného topení")
MAKE_TRANSLATION(auxLimitStart, "auxlimitstart", "aux heater limit start", "Zusatzheizer Grenze Start", "Bijverwarmer grens voor start", "Eltillskott begränsningsstart", "dogrzewacz, początek ograniczenia", "tillegsvarme startgrense", "ilave ısıtıcı limir başlangıcı", "avvio limite massimo riscaldatore addizionale", "spustenie limitu pomocného ohrievača", "startovací limit pomocného topení")
MAKE_TRANSLATION(manDefrost, "mandefrost", "manual defrost", "Manuelle Enteisung", "Handmatige ontdooicyclus", "Manuell avfrostning", "ręczne odladzanie", "manuell avisning", "", "manuel buz çözme", "sbrinamento manuale", "manuálne odmrazovanie", "ruční odmrazování") // TODO translate
MAKE_TRANSLATION(pvCooling, "pvcooling", "cooling only with PV", "Kühlen nur mit PV", "Koelen alleen met solar PV", "Kyla endast med solpanel", "chłodzenie tylko z PV", "kjøling med solpanel", "", "sadece PV ile soğutma", "solo raffrescamento con solare", "Chladenie len s FV", "chlazení pouze s FV") // TODO translate
MAKE_TRANSLATION(hpCircPumpWw, "hpcircpump", "circulation pump available during dhw", "Zirkulation möglich bei WW-Bereitung", "Circulatiepomp WP beschikbaar tijdens ww", "Värmebärarpump på vid varmvattenberedning", "pompa cyrkulacji dostępna w trakcie c.w.u.", "sirkulasjonspumpe tilgjengelig under varmtvann", "", "SKS esnasında sirkülasyon pompasu uygun", "pompa di circolazione disponibile durante ACS", "obehové čerpadlo k dispozícii počas TÚV", "oběhové čerpadlo dostupné během TUV") // TODO translate
MAKE_TRANSLATION(vp_cooling, "vpcooling", "valve/pump cooling", "Ventil/Pumpe für Kühlen", "Klep koeling", "Ventil/Pump kyla", "zawór/pompa chłodzenia", "varmepumpe kjøling", "", "vana/pompa soğuyor", "valvola/pompa raffrescamento", "chladenie ventilu/čerpadla", "ventil/čerpadlo chlazení") // TODO translate
MAKE_TRANSLATION(VC0valve, "vc0valve", "VC0 valve", "VC0 Ventil", "Klep VC0", "VC0 Ventil", "zawór VC0", "vc0 ventil", "", "VC0 vana", "valvola VC0", "VC0 ventil", "ventil VC0") // TODO translate
MAKE_TRANSLATION(primePump, "primepump", "primary heatpump", "Hauptpumpe", "Hoofdpomp", "Primär Pump", "główna pompa ciepła", "primærpumpe", "", "ana ısı pompası", "pompa principale riscaldamento", "primárne tepelné čerpadlo", "primární tepelný čerpadlo") // TODO translate
MAKE_TRANSLATION(primePumpMod, "primepumpmod", "primary heatpump modulation", "Modulation Hauptpumpe", "Modulatie hoofdpomp", "Modulation Primär Pump", "wysterowanie głównej pompy ciepła", "primærpumpelast", "", "ana ısı pompası modülasyon", "pompa principale modulazione riscaldamento", "primárna modulácia tepelného čerpadla", "modulace primárního tepelného čerpadla") // TODO translate
MAKE_TRANSLATION(hp3wayValve, "hp3way", "3-way valve", "3-Wege-Ventil", "3-weg klep", "3-vägsventil", "zawór 3-drogowy pompy ciepła", "3-veisventil", "", "3 yollu vana", "valvola 3-vie", "3-cestný ventil", "3-cestný ventil") // TODO translate
MAKE_TRANSLATION(hp4wayValve, "hp4way", "4-way valve (VR4)", "4-Wege-Ventil (VR4)", "4-weg klep (VR4)", "4-vägsventil (VR4)", "zawór 4-drogowy pompy ciepła (VR4)", "4-veisventil (VR4)", "(VR4)", "4 yollu vana (VR4)", "valvola 4-vie (VR4)", "4-cestný ventil (VR4)", "4-cestný ventil (VR4)") // TODO translate
MAKE_TRANSLATION(elHeatStep1, "elheatstep1", "el. heater step 1", "El. Heizer Stufe 1", "Electrische bijverwarmer niveau 1", "Eltillskott Steg 1", "dogrzewacz poziom 1", "el-kolbe steg 1", "", "el.ısıtıcı adım 1", "riscaldatore elettrico livello 1", "krok 1 elektrického ohrievača", "elektrický ohřívač stupeň 1") // TODO translate
MAKE_TRANSLATION(elHeatStep2, "elheatstep2", "el. heater step 2", "El. Heizer Stufe 2", "Electrische bijverwarmer niveau 2", "Eltillskott Steg 2", "dogrzewacz poziom 2", "el-kolbe steg 2", "", "el.ısıtıcı adım 2", "riscaldatore elettrico livello 2", "krok 2 elektrického ohrievača", "elektrický ohřívač stupeň 2") // TODO translate
MAKE_TRANSLATION(elHeatStep3, "elheatstep3", "el. heater step 3", "El. Heizer Stufe 3", "Electrische bijverwarmer niveau 3", "Eltillskott Steg 3", "dogrzewacz poziom 3", "el-kolbe steg 3", "", "el.ısıtıcı adım 3", "riscaldatore elettrico livello 3", "krok 3 elektrického ohrievača", "elektrický ohřívač stupeň 3") // TODO translate
MAKE_TRANSLATION(wwAlternatingOper, "alternatingop", "alternating operation", "Wechselbetrieb", "Wisselbedrijf ww", "Växeldrift", "praca naprzemienna", "alternativ drift", "", "sıcak kullanım suyu alternatif işletim", "funzionamento alternato", "striedavá prevádzka", "střídavý provoz") // TODO translate
MAKE_TRANSLATION(wwAltOpPrioHeat, "altopprioheat", "prioritise heating during dhw", "Heizen bevorzugt vor WW", "Proriteit verwarming boven ww", "Varmvattenprioritet", "czas na ogrzewanie w trakcie c.w.u", "prioritert oppvarmning", "", "sıcak kullanım suyu esnasında ısıtmayı öne al", "dare la priorità al riscaldamento durante l'ACS", "Uprednostniť ohrev počas TÚV", "prioritizace vytápění během TUV") // TODO translate
MAKE_TRANSLATION(wwAltOpPrioWw, "altopprio", "prioritise dhw during heating", "bevorzugt vor Heizen", "Prioriteit ww boven verwarming", "Värmeprioritet", "czas na c.w.u w trakcie ogrzewania", "prioritert varmtvann", "", "ısıtma esnasında sıcak kullanım suyunu öne al", "dare priorità all'acqua calda durante il riscaldamento", "uprednostniť TÚV počas ohrevu", "prioritizace TUV během vytápění") // TODO translate
MAKE_TRANSLATION(hpEA0, "hpea0", "condensate reservoir heating (EA0)", "Heizung Kondensatwanne (EA0)", "", "Värme kondenstråg (EA0)", "ogrzewanie zbiornika kondensatu (EA0)", "", "", "", "", "ohrievanie zásobníka kondenzátu (EA0)", "ohřev kondenzátní nádrže (EA0)") // TODO translate
MAKE_TRANSLATION(boost, "boost", "boost mode", "Boost", "", "Boost-läge", "tryb wzmocnienia (boost)", "", "", "", "", "boost režim", "režim boost") // TODO translate
MAKE_TRANSLATION(boosttime, "boosttime", "boost time", "Boost-Dauer", "", "Boost-tid", "czas trwania wzmocnienia", "", "", "", "", "čas trvania posilnenia", "čas trvání režimu boost") // TODO translate
MAKE_TRANSLATION(hpPumpMode, "hppumpmode", "primary heatpump mode", "primärer Wärmepumpenmodus", "", "Driftläge värmebärarpump", "tryb pracy głównej pompy ciepła", "", "", "", "", "režim primárneho tepelného čerpadla", "režim primárního tepelného čerpadla") // TODO translate
MAKE_TRANSLATION(instantstart, "instantstart", "instant start", "Sofortstart", "", "Gränsvärde direktstart värme", "natychmiastowy start", "", "", "", "", "okamžité spustenie", "okamžité spuštění") // TODO translate
MAKE_TRANSLATION(heatondelay, "heatondelay", "heat-on delay", "Einschaltverzögerung Heizen", "", "Inkopplingsfördröjning värme", "opóźnienie włączania ogrzewania", "", "", "", "", "Oneskorenie zapnutia kúreni", "zpoždění zapnutí topení") // TODO translate
MAKE_TRANSLATION(heatoffdelay, "heatoffdelay", "heat-off delay", "Ausschaltverzögerung Heizen", "", "Frånkopplingsfördröjning värme", "opóźnienie włączania ogrzewania", "", "", "", "", "Oneskorenie vypnutia kúrenia", "zpoždění vypnutí topení") // TODO translate
MAKE_TRANSLATION(hpSetDiffPress, "hpsetdiffpress", "set differential pressure", "Pumpensolldruck", "", "VP Tryckskillnad", "różnica ciśnień", "", "", "", "", "nastaviť diferenčný tlak", "nastavení rozdílového tlaku") // TODO translate
MAKE_TRANSLATION(hpFan, "fan", "fan", "Lüfter", "", "Fläkt", "wentylator", "", "", "", "", "ventilátor", "ventilátor") // TODO translate
MAKE_TRANSLATION(hpShutdown, "shutdown", "shutdown", "Abschalten", "", "Stäng av", "wyłączenie", "", "", "", "", "vypnutie", "vypnutí") // TODO translate
MAKE_TRANSLATION(pc0Flow, "pc0flow", "Flow PC0", "Durchfluss PC0", "", "Flöde värmebärarpump", "", "", "", "", "", "prietok PC0", "průtok PC0") // TODO translate
MAKE_TRANSLATION(pc1Flow, "pc1flow", "Flow PC1", "Durchfluss PC1", "", "Flöde cirkulationspump", "", "", "", "", "", "prietok PC1", "průtok PC1") // TODO translate
MAKE_TRANSLATION(pc1On, "pc1on", "PC1", "PC1", "", "Cirkulationspump", "", "", "", "", "", "PC1", "PC1") // TODO translate
MAKE_TRANSLATION(pc1Rate, "pc1rate", "PC1 rate", "PC1 Rate", "", "Hastighet cirkulationspump", "", "", "", "", "", "sadzba PC1", "míra PC1") // TODO translate
// hybrid heatpump
MAKE_TRANSLATION(hybridStrategy, "hybridstrategy", "hybrid control strategy", "Hybrid-Steuerungsstrategie", "Hybride strategie", "Hybrid kontrollstrategi", "strategia sterowania hybrydowego", "hybrid kontrollstrategi", "stratégie contrôle hybride", "hibrit kontrol stratejisi", "strategia comtrollo ibrido", "hybridná stratégia riadenia", "strategie hybridního řízení")
MAKE_TRANSLATION(switchOverTemp, "switchovertemp", "outside switchover temperature", "Außentemperatur für Umschaltung", "Schakeltemperatuur buitentemperatuur", "Utomhus Omställningstemperatur", "zewnętrzna temperatura przełączania", "utendørstemp styring", "basculement par température extérieure", "geçiş için dış sıcaklık", "temperatura esterna per commutazione", "vonkajšia prepínacia teplota", "teplota přepnutí venku")
MAKE_TRANSLATION(energyCostRatio, "energycostratio", "energy cost ratio", "Energie-/Kostenverhältnis", "Energiekostenratio", "Energi/Kostnads-förhållande", "współczynnik energia/koszt", "energi/kostnads forhold", "ratio coût énergie", "enerji maliyet oranı", "rapporto energia/costo", "pomer nákladov na energiu", "poměr nákladů na energii")
MAKE_TRANSLATION(fossileFactor, "fossilefactor", "fossile energy factor", "Energiefaktor Fossil", "Energiefactor fossiele brandstof", "Faktor fossilenergi", "udział energii z paliw kopalnych", "energifaktor fossilenergi", "facteur énergie fossile", "fosil yakıt faktörü", "fattore energia fossile", "faktor fosílnej energie", "faktor fosilní energie")
MAKE_TRANSLATION(electricFactor, "electricfactor", "electric energy factor", "Energiefaktor elektrisch", "Energiefactor electrisch", "Faktor elenergi", "udział energii elektrycznej", "elektrisk energifaktor", "facteur énergie électrique", "elektrik enerjisi faktörü", "fattore energia elettrica", "faktor elektrickej energie", "faktor elektrické energie")
MAKE_TRANSLATION(delayBoiler, "delayboiler", "delay boiler support", "Verzögerungsoption", "Vertragingsoptie", "Fördröjningsoption", "opcja opóźnienia", "Fördörjningsoption", "option retardement chaudière", "kazan desteğini ötele", "opzione ritardo caldaia", "oneskorená podpora kotla", "zpoždění podpory kotle")
MAKE_TRANSLATION(tempDiffBoiler, "tempdiffboiler", "temp diff boiler support", "Temperaturdifferenzoption", "Verschiltemperatuuroptie", "Temperaturskillnadsoption", "opcja różnicy temperatur", "temperatursforskjell kjele", "option différence température", "sıcaklık farkı kazan desteği", "opzione differenza temperatura", "možnosť rozdielu teplôt", "teplotní rozdíl pro podporu kotle")
MAKE_TRANSLATION(lowNoiseMode, "lownoisemode", "low noise mode", "Geräuscharmer Betrieb", "Stil bedrijf", "Tyst drift", "tryb cichy", "stillemodus", "mode faible bruit", "düşük ses modu", "modalità a basso rumore", "režim nízkej hlučnosti", "režim nízkého hluku")
MAKE_TRANSLATION(lowNoiseStart, "lownoisestart", "low noise starttime", "Start geräuscharmer Betrieb", "Start stil bedrijf", "Tyst drift starttid", "początek trybu cichego", "stille modu starttid", "heure démarrage faible bruit", "düşük ses başlangıç", "ora di avvio a basso rumore", "nízka hlučnosť spustenia", "čas začátku nízkého hluku")
MAKE_TRANSLATION(lowNoiseStop, "lownoisestop", "low noise stoptime", "Stopp geräuscharmer Betrieb", "Stop stil bedrijf", "Tyst drift stopptid", "koniec trybu cichego", "stille modus stopptid", "heure arrêt faible bruit", "düşük ses bitiş", "ora di arresto funzionamento silenzioso", "doba zastavenia s nízkou hlučnosťou", "čas konce nízkého hluku")
MAKE_TRANSLATION(energyPriceGas, "energypricegas", "energy price gas", "Energiepreis Gas", "Energieprijs gas", "Gaspris", "cena energii z gazu", "energipris gass", "prix énergie gaz", "gaz enerjisi fiyatı", "prezzo energia gas", "cena energie plyn", "cena energie plyn")
MAKE_TRANSLATION(energyPriceEl, "energypriceel", "energy price electric", "Energiepreis Eletrizität", "energieprijs electriciteit", "Elpris", "cena energii elektrycznej", "strømpris", "prix énergie électrique", "elektrik enerjisi fiyatı", "prezzo energia elettrica", "cena elektrickej energie", "cena energie elektřina")
MAKE_TRANSLATION(energyPricePV, "energyfeedpv", "feed in PV", "PV-Einspeisevergütung", "PV teruglevertarief", "PV Energi", "cena energii PV", "strømpris PV", "alimentation PV", "giren güneş enerjisi", "energia fotovoltaico", "Výkupná cena FV", "přetok FV")
MAKE_TRANSLATION(hybridDHW, "hybriddhw", "hybrid DHW", "Hybrid-Warmwasser", "Hybride ww", "Hybridläge varmvatten", "hybrydowa c.w.u.", "hybridmodus varmtvann", "ecs hybride", "hibrit SKS", "ACS ibrida", "hybridná TÚV", "hybridní TUV")
MAKE_TRANSLATION(airPurgeMode, "airpurgemode", "air purge mode", "Luftspülung", "Luchtzuivering", "Luftreningsläge", "tryb oczyszczania powietrza", "luftsrensningsmodus", "mode purge air", "hava temizleme modu", "modalita spurgo aria", "režim čistenia vzduchu", "režim odvzdušnění")
MAKE_TRANSLATION(heatPumpOutput, "heatpumpoutput", "heatpump output", "WP-Leistung", "WP output", "Värmepumpseffekt", "moc wyjściowa pompy ciepła", "varmepumpeeffekt", "sortie pompe à chaleur", "ısı pompası çıkışı", "prestazione pompa calore", "Výkon tepelného čerpadla", "výstup tepelného čerpadla")
MAKE_TRANSLATION(coolingCircuit, "coolingcircuit", "cooling circuit", "Kühlkreislauf", "Koelcircuit", "Kylkrets", "obwód chłodzący", "kjølekrets", "circuit refroidissement", "soğutma devresi", "circuito raffreddante", "chladiaci okruh", "chlazení okruhu")
MAKE_TRANSLATION(compStartMod, "compstartmod", "compressor start modulation", "Kompressorstartmodulation", "Beginvermogen compressor", "Kompressor startmodulering", "początkowa modulacja sprężarki", "kompressor startmodulering", "modulation démarrage compresseur", "kazan başlangıç modülasyonu", "avvio modulazione compressore", "modulácia štartu kompresora", "modulace startu kompresoru")
MAKE_TRANSLATION(heatDrainPan, "heatdrainpan", "heat drain pan", "Wärmeausgleichsgefäß", "Vereffeningsvat", "Uppvärm. dränering", "zbiornik wyrównawczy ciepła", "oppvarming drenering", "bac récupération chaleur", "ısı tahliye tablası", "serbatoio scarico condensa", "odkvapkávacia nádoba na teplo", "ohřev odtokové pánve")
MAKE_TRANSLATION(heatCable, "heatcable", "heating cable", "Heizband", "heating cable", "värmekabel", "przewód grzejny", "varmekabel", "câble chauffant", "ısıtma kablosu", "cavo riscaldante", "vykurovací kábel", "topný kabel")
// alternative heatsource AM200
MAKE_TRANSLATION(aCylTopTemp, "cyltoptemp", "cylinder top temperature", "Speichertemperatur Oben", "Buffer temperatuur boven", "Cylindertemperatur Toppen", "temperatura na górze cylindra", "beredertemperatur topp", "température haut cylindre", "silindir üst yüzey sıcaklığı", "temperatura superiore accumulo", "vrchná teplota valca", "teplota horní části zásobníku")
MAKE_TRANSLATION(aCylCenterTemp, "cylcentertemp", "cylinder center temperature", "Speichertemperatur Mitte", "Buffer temperatuur midden", "Cylindertemperatur Mitten", "temperatura na środku cylindra", "beredertemperatur midten", "température centre cylindre", "silindir merkez sıcaklığı", "temperatura centrale accumulo", "stredná teplota valca", "teplota středu zásobníku")
MAKE_TRANSLATION(aCylBottomTemp, "cylbottomtemp", "cylinder bottom temperature", "Speichertemperatur Unten", "Buffer temperatuur onder", "Cylindertemperatur Botten", "temperatura na dole cylindra", "beredertemperatur nederst", "température fond cylindre", "silindir taban sıcaklığı", "temperatura inferiore accumulo", "teplota dna valca", "teplota dolní části zásobníku")
MAKE_TRANSLATION(aFlowTemp, "altflowtemp", "alternative hs flow temperature", "Alternative WE-Vorlauftemperatur", "Alternatieve warmtebron aanvoertemperatuur", "Alternativ flödestemp värmekälla", "temperatura zasilania z alternatywnego źródła", "alternativ varmekilde tilførselstemperatur", "température flux hs alternative", "alternatif ısı kaynağı besleme sıcaklığı", "temperatura alternativa mandata hs", "alternatívna výstupná teplota hs", "alternativní výstupný teplota hs")
MAKE_TRANSLATION(aRetTemp, "altrettemp", "alternative hs return temperature", "Alternative WE-Rücklauftemperatur", "Alternatieve warmtebron retourtemperatuur", "Alternativ returtemp värmekälla", "temperatura powrotu z alternatywnego źródła", "alternativ varmekilde returtemperatur", "température retour hs alternative", "alternatif ısı kaynağı dönüş sıcaklığı", "temperatura alternativa ritorno hs", "alternatívna teplota spiatočky hs", "alternatívna teplota zpátečky hs")
MAKE_TRANSLATION(sysFlowTemp, "sysflowtemp", "system flow temperature", "Systemvorlauftemperatur", "Systeem aanvoertemperatuur", "Systemflödestemperatur", "temperatura zasilania systemu", "systemturtemperatur", "température flux système", "sistem besleme sıcaklığı", "temperatura di mandata impianto", "teplota prívodu systému", "teplota přívodu systému")
MAKE_TRANSLATION(sysRetTemp, "sysrettemp", "system return temperature", "Systemrücklauftemperatur", "Systeem retourtemperatuur", "Systemreturtemperatur", "temperatura powrotu z systemu", "systemreturtemperatur", "température retour système", "sistem dönüş sıcaklığı", "temperatura di ritorno impianto", "teplota spiatočky systému", "teplota zpátečky systému")
MAKE_TRANSLATION(valveByPass, "valvebypass", "bypass valve", "Bypassventil", "Bypass klep", "Bypassventil", "zawór obejścia", "bypassventil", "vanne dérivation", "baypas vanası", "valvola Bypass", "obtokový ventil", "bypassový ventil")
MAKE_TRANSLATION(valveBuffer, "valvebuffer", "buffer valve", "Pufferventil", "Bufferklep", "Buffertventil", "zawór bufora", "buffertventil", "vanne tampon", "tampon vanası", "valvola tampone", "nárazový ventil", "ventil vyrovnávací nádrže")
MAKE_TRANSLATION(valveReturn, "valvereturn", "return valve", "Rückflussventil", "Retourklep", "Returventil", "zawór powrotu", "returventil", "vanne retour", "dönüş vanası", "valvola ritorno", "spätný ventil", "návratový ventil")
MAKE_TRANSLATION(aPumpMod, "apumpmod", "alternative hs pump modulation", "Alternative WE-Pumpenmodulation", "Alternatieve warmtebron pomp modulatie", "Alternativ Pumpmodulering Värmekälla", "modulacja pompy alternatywnego źródła ciepła", "alternativ pumpemodulering varmekilde", "modulation alternative pompe hs", "alternatif ısı kaynağı pompa modülasyonu", "pompa modulazione alternativa hs", "alternatívna modulácia čerpadla hs", "alternativní modulace čerpadla hs")
MAKE_TRANSLATION(heatSource, "heatsource", "alternative heating active", "Alternativer Wärmeerzeuger aktiv", "Alternatieve warmtebron aktief", "Alternativ Värmekälla aktiv", "aktywne alternatywne źródło ciepła", "alternativ varmekilde aktiv", "chauffage alternatif actif", "alternatif ısınma devrede", "riscaldamento alternativo attivo", "alternatívne kúrenie aktívne", "alternativní vytápění aktivní")
MAKE_TRANSLATION(aPump, "apump", "alternative hs pump", "Alternative WE-Pumpe", "Alternatieve warmtebron pomp", "Alternativ Pump Värmekälla", "pompy alternatywnego źródła ciepła", "alternativ pumpe varmekilde", "alternative pompe hs", "alternatif ısı kaynağı pompası", "pompa alternativa hs", "alternatívne čerpadlo hs", "alternativní čerpadlo hs")
MAKE_TRANSLATION(burner, "burner", "burner", "Brenner", "Brander", "Brännare", "palnik", "", "", "kazan", "bruciatore", "horák", "hořák") // TODO translate
MAKE_TRANSLATION(heatRequest, "heatrequest", "heat request", "Wärmeanforderung", "Warmtevraag", "Värmebehov", "zapotrzebowanie na ciepło", "varmeforespørsel", "", "ısı talebi", "richiesta calore", "požiadavka na teplo", "požadavek na teplo") // TODO translate
MAKE_TRANSLATION(blockRemain, "blockremain", "remaining blocktime", "verbleibende Sperrzeit", "Resterende bloktijd", "Återstående blockeringstid", "czas do końca blokady", "gjenstående blokkeringstid", "", "kalan blok süresi", "tempo di blocco rimanente", "zostávajúci čas blokovania", "zbývající doba blokace") // TODO translate
MAKE_TRANSLATION(blockRemainWw, "blockremaindhw", "remaining blocktime dhw", "verbleibende Sperrzeit WW", "Resterende bloktijd ww", "Återstående blockeringstid varmvatten", "czas do końca blokady c.w.u.", "gjenværende blokkeringstid bereder", "", "kalan sıcak kullanım suyu blok süresi", "tempo di blocco rimanente ACS", "zostávajúci čas blokovania TÚV", "zbývající doba blokace TUV") // TODO translate
MAKE_TRANSLATION(flueGasTemp, "fluegastemp", "flue gas temperature", "Abgastemperatur", "Rookafvoertemperatuur", "Rökgastemperatur", "temperatura spalin", "røykgasstemperatur", "", "baca gazı sıcaklığı", "temperatura gas di scarico", "teplota spalín", "teplota spalin") // TODO translate
MAKE_TRANSLATION(vr2Config, "vr2config", "vr2 configuration", "VR2 Konfiguration", "VR2 configuratie", "VR2 Konfiguration", "konfiguracja VR2", "vr2 konfigurasjon", "configuration vr2", "vr2 ayarı", "configurazione VR2", "konfigurácia vr2", "konfigurace VR2")
MAKE_TRANSLATION(ahsActivated, "ahsactivated", "alternate heat source activation", "Alt. Wärmeerzeuger aktiviert", "Altenatieve warmtebron geactiveerd", "Alternativ värmekälla aktivering", "aktywacja alternatywnego źródła ciepła", "alternativ varmekilde aktivering", "activation source chaleur alternative", "alternatif ısı kaynağı devrede", "attivazione fonte di calore alternativa", "aktivácia alternatívneho zdroja tepla", "aktivace alternativního zdroje tepla")
MAKE_TRANSLATION(aPumpConfig, "apumpconfig", "primary pump config", "Konfig. Hauptpumpe", "Primaire pomp configuratie", "Konfiguration Primärpump", "konfiguracja pompy głównej", "konfiguration primærpumpe", "configuration pompe primaire", "ana pompa ayarı", "configurazione pompa primaria", "konfigurácia primárneho čerpadla", "konfigurace primárního čerpadla")
MAKE_TRANSLATION(aPumpSignal, "apumpsignal", "output for pr1 pump", "Ausgang Pumpe PR1", "Output voor pomp PR1", "Utgång från pump PR1", "wyjście pompy PR1", "utgang fra pumpe PR1", "sortie pompe pr1", "p1 pompa çıkışı", "uscita per pompa PR1", "výstup pre čerpadlo pr1", "výstup pro čerpadlo PR1")
MAKE_TRANSLATION(aPumpMin, "apumpmin", "min output pump pr1", "Minimale Pumpenansteuerung PR1", "Minimale output pomp PR1", "Min Output Pump PR1", "minimalne wysterowanie pompy PR1", "minimal output pumpe PR1", "sortie min pompe pr1", "p1 pompa minimum çıkış", "uscita minima pompa PR1", "min. výstupné čerpadlo pr1", "minimální výstup čerpadla PR1")
MAKE_TRANSLATION(tempRise, "temprise", "ahs return temp rise", "Rücklauftemperaturerhöhung", "Verhoging retourtemperatuur", "Förhöjd returtemperatur", "wzrost temperatury powrotu", "forhøyd returtemperatur", "augmentation température retour ahs", "alternatif ısı kaynağı dönüş sıcaklığı yükseldi", "aumento della temperatura di ritorno", "Zvýšenie teploty spiatočky", "zvýšení teploty zpátečky")
MAKE_TRANSLATION(setReturnTemp, "setreturntemp", "set temp return", "Soll-Rücklauftemperatur", "Streeftemperatuur retour", "Vald returtemperatur", "zadana temperatura powrotu", "valgt returtemperatur", "régler température retour", "hedef dönüş sıcaklığı", "imposta temperatura di ritorno", "cieľová teplota spiatočky", "nastavení teploty zpátečky")
MAKE_TRANSLATION(mixRuntime, "mixruntime", "mixer run time", "Mischerlaufzeit", "Mixer looptijd", "Blandningsventil drifttid", "czas pracy miksera", "blandingsventil drifttid", "durée fonctionnement mélangeur", "karışım çalışma süresi", "tempo di funzionamento del miscelatore", "doba chodu mixéra", "doba běhu mixéru")
MAKE_TRANSLATION(bufBypass, "bufbypass", "buffer bypass config", "Puffer-Bypass-Konfig.", "Buffer bypass configuratie", "Konfiguration Buffer bypass", "konfiguracja z obejściem bufora", "konfigurasjon buffer bypass", "configuration contournement buffer", "tampon baypas ayarı", "configurazione bypass del tampone", "konfigurácia vynechania vyrovnávacej pamäte", "konfigurace vyrovnávacího bypassu")
MAKE_TRANSLATION(bufMixRuntime, "bufmixruntime", "bypass mixer run time", "Speicher-Mischer-Laufzeit", "Buffer mixer looptijd", "Blandningsventil Bypass drifttid", "czas pracy mieszacza obejścia", "blandningsventil bypass drifttid", "durée fonctionnement contournement mélangeur", "baypas karıştırıcı çalışma süresi", "tempo funzionamento bypass miscelatore", "doba chodu obtokového mixéra", "doba běhu bypassového mixéru")
MAKE_TRANSLATION(bufConfig, "bufconfig", "dhw buffer config", "Konfig. Warmwasserspeicher", "Warmwater boiler configuratie", "Konfiguration Varmvattentank", "konfiguracja bufora c.w.u.", "konfigurasjon varmvannstank", "configuration buffer ecs", "sıcak su tampon ayarı", "configurazione tampone ACS", "konfigurácia zásobníka TÚV", "konfigurace vyrovnávacího zásobníku TUV")
MAKE_TRANSLATION(blockMode, "blockmode", "config htg. blocking mode", "Konfig. Sperrmodus", "Configuratie blokeermodus", "Konfiguration Blockeringsläge", "konfiguracja trybu blokady", "konfigurasjon blokkeringsmodus", "config mode blocage htg.", "blok modu yapılandırması", "configurazione modalità di blocco", "Režim uzamknutia konfigurácie", "konfigurace blokovacího režimu vytápění")
MAKE_TRANSLATION(blockTerm, "blockterm", "config of block terminal", "Konfig. Sperrterminal", "Configuratie blookerterminal", "Konfiguration Blockeringsterminal", "konfiguracja terminala blokującego", "konfigurasjon blokkeringsterminal", "config. du bloque terminal", "blok terminal yapılandırması", "configurazione terminale di blocco", "Konfiguračný blokovací terminál", "konfigurace blokovacího terminálu")
MAKE_TRANSLATION(blockHyst, "blockhyst", "hyst. for boiler block", "Hysterese Sperrmodus", "Hysterese blokeerterminal", "Hysteres Blockeringsmodul", "tryb blokowania histerezy", "hystrese blokkeringsmodus", "hyst. Blocage chaudière", "kazan blok geçikmesi", "modalità blocco isteresi", "Režim hysterézneho zámku", "hystereze pro blokaci kotle")
MAKE_TRANSLATION(releaseWait, "releasewait", "boiler release wait time", "Wartezeit Kesselfreigabe", "Wachttijd ketel vrijgave", "Väntetid Frisläppning", "czas oczekiwania na zwolnienie kotła", "kjele frigjøringsventetid", "temps attente libération chaudière", "kazan tahliyesi bekleme süresi", "tempo di attesa sblocco caldaia", "doba čakania na uvoľnenie kotla", "doba čekání na uvolnění kotle")
// energy
MAKE_TRANSLATION(nrgTotal, "nrgtotal", "total energy", "Gesamtenergie", "", "Avgiven energi totalt", "całkowita energia", "", "", "", "", "celková energia", "celková energie") // TODO translate
MAKE_TRANSLATION(nrgHeat, "nrgheat", "energy heating", "Energie Heizen", "", "Avgiven energi värme", "energia na ogrzewanie", "", "", "ısıtma enerjisi", "energia vykurovania", "energetické vykurovanie", "energie pro vytápění") // TODO translate
MAKE_TRANSLATION(nrgCool, "nrgcool", "energy cooling", "Energie Kühlen", "", "Avgiven energi kyla", "", "", "", "", "", "energia chladenia", "energie pro chlazení") // TODO translate
MAKE_TRANSLATION(nrgWw, "nrg", "energy", "Energie", "", "Avgiven energi varmvatten", "energia", "", "", "sıcak kullanım suyu enerjisi", "", "energia", "energie") // TODO translate
MAKE_TRANSLATION(nrgHeat2, "nrgheat2", "energy heating 2", "Energie Heizen 2", "", "Avgiven energi värme 2", "energia na ogrzewanie 2", "", "", "ısıtma enerjisi 2", "", "energia vykurovania 2", "energie pro vytápění 2") // TODO translate
MAKE_TRANSLATION(nrgWw2, "nrg2", "energy 2", "Energie 2", "", "", "energia 2", "", "Avgiven energi varmvatten 2", "sıcak kullanım suyu enerjisi 2", "", "energia 2", "energie 2") // TODO translate
MAKE_TRANSLATION(nomPower, "nompower", "nominal Power", "Brennerleistung", "", "Nominell effekt", "moc nominalna", "", "", "nominal güç", "", "nominálny výkon", "nominální výkon") // TODO translate
MAKE_TRANSLATION(meterTotal, "metertotal", "meter total", "Gesamtmessung", "", "Förbrukad energi totalt", "licznik całkowity", "", "", "", "", "počítadlo celkom", "počítadlo celkem") // TODO translate
MAKE_TRANSLATION(meterComp, "metercomp", "meter compressor", "Messung Kompressor", "", "Förbrukad energi kompressor", "licznik sprężarki", "", "", "", "", "počítadlo kompresor", "počítadlo kompresoru") // TODO translate
MAKE_TRANSLATION(meterEHeat, "metereheat", "meter e-heater", "Messung E-Heizer", "", "Förbrukad energi eltillskott", "licznik dogrzewacza", "", "", "", "", "počítadlo e-ohrievača", "počítadlo elektrického topení") // TODO translate
MAKE_TRANSLATION(meterHeat, "meterheat", "meter heating", "Messung Heizen", "", "Förbrukad energi värme", "licznik ogrzewania", "", "", "", "", "počítadlo kúrenia", "počítadlo vytápění") // TODO translate
MAKE_TRANSLATION(meterCool, "metercool", "meter cooling", "Messung Kühlen", "", "Förbrukad energi kyla", "", "", "", "", "", "počítadlo chladenia", "počítadlo chlazení") // TODO translate
MAKE_TRANSLATION(meterWw, "meter", "meter", "Messung", "", "", "licznik", "", "Förbrukad energi varmvatten", "", "", "počítadlo", "počítadlo") // TODO translate
MAKE_TRANSLATION(gasMeterHeat, "gasmeterheat", "gas meter heating", "Gaszähler Heizen", "", "Gasförbrukning värme", "licznik gazu na ogrzewanie", "", "", "", "", "počítadlo plynu kúrenia", "počítadlo plynu pro vytápění") // TODO translate
MAKE_TRANSLATION(gasMeterWw, "gasmeter", "gas meter", "Gaszähler", "", "Gasförbrukning varmvatten", "licznik gazu", "", "", "", "", "počítadlo plynu", "počítadlo plynu") // TODO translate
MAKE_TRANSLATION(hpCurrPower, "hpcurrpower", "compressor current power", "akt. Kompressorleistung", "", "Kompressoreffekt", "", "", "", "", "", "aktuálny výkon kompresoru", "aktuální výkon kompresoru") // TODO translate
MAKE_TRANSLATION(hpPowerLimit, "hppowerlimit", "power limit", "Leistungsgrenze", "", "Begränsning kompressoreffekt", "", "", "", "", "", "obmedzenie výkonu", "omezení výkonu") // TODO translate
MAKE_TRANSLATION(powerReduction, "powerreduction", "power reduction", "Leistungsverringerung", "", "Reducerad kompressoreffekt", "", "", "", "", "", "obmedzenie výkonu", "omezení výkonu") // TODO translate
MAKE_TRANSLATION(fuelHeat, "fuelheat", "fuel consumption heating", "Verbrauch Heizen", "", "Bränsleförbrukning värme", "", "", "", "", "", "obmedzenie výkonu", "omezení výkonu") // TODO translate
MAKE_TRANSLATION(fuelDhw, "fueldhw", "fuel consumption", "Verbrauch", "", "Bränsleförbrukning varmvatten", "", "", "", "", "", "spotreba paliva", "") // TODO translate
MAKE_TRANSLATION(elHeat, "elheat", "el. consumption heating", "el. Verbrauch Heizen", "", "Elförbrukning värme", "", "", "", "", "", "el. spotreba kúrenie", "") // TODO translate
MAKE_TRANSLATION(elDhw, "eldhw", "el. consumption", "el. Verbrauch", "", "Elförbrukning varmvatten", "", "", "", "", "", "el. spotreba", "") // TODO translate
MAKE_TRANSLATION(elGenHeat, "elgenheat", "el. generation heating", "el. Erzeugung Heizen", "", "Elgenerering värme", "", "", "", "", "", "el. generovanie kúrenia", "") // TODO translate
MAKE_TRANSLATION(elGenDhw, "elgendhw", "el generation", "el. Erzeugung", "", "Elgenerering varmvatten", "", "", "", "", "", "el. generovanie", "") // TODO translate
// HIU
MAKE_TRANSLATION(netFlowTemp, "netflowtemp", "heat network flow temp", "Systemvorlauftemperatur", "Netto aanvoertemperatuur", "Temperatur fjärrvärmenät", "temp. zasilania sieci cieplnej", "", "", "ısıtma şebekesi akış derecesi", "temperatura di mandata della rete di riscaldamento", "teplota prívodu tepelnej siete", "teplota přívodu tepelné sítě") // TODO translate
// MAKE_TRANSLATION(cwFlowRate, "cwflowrate", "cold water flow rate", "Kaltwasser Durchfluss", "Stroomsnelheid koud water", "Kallvattensflöde", "przepływ zimnej wody", "", "", "soğuk su akış hızı", "portata acqua fredda", "prietok studenej vody", "průtok studené vody") // TODO translate
MAKE_TRANSLATION(keepWarmTemp, "keepwarmtemp", "keep warm temperature", "Warmhaltetemperatur", "Warmhoudtemperatuur", "", "Varmhållningstemperatur", "temperatura podtrzymywania ciepła", "", "sıcaklığı koruma derecesi", "mantenere la temperatura calda", "udržať teplú teplotu", "udržovací teplota") // TODO translate
MAKE_TRANSLATION(heatValve, "heatvalve", "heating valve", "Heizungsventil", "", "Ventil uppvärmning", "zawór ogrzewania", "", "", "", "", "vykurovací ventil", "ventil pro vytápění") // TODO translate
MAKE_TRANSLATION(wwValve, "dhwvalve", "valve", "Ventil", "", "Ventil varmvatten", "zawór", "", "", "", "", "ventil", "ventil") // TODO translate
// the following are dhw for the boiler and automatically tagged with 'dhw'
MAKE_TRANSLATION(wwSelTemp, "seltemp", "selected temperature", "gewählte Temperatur", "Geselecteerd temperatuur", "Vald Temperatur", "temperatura wyższa/komfort", "valgt temperatur", "température sélectionnée", "seçili sıcaklık", "temperatura selezionata", "zvolená teplota", "nastavená teplota")
MAKE_TRANSLATION(wwSelTempLow, "seltemplow", "selected lower temperature", "ausgewählte untere Temperatur", "Onderste streeftemperatuur", "Vald lägstatemperatur", "temperatura niższa/eko", "valgt nedre temperatur", "température basse sélectionnée", "seçili düşük sıcaklık", "bassa temperatura selezionata", "zvolená nižšia teplota", "redukovaná teplota")
MAKE_TRANSLATION(wwSelTempEco, "tempecoplus", "selected eco+ temperature", "ausgewählte ECO+ Temperatur", "eco+ streeftemperatuur", "Eco+ lägstatemperatur", "temperatura niższa/eko+", "valgt eco+ temperatur", "température eco+ sélectionnée", "seçili eco+ sıcaklık", "eco+ temperatura selezionata", "zvolená teplota eco+", "Eco+ teplota")
MAKE_TRANSLATION(wwSelTempOff, "seltempoff", "selected temperature for off", "ausgewählte Temperatur bei AUS", "Streeftemperatuur bij UIT", "Vald temperatur för AV", "temperatura gdy grzanie wyłączone", "valgt tempereatur for av", "température sélectionnée pour arrêt", "kapanma için seçili sıcaklık", "temperatura selezionata per spegnimento", "zvolená teplota pre vypnutie", "teplota pro vypnutí")
MAKE_TRANSLATION(wwOneTime, "onetime", "one time charging", "Einmalladung", "Buffer eenmalig laden", "Engångsladdning", "jednorazowa dodatkowa ciepła woda", "engangsoppvarming", "charge unique", "tek seferlik doldurma", "carica singola", "jednorazové nabíjanie", "jednorázové nabíjení")
MAKE_TRANSLATION(wwSelTempSingle, "seltempsingle", "single charge temperature", "Einmalladungstemperatur", "Streeftemperatuur enkele lading", "Temperatur Engångsladdning", "temperatura dodatkowej ciepłej wody", "temp engangsoppvarming", "température charge unique", "tek şarj sıcaklığı", "temperatura singolaa carica", "teplota na jedno nabitie", "teplota jednorázového nabíjení")
MAKE_TRANSLATION(wwOneTimeKey, "onetimekey", "one time key function", "Einmalladungstaste", "Knop voor eenmalig laden buffer", "Engångsfunktion", "przycisk jednorazowego ogrzania", "engangsknapp varme", "fonction touche unique", "tek seferlik doldurma fonksiyonu", "pulsante funzione singola", "jednorazová kľúčová funkcia", "jednorázová funkce klíče")
MAKE_TRANSLATION(wwCylMiddleTemp, "cylmiddletemp", "cylinder middle temperature (TS3)", "Speichertemperatur Mitte", "Buffer temperatuur midden", "Cylinder Temperatur Mitten (TS3)", "temperatura środka cylindra (TS3)", "vanntank midten temperatur (TS3)", "température moyenne ballon (TS3)", "Silindir orta sıcaklığı", "temperatura centrale accumulo (TS3)", "stredná teplota valca (TS3)", "teplota středu zásobníku (TS3)")
MAKE_TRANSLATION(wwSetTemp, "settemp", "set temperature", "Solltemperatur", "Streeftemperatuut", "Börtemperatur", "temperatura zadana", "innstilt temperatur", "régler température", "hedef sıcaklık", "imposta temperatura", "nastavená teplota", "nastavená teplota")
MAKE_TRANSLATION(wwType, "type", "type", "Typ", "type", "Typ", "typ", "type", "type", "tip", "tipo", "typ", "typ")
MAKE_TRANSLATION(wwComfort, "comfort", "comfort", "Komfort", "Comfort", "Komfort", "komfort", "komfort", "confort", "konfor", "Comfort", "komfort", "komfort")
MAKE_TRANSLATION(wwComfort1, "comfort1", "comfort mode", "Komfort-Modus", "Comfort modus", "Komfortläge", "tryb komfortu", "komfort modus", "mode confort", "konfor modu", "modalità comfort", "komfortný režim", "komfortní režim")
MAKE_TRANSLATION(wwFlowTempOffset, "flowtempoffset", "flow temperature offset", "Anhebung Vorlauftemperatur", "Aanvoertemperatuur offset", "Flödestemperatur förskjutning", "korekta temperatury wypływu", "turtemperaturforskyvning", "offset température flux", "akış sıcaklığı artışı", "aumento della temperatura di ritorno", "Posun teploty prívodu", "kompenzace teploty přívodu")
MAKE_TRANSLATION(wwMaxPower, "maxpower", "max power", "max. Leistung", "Maximaal vermogen", "Max. Effekt", "moc. maksymalna", "maks. effekt", "puissance max.", "maksimum güç", "potenza massima", "maximálny výkon", "maximální výkon")
MAKE_TRANSLATION(wwCircPump, "circpump", "circulation pump available", "Zirkulationspumpe vorhanden", "Circulatiepomp aanwezig", "Cirkulationspump tillgänglig", "pompa cyrkulacji zainstalowana", "sirkulasjonspumpe tilgjengelig", "pompe circulation disponible", "sikülasyon pompası müsait", "pompa circolazione disponibile", "dostupné obehové čerpadlo", "oběhové čerpadlo k dispozici")
MAKE_TRANSLATION(wwChargeType, "chargetype", "charging type", "Speicherladungstyp", "Buffer laadtype", "Laddningstyp", "sposób grzania zasobnika", "varmetype", "type chargement", "şarj tipi", "tipo caricamento", "typ nabíjania", "typ nabíjení")
MAKE_TRANSLATION(wwCircMode, "circmode", "circulation pump mode", "Zirkulationspumpenmodus", "Modus circulatiepomp", "Läge Cirkulationspump", "tryb pracy cyrkulacji", "sikulasjonspumpemodus", "mode pompe circulation", "sirkülasyon pompa modu", "modalità pompa circolazione", "režim obehového čerpadla", "režim oběhového čerpadla")
MAKE_TRANSLATION(wwCirc, "circ", "circulation active", "Zirkulation aktiv", "Circulatiepomp actief", "Cirkulation aktiv", "pompa cyrkulacji", "sirkulasjon aktiv", "circulation active", "sirkülasyon devrede", "circolazione attiva", "obeh aktívny", "oběh aktivní")
MAKE_TRANSLATION(wwCurTemp, "curtemp", "current intern temperature", "aktuelle interne Temperatur", "Huidige interne temperatuur", "Intern Temperatur", "temperatura zasobnika", "gjeldende intern temperatur", "température interne actuelle", "güncel iç sıcaklık", "temperatura interna attuale", "aktuálna vnútorná teplota", "aktuální vnitřní teplota")
MAKE_TRANSLATION(wwCurTemp2, "curtemp2", "current extern temperature", "aktuelle externe Temperatur", "Huidige externe temperatuur", "Extern Temperatur", "temperatura wypływu", "gjeldende ekstern temperaur", "température externe actuelle", "güncel dış sıcaklık", "temperatura esterna attuale", "aktuálna vonkajšia teplota", "aktuální venkovní teplota")
MAKE_TRANSLATION(wwCurFlow, "curflow", "current tap water flow", "aktueller Durchfluss", "Hudige warmwater doorstroming", "Aktuellt tappvattenflöde", "aktualny przepływ", "gjeldende tappevannshastighet", "débit actuel eau robinet", "güncel musluk suyu akışı", "portata corrente dell'acqua del rubinetto", "aktuálny prietok vody z vodovodu", "aktuální průtok TUV")
MAKE_TRANSLATION(wwStorageTemp1, "storagetemp1", "storage intern temperature", "interne Speichertemperatur", "Interne buffertemperatuur", "Beredare Intern Temperatur", "temperatura wewnątrz zasobnika", "intern temperatur bereder", "température interne stockage", "depo iç sıcaklığı", "temperatura di conservazione interna", "interná teplota skladovania", "vnitřní teplota zásobníku")
MAKE_TRANSLATION(wwStorageTemp2, "storagetemp2", "storage extern temperature", "externe Speichertemperatur", "Externe buffertemperatuur", "Beredare Extern Temperatur", "temperatura na wyjściu zasobnika", "ekstern temperatur bereder", "température externe stockage", "depo dış sıcaklığı", "temperatura di conservazione esterna", "vonkajšia teplota skladovania", "venkovní teplota zásobníku")
MAKE_TRANSLATION(wwActivated, "activated", "activated", "aktiviert", "geactiveerd", "Aktiverad", "system przygotowywania c.w.u.", "aktivert", "activé", "devreye girdi", "attivato", "aktivovaný", "aktivováno")
MAKE_TRANSLATION(wwDisinfecting, "disinfecting", "disinfecting", "Desinfizieren", "Desinfectie", "Desinficerar", "dezynfekcja termiczna", "desinfiserer", "désinfection", "dezenfekte ediliyor", "disinfezione", "dezinfekcia", "dezinfekce")
MAKE_TRANSLATION(wwDisinfectionTemp, "disinfectiontemp", "disinfection temperature", "Desinfektionstemperatur", "Desinfectietemperatuur", "Desinfektionstemperatur", "temperatura dezynfekcji termicznej", "desinfeksjonstemperatur", "température désinfection", "dezenfeksiyon sıcaklığı", "temperatura disinfezione", "teplota dezinfekcie", "dezinfekční teplota")
MAKE_TRANSLATION(wwCharging, "charging", "charging", "Laden", "Laden", "Värmer", "grzanie", "varmer", "chargement", "dolduruluyor", "caricamento", "nabíjanie", "nabíjení")
MAKE_TRANSLATION(wwChargeOptimization, "chargeoptimization", "charge optimization", "Ladungsoptimierung", "laadoptimalisatie", "Laddningsoptimering", "optymalizacja grzania", "oppvarmingsoptimalisering", "optimisation charge", "dolum optimizasyonu", "ottimizzazione carica", "optimalizácia poplatkov", "optimalizace nabíjení")
MAKE_TRANSLATION(wwRecharging, "recharging", "recharging", "Nachladen", "herladen", "Laddar om", "ponowne grzanie", "varm på nytt", "en recharge", "tekrar dolduruluyor", "in ricarica", "nabíjanie", "dobíjení")
MAKE_TRANSLATION(wwTempOK, "tempok", "temperature ok", "Temperatur ok", "Temperatuur OK", "Temperatur OK", "temperatura OK", "temperatur ok!", "température ok", "sıcaklık tamam", "Temperatura OK", "teplota ok", "teplota v pořádku")
MAKE_TRANSLATION(wwActive, "active", "active", "aktiv", "Actief", "Aktiv", "aktywna", "aktiv", "actif", "devrede", "attivo", "aktívny", "aktivní")
MAKE_TRANSLATION(ww3wayValve, "3wayvalve", "3-way valve active", "3-Wege-Ventil aktiv", "3-wegklep actief", "Trevägsventil aktiv", "zawór 3-drogowy aktywny", "aktiv trevisventil", "vanne 3 voies active", "3 yollu vana", "valvola 3-vie", "3-cestný ventil aktívny", "aktivní 3-cestný ventil")
MAKE_TRANSLATION(wwMixerTemp, "mixertemp", "mixer temperature", "Mischertemperatur", "Mixertemperatuur", "Blandningsventil tempertur", "temperatura mieszacza", "temperatur blandeventil", "température mélangeur", "karıştırıcı sıcaklığı", "temperatura miscelatore", "teplota mixéra", "teplota směšovače")
MAKE_TRANSLATION(wwStarts, "starts", "starts", "Anzahl Starts", "Aantal starts", "Antal starter", "liczba załączeń", "antall starter", "démarrages", "başlıyor", "avvii", "Počet štartov", "Počet startů")
MAKE_TRANSLATION(wwStartsHp, "startshp", "starts hp", "Anzahl Starts WP", "", "Antal starter VP", "", "", "", "", "", "Počet spustení TČ", "") // TODO translate
MAKE_TRANSLATION(wwWorkM, "workm", "active time", "aktive Zeit", "Actieve tijd", "Aktiv Tid", "czas aktywności", "driftstid", "temps actif", "aktif zaman", "tempo attivo", "aktívny čas", "aktivní čas")
MAKE_TRANSLATION(wwHystOn, "hyston", "hysteresis on temperature", "Einschalttemperaturdifferenz", "Inschakeltemperatuurverschil", "Hysteres PÅ-temperatur", "histereza załączania", "innkoblingstemperaturforskjell", "hystérésis température allumage", "çalışma sıcaklığı farkı", "differenza di temperatura di accensione", "hysterézia teploty", "hystereze zapnutí")
MAKE_TRANSLATION(wwHystOff, "hystoff", "hysteresis off temperature", "Ausschalttemperaturdifferenz", "Uitschakeltemperatuurverschil", "Hysteres AV-temperatur", "histereza wyłączania", "utkoblingstemperaturforskjell", "hystérésis température extinction", "kapatma sıcaklığı farkı", "differenza di temperatura di spegnimento", "teplota hysterézie", "hystereze vypnutí")
MAKE_TRANSLATION(wwProgMode, "progmode", "program", "Programmmodus", "Programma", "Program", "program", "program", "programme", "program", "Programma", "program", "program")
MAKE_TRANSLATION(wwCircProg, "circprog", "circulation program", "Zirkulationsprogramm", "Circulatieprogramma", "Cirkulationsprogram", "program cyrkulacji c.w.u.", "sirkulationsprogram", "programme circulation", "sirkülasyon programı", "programma circolazione", "obehový program", "program oběhu")
MAKE_TRANSLATION(wwMaxTemp, "maxtemp", "maximum temperature", "maximale Temperatur", "maximale temperatuur", "maximal Temperatur", "temperatura maksymalna", "maksimal temperatur", "température max", "maksimum sıcaklık", "temperatura massima", "maximálna teplota", "maximální teplota")
MAKE_TRANSLATION(wwSolarTemp, "solartemp", "solar boiler temperature", "Solarkesseltemperatur", "Zonneboiler temperatuur", "Solpanel Temperatur", "temperatura zasobnika solarnego", "solpaneltemp", "température chaudière solaire", "güneş enerjisi kazan sıcaklığı", "temperatura pannello solare", "teplota solárneho kotla", "teplota solárního kotle")
// mqtt values / commands
MAKE_TRANSLATION(switchtime, "switchtime", "program switchtime", "Programmschaltzeit", "Programma schakeltijd", "Program Bytestid", "program czasowy", "programbyttetid", "heure commutation programme", "program değiştirme süresi", "ora commutazione programmata", "čas prepnutia programu", "čas přepnutí programu")
MAKE_TRANSLATION(switchtime1, "switchtime1", "own1 program switchtime", "Programmschaltzeit 1", "Schakeltijd programma 1", "Program 1 Bytestid", "program przełączania 1", "byttetidprogram 1", "heure de commutation programme 1", "program1 değiştirme süresi", "ora commutazione programma 1", "vlastný 1 program prepnutia", "vlastní program 1 přepnutí")
MAKE_TRANSLATION(switchtime2, "switchtime2", "own2 program switchtime", "Programmschaltzeit 2", "Schakeltijd programma 2", "Program 2 Bytestid", "program przełączania 2", "byttetid program 2", "heure de changement programme 2", "program1 değiştirme süresi", "ora commutazione programma 2", "vlastný 2 program prepnutia", "vlastní program 2 přepnutí")
MAKE_TRANSLATION(wwswitchtime, "switchtimeWW", "program switchtime warm water", "Programmschaltzeit Warmwasser", "Warm water programma schakeltijd", "Varmvattenprogram Bytestid", "program czasowy", "byttetid varmtvannsprogram", "heure commutation programme", "sıcak kullanıom suyu program değiştirme süresi", "Tempo di commutazione del programma", "čas prepnutia programu", "čas přepnutí program TUV")
MAKE_TRANSLATION(wwcircswitchtime, "circswitchtime", "circulation program switchtime", "Zirculationsprogramm Schaltzeit", "Schakeltijd circulatieprogramma", "Cirkulationsprogram Bytestid", "program cyrkulacji", "byttetid sirkulasjonsprogram", "heure commutation programme circulation", "sirkülasyon program değiştirme süresi", "ora commutazione programma circolazione", "čas prepnutia cirkulačného programu", "čas přepnutí programu cirkulace")
MAKE_TRANSLATION(dateTime, "datetime", "date/time", "Datum/Zeit", "Datum/Tijd", "Datum/Tid", "data i godzina", "dato/tid", "date/heure", "zaman/saat", "Data/Ora", "dátum/čas", "datum/čas")
MAKE_TRANSLATION(errorCode, "errorcode", "error code", "Fehlercode", "Foutmeldingscode", "Felkod", "kod błędu", "feikode", "code erreur", "hata kodu", "codice errore", "error kód", "chybový kód")
MAKE_TRANSLATION(ibaMainDisplay, "display", "display", "Anzeige", "Display", "Display", "wyświetlacz", "skjerm", "affichage", "ekran", "Display", "display", "displej")
MAKE_TRANSLATION(ibaLanguage, "language", "language", "Sprache", "Taal", "Sprak", "język", "språk", "langue", "dil", "Lingua", "jazyk", "jazyk")
MAKE_TRANSLATION(ibaClockOffset, "clockoffset", "clock offset", "Uhrkorrektur", "Klokcorrectie", "Tidskorrigering", "korekta zegara", "tidskorrigering", "offset horloge", "saat farkı", "correzione orario", "korekcia času", "posun hodin")
MAKE_TRANSLATION(ibaBuildingType, "building", "building type", "Gebäudetyp", "Type gebouw", "Byggnadstyp", "typ budynku", "bygningstype", "type bâtiment", "bina tipi", "tipo di edificio", "typ budovy", "typ budovy")
MAKE_TRANSLATION(heatingPID, "heatingpid", "heating PID", "Heizungs-PID", "PID verwarming", "Uppvärmning PID", "PID ogrzewania", "oppvarmings PID", "PID chauffage", "PID ısınıyor", "PID-riscaldamento", "PID kúrenia", "PID regulace vytápění")
MAKE_TRANSLATION(ibaCalIntTemperature, "intoffset", "internal temperature offset", "Korrektur interner Temperatur", "Offset interne temperatuur", "Korrigering interntemperatur", "korekta temperatury w pomieszczeniu", "Korrigering interntemperatur", "offset température interne", "iç sıcaklık artışı", "scostamento della temperatura interna", "odchýlka vnútornej teploty", "kompenzace vnitřní teploty")
MAKE_TRANSLATION(ibaMinExtTemperature, "minexttemp", "minimal external temperature", "Min. Außentemperatur", "Min. buitentemperatuur", "Min Extern Temperatur", "minimalna miejscowa temperatura zewnętrzna", "minimal eksterntemperatur", "température extérieure minimale", "en düşük sış sıcaklık", "temperatura esterna minima", "minimálna vonkajšia teplota", "minimální venkovní teplota")
MAKE_TRANSLATION(backlight, "backlight", "key backlight", "Tastenbeleuchtung", "Toetsverlichting", "Bakgrundsbelysning", "podświetlenie klawiatury", "bakgrunnsbelysning", "rétroéclairage touches", "tuş takımı aydınlatması", "retroilluminazione dei tasti", "podsvietenie kláves", "podsvícení kláves")
MAKE_TRANSLATION(damping, "damping", "damping outdoor temperature", "Dämpfung der Außentemperatur", "Demping buitentemperatuur", "Utomhustemperatur dämpning", "tłumienie temperatury zewnętrznej", "demping av utetemperatur", "température extérieure minimale", "dış sıcaklığın sönümlenmesi", "smorzamento della temperatura esterna", "tlmenie vonkajšej teploty", "tlumení venkovní teploty")
MAKE_TRANSLATION(tempsensor1, "inttemp1", "temperature sensor 1", "Temperatursensor 1", "Temperatuursensor 1", "Temperatursensor 1", "czujnik temperatury 1", "temperatursensor 1", "sonde température 1", "sıcaklık sensörü 1", "sensore temperatura 1", "snímač teploty 1", "snímač teploty 1")
MAKE_TRANSLATION(tempsensor2, "inttemp2", "temperature sensor 2", "Temperatursensor 2", "Temperatuursensor 2", "Temperatursensor 2", "czujnik temperatury 2", "temperatursensor 2", "capteur température 2", "sıcaklık sensörü 2", "sensore temperatura 2", "snímač teploty 2", "snímač teploty 2")
MAKE_TRANSLATION(dampedoutdoortemp, "dampedoutdoortemp", "damped outdoor temperature", "Gedämpfte Außentemperatur", "Gedempte buitentemperatuur", "Utomhustemperatur dämpad", "tłumiona temperatura zewnętrzna", "dempet utetemperatur", "température extérieure amortie", "sönümlenmiş dış sıcaklık", "temperatura esterna smorzata", "tlmená vonkajšia teplota", "tlumená venkovní teplota")
MAKE_TRANSLATION(floordrystatus, "floordry", "floor drying", "Estrichtrocknung", "Vloerdroogprogramma", "Golvtorkning", "suszenie jastrychu", "gulvtørkeprogram", "séchage sol", "yerden ısıtma", "asciugatura pavimento", "sušenie podlahy", "sušení podlahy")
MAKE_TRANSLATION(floordrytemp, "floordrytemp", "floor drying temperature", "Estrichtrocknungstemperatur", "Temperatuur vloerdroogprogramma", "Golvtorkning Temperatur", "temperatura suszenia jastrychu", "gulvtørketemperatur", "température séchage sol", "yerden ısıtma sıcaklığı", "Temperatura asciugatura pavimento", "teplota sušenia podlahy", "teplota sušení podlah")
MAKE_TRANSLATION(brightness, "brightness", "screen brightness", "Bildschirmhelligkeit", "Schermhelderheid", "Ljusstyrka", "jasność", "lysstyrke", "luminosité écran", "ekran parlaklığı", "luminosita display", "jas obrazovky", "jas obrazovky")
MAKE_TRANSLATION(autodst, "autodst", "automatic change daylight saving time", "Automatische Sommerzeit Umstellung", "Automatische omschakeling zomer-wintertijd", "Automatisk växling sommar/vinter-tid", "automatycznie przełączaj na czas letni/zimowy", "automatisk skifte av sommer/vinter-tid", "changement automatique heure d'été", "gün ışığından yararlanma saatini otomatik olarak değiştir", "cambio automatico dell'ora legale", "automatická zmena letného času", "automatická změna letního času")
MAKE_TRANSLATION(preheating, "preheating", "preheating in the clock program", "Vorheizen im Zeitprogramm", "Voorverwarming in het klokprogramma", "Förvärmning i tidsprogram", "podgrzewanie w programie czasowym", "forvarming i tidsprogram", "préchauffage dans programme horloge", "saat programında ön ısıtma", "preriscaldamento nel programma orologio", "predohrev v programe hodín", "předehřev v programovaném režimu")
MAKE_TRANSLATION(offtemp, "offtemp", "temperature when mode is off", "Temperatur bei AUS", "Temperatuur bij UIT", "Temperatur Avslagen", "temperatura w trybie \"wył.\"", "temperatur avslått", "température lorsque mode désactivé", "mod kapalı iken sıcaklık", "temperatura quando la modalità è disattivata", "teplota, keď je režim vypnutý", "teplota při vypnutém režimu")
MAKE_TRANSLATION(mixingvalves, "mixingvalves", "mixing valves", "Mischventile", "Mengkleppen", "Blandningsventiler", "zawory mieszające", "blandeventiler", "vannes mélange", "karışım vanaları", "valvole miscela", "zmiešavacie ventily", "směšovací ventily")
MAKE_TRANSLATION(pvEnableWw, "pvenabledhw", "enable raise dhw", "aktiviere WW-Anhebung", "Verhoging WW activeren", "Höj varmvatten med solpanel", "podwyższenie c.w.u. z PV", "aktivere hevet temperatur bereder", "", "sıcak kullanım suyu yükseltmeyi etkinleştir", "abilitare aumento ACS", "povoliť zvýšenie TÚV", "povolit zvýšení TUV") // TODO translate
MAKE_TRANSLATION(pvRaiseHeat, "pvraiseheat", "raise heating with PV", "Anhebung Heizen mit PV", "Verwarmen met PV activeren", "Höj värmen med solpanel", "podwyższenie grzania z PV", "heve varmen med solpanel", "", "ısıtmayı G.E. İle yükselt", "Aumentare il riscaldamento con il solare", "zvýšiť kúrenie s FV", "zvýšení vytápění s FV") // TODO translate
MAKE_TRANSLATION(pvLowerCool, "pvlowercool", "lower cooling with PV", "Absenkung Kühlen mit PV", "Verlagen koeling met PV activeren", "Sänk kylning med solpanel", "obniżenie chłodzenia z PV", "nedre kjøling solpanel", "", "soğutmayı G.E. İle düşür", "Riduzione del raffreddamento con il solare", "nižšie chladenie s PV", "snížení chlazení s FV") // TODO translate
MAKE_TRANSLATION(hasSolar, "solar", "solar", "Solar", "", "Solpaneler", "", "", "", "", "", "solár", "") // TODO translate
// thermostat dhw
MAKE_TRANSLATION(wwMode, "mode", "operating mode", "Betriebsart", "Modus", "Läge", "tryb pracy", "modus", "mode", "mod", "modalità", "režim", "provozní režim")
MAKE_TRANSLATION(wwSetTempLow, "settemplow", "set low temperature", "untere Solltemperatur", "Onderste streeftemperatuur", "Nedre Börvärde", "zadana temperatura obniżona", "nedre settverdi", "réglage température basse", "hedef düşük sıcaklık", "imposta bassa temperatura", "nastaviť nízku teplotu", "nastavit nízkou teplotu")
MAKE_TRANSLATION(wwWhenModeOff, "whenmodeoff", "when thermostat mode off", "bei Thermostatmodus AUS", "Als Thermostaat op UIT", "när Termostatläge är AV", "gdy wyłączono na termostacie", "når modus er av", "lorsque mode thermostat off", "termostat modu kapalı olduğunda", "quando termostato modalita OFF", "keď je režim termostatu vypnutý", "při vypnutém termostatu")
MAKE_TRANSLATION(wwExtra, "extra", "extra", "Extra", "extra", "Extra", "obieg", "ekstra", "extra", "ekstra", "extra", "extra", "extra")
MAKE_TRANSLATION(wwCharge, "charge", "charge", "Laden", "Laden", "Ladda", "grzanie", "lade", "charge", "doldurma", "carica", "nabiť", "nabíjení")
MAKE_TRANSLATION(wwChargeDuration, "chargeduration", "charge duration", "Ladedauer", "Laadtijd", "Laddtid", "czas grzania dodatkowej ciepłej wody", "ladetid", "durée charge", "doldurma süresi", "durata carica", "doba nabíjania", "doba nabíjení")
MAKE_TRANSLATION(wwDisinfect, "disinfect", "disinfection", "Desinfektion", "Desinfectie", "Desinfektion", "dezynfekcja termiczna", "desinfeksjon", "désinfection", "dezenfeksiyon", "disinfezione", "dezinfekcia", "dezinfekce")
MAKE_TRANSLATION(wwDisinfectDay, "disinfectday", "disinfection day", "Desinfektionstag", "Desinfectiedag", "Desinfektionsdag", "dzień dezynfekcji termicznej", "desinfeksjonsdag", "jour désinfection", "dezenfeksiyon günü", "giorno disinfezione", "deň dezinfekcie", "den dezinfekce")
MAKE_TRANSLATION(wwDisinfectHour, "disinfecthour", "disinfection hour", "Desinfektionsstunde", "Desinfectieuur", "Desinfektionstimme", "godzina dezynfekcji termicznej", "desinfeksjonstime", "heure désinfection", "dezenfeksiyon saati", "ora disinfezione", "hodina dezinfekcie", "hodina dezinfekce")
MAKE_TRANSLATION(wwDisinfectTime, "disinfecttime", "disinfection time", "Desinfektionszeit", "Desinfectietijd", "Desinfektionstid", "maksymalny czas trwania dezynfekcji termicznej", "desinfeksjonstid", "durée désinfection", "dezenfeksiyon zamanı", "orario disinfezione", "čas na dezinfekciu", "čas dezinfekce")
MAKE_TRANSLATION(wwDailyHeating, "dailyheating", "daily heating", "täglich Heizen", "Dagelijks opwarmen", "Daglig Uppvärmning", "codzienne nagrzewanie", "daglig oppvarming", "chauffage quotidien", "günlük ısıtma", "riscaldamento giornaliero", "denné kúrenie", "denní ohřev")
MAKE_TRANSLATION(wwDailyHeatTime, "dailyheattime", "daily heating time", "tägliche Heizzeit", "Tijd dagelijkse opwarming", "Daglig Uppvärmningstid", "czas trwania codziennego nagrzewania", "daglig oppvarmingstid", "heure chauffage quotidien", "günlük ısıtma süresi", "orario riscaldamento giornaliero", "denný čas vykurovania", "čas denního ohřevu")
// thermostat hc
MAKE_TRANSLATION(selRoomTemp, "seltemp", "selected room temperature", "gewählte Raumtemperatur", "Streeftemperatuur kamer", "Vald Rumstemperatur", "zadana temperatura w pomieszczeniu", "valgt rumstemperatur", "température ambiante sélectionnée", "seçili oda sıcaklığı", "temperatura ambiente selezionata", "zvolená izbová teplota", "zvolená teplota místnosti")
MAKE_TRANSLATION(roomTemp, "currtemp", "current room temperature", "aktuelle Raumtemperatur", "Huidige kamertemperatuur", "Aktuell Rumstemperatur", "temperatura w pomieszczeniu", "gjeldende romstemperatur", "température ambiante actuelle", "güncel oda sıcaklığı", "temperatura ambiente attuale", "aktuálna izbová teplota", "aktuální teplota místnosti")
MAKE_TRANSLATION(mode, "mode", "operating mode", "Betriebsart", "Modus", "Läge", "sposób sterowania", "modus", "mode", "mod", "modalità", "režim", "provozní režim")
MAKE_TRANSLATION(modetype, "modetype", "mode type", "Modustyp", "Type modus", "Typ av läge", "aktualny tryb pracy", "modusrype", "type mode", "mod tipi", "tipo di modalita", "typ režimu", "typ režimu")
MAKE_TRANSLATION(fastheatup, "fastheatup", "fast heatup", "schnelles Aufheizen", "Snel opwarmen", "Snabb Uppvärmning", "szybkie nagrzewanie", "rask oppvarming", "chauffage rapide", "hızlı ısıtma", "riscaldamento rapido", "rýchle zahriatie", "rychlé předehřátí")
MAKE_TRANSLATION(heatup, "heatup", "heatup", "Aufheizen", "opwarmen", "Uppvärmning", "nagrzewanie", "oppvarming", "chauffage", "hızlı", "riscaldamento", "rýchle zahriatie", "předehřev")
MAKE_TRANSLATION(daytemp, "daytemp", "day temperature", "Tagestemperatur", "temperatuur dag", "Dagstemperatur", "temperatura w dzień", "dagtemperatur", "température jour", "gündüz sıcaklığı", "temperatura giornaliera", "denná teplota", "denní teplota")
MAKE_TRANSLATION(daylowtemp, "daytemp2", "day temperature T2", "Tagestemperatur T2", "Temperatuur dag T2", "Dagstemperatur T2", "temperatura w dzień T2", "dagtemperatur T2", "température jour T2", "gündüz sıcaklığı T2", "temperatura giornaliera T2", "denná teplota T2", "denní teplota T2")
MAKE_TRANSLATION(daymidtemp, "daytemp3", "day temperature T3", "Tagestemperatur T3", "Temperatuur dag T3", "Dagstemperatur T3", "temperatura w dzień T3", "dagtemperatur T3", "température jour T3", "gündüz sıcaklığı T3", "temperatura giornaliera T3", "denná teplota T3", "denní teplota T3")
MAKE_TRANSLATION(dayhightemp, "daytemp4", "day temperature T4", "Tagestemperatur T4", "Temperatuur dag T4", "Dagstemperatur T4", "temperatura w dzień T4", "dagtemperatur T4", "température jour T4", "gündüz sıcaklığı T4", "temperatura giornaliera T4", "denná teplota T4", "denní teplota T4")
MAKE_TRANSLATION(heattemp, "heattemp", "heat temperature", "Heiztemperatur", "Temperatuur verwarming", "Temperatur Uppvärmning", "temperatura ogrzewania", "oppvarmingstemperatur", "température chauffage", "ısıtma sıcaklığı", "temperatura riscaldamento", "teplota ohrevu", "teplota topení")
MAKE_TRANSLATION(nighttemp, "nighttemp", "night temperature", "Nachttemperatur", "Nachttemperatuur", "Nattemperatur", "temperatura w nocy", "nattemperatur", "température de nuit", "gece sıcaklığı", "temperatura notturna", "nočná teplota", "noční teplota")
MAKE_TRANSLATION(nighttemp2, "nighttemp", "night temperature T1", "Nachttemperatur T1", "Nachttemperatuur T1", "Nattemperatur T1", "temperatura w nocy T1", "nattemperatur T1", "température nuit T1", "gece sıcaklığı T1", "temperatura notturna T1", "nočná teplota T1", "noční teplota T1")
MAKE_TRANSLATION(ecotemp, "ecotemp", "eco temperature", "eco Temperatur", "Temperatuur eco", "Eko-temperatur", "temperatura w trybie eko", "øko temperatur", "température éco", "eko sıcaklık", "Temperatura eco", "eko teplota", "eko teplota")
MAKE_TRANSLATION(manualtemp, "manualtemp", "manual temperature", "manuelle Temperatur", "Temperatuur handmatig", "Temperatur Manuell", "temperatura ustawiona ręcznie", "manuell temperatur", "température manuelle", "manuel sıcaklık", "temperatura manuale", "manuálna teplota", "manuální teplota")
MAKE_TRANSLATION(tempautotemp, "tempautotemp", "temporary set temperature automode", "temporäre Solltemperatur Automatikmodus", "Streeftemperatuur automodus tijdelijk", "Temporär Aktivering av Auto-läge", "zadana temperatura w pomieszczeniu w trybie \"auto\" (tymczasowa)", "temporær valgt temp i automodus", "température temporaire mode automatique", "geçici ayarlı sıcaklık otomatik mod", "impostare temporaneamente temperatura automatica", "automatický režim dočasnej nastavenej teploty", "dočasné nastavení teploty v automatickém režimu")
MAKE_TRANSLATION(remoteseltemp, "remoteseltemp", "temporary set temperature from remote", "temporäre Solltemperatur Remote", "Temperatuur van afstandsbedieding", "Temperatur från fjärruppkoppling", "zadana zdalnie temperatura a pomieszczeniu (tymczasowa)", "temporær valgt temp fra fjernbetjening", "température temporaire depuis télécommande", "geçici ayarlı sıcaklık uzaktan", "Temperatura temporanea da remoto", "dočasne nastavená teplota z diaľkového ovládania", "dočasné nastavení teploty z dálkového ovladače")
MAKE_TRANSLATION(comforttemp, "comforttemp", "comfort temperature", "Komforttemperatur", "Comforttemperatuur", "Komforttemperatur", "temperatura w trybie komfort", "komforttemperatur", "température confort", "konfor sıcaklığı", "temperatura comfort", "komfortná teplota", "komfortní teplota")
MAKE_TRANSLATION(summertemp, "summertemp", "summer temperature", "Sommertemperatur", "Zomertemperatuur", "Sommartemperatur", "temperatura przełączania lato/zima", "Sommertemperatur", "température été", "yaz sıcaklığı", "temperatura estiva", "letná teplota", "letní teplota")
MAKE_TRANSLATION(designtemp, "designtemp", "design temperature", "Auslegungstemperatur", "Ontwerptemperatuur", "Design-temperatur", "temperatura projektowa", "designtemperatur", "température conception", "özel sıcaklık", "temperatura predefinita", "návrhová teplota", "dimenzovaná teplota")
MAKE_TRANSLATION(offsettemp, "offsettemp", "offset temperature", "Temperaturanhebung", "Temperatuur offset", "Temperaturkorrigering", "korekta temperatury", "temperaturkorrigering", "température offset", "artış sıcaklığı", "aumento della temperatura", "offsetová teplota", "offset teploty")
MAKE_TRANSLATION(minflowtemp, "minflowtemp", "min flow temperature", "min. Vorlauftemperatur", "Minimale aanvoertemperatuur", "Min. Flödestemperatur", "minimalna temperatura zasilania", "min. turtemperatur", "température min. flux", "minimun akış sıcaklığı", "temperatura minima di mandata", "min. výstupná teplota", "vytápění minimální teplota")
MAKE_TRANSLATION(maxflowtemp, "maxflowtemp", "max flow temperature", "max. Vorlauftemperatur", "Maximale aanvoertemperatuur", "Max. Flödestemperatur", "maksymalna temperatura zasilania", "maks turtemperatur", "température max flux", "maksimum akış sıcaklığı", "temperatura massima di mandata", "maximálna teplota prívodu", "vytápění maximální teplota")
MAKE_TRANSLATION(roominfluence, "roominfluence", "room influence", "Raumeinfluss", "Ruimteinvloed", "Rumspåverkan", "wpływ pomieszczenia", "rominnflytelse", "influence pièce", "oda etkisi", "influenza della camera", "vplyv miestnosti", "vliv prostoru")
MAKE_TRANSLATION(roominfl_factor, "roominflfactor", "room influence factor", "Raumeinflussfaktor", "Factor ruimteinvloed", "Rumspåverkansfaktor", "współczynnik wpływu pomieszczenia", "rominnflytelsesfaktor", "facteur d'influence pièce", "oda etkisi faktörü", "fattore influenza camera", "faktor vplyvu miestnosti", "ofset teploty prostoru")
MAKE_TRANSLATION(curroominfl, "curroominfl", "current room influence", "aktueller Raumeinfluss", "Huidige ruimteinvloed", "Aktuell Rumspåverkan", "aktualny wpływ pomieszczenia", "gjeldende rominnflytelse", "influence actuelle pièce", "güncel oda etkisi", "fattore corrente influenza camera", "aktuálny vplyv miestnosti", "aktuální vliv místnosti")
MAKE_TRANSLATION(nofrosttemp, "nofrosttemp", "nofrost temperature", "Frostschutztemperatur", "Temperatuur vorstbeveiliging", "Temperatur Frostskydd", "temperatura ochrony przed zamarzaniem", "frostbeskyttelsestemperatur", "température protection gel", "donma koruması sıcaklığı", "temperatura protezione antigelo", "nofrost teplota", "teplota proti zamrznutí")
MAKE_TRANSLATION(targetflowtemp, "targetflowtemp", "target flow temperature", "berechnete Vorlauftemperatur", "Berekende aanvoertemperatuur", "Målvärde Flödestemperatur", "zadana temperatura zasilania", "målverdi turtemperatur", "température cible flux", "hedef akış sıcaklığı", "temperatura di mandata calcolata", "cieľová teplota prívodu", "cílová teplota přívodu")
MAKE_TRANSLATION(heatingtype, "heatingtype", "heating type", "Heizungstyp", "Verwarmingstype", "Värmesystem", "system grzewczy", "varmesystem", "type chauffage", "ısıtma tipi", "tipo riscaldamento", "typ vykurovania", "typ vytápění")
MAKE_TRANSLATION(summersetmode, "summersetmode", "set summer mode", "Einstellung Sommerbetrieb", "Instelling zomerbedrijf", "Aktivera Sommarläge", "tryb lato/zima", "aktiver sommermodus", "activer mode été", "yaz modu ayarı", "Impostazione della modalità estiva", "nastaviť letný režim", "nastavit letní režim")
MAKE_TRANSLATION(hpoperatingmode, "hpoperatingmode", "heatpump operating mode", "WP Betriebsmodus", "Bedrijfsmodus warmtepomp", "Värmepump Driftläge", "tryb pracy pompy ciepła", "driftsmodus varmepumpe", "mode fonctionnement pompe à chaleur", "ısı pompası çalışma modu", "Modalità di funzionamento della pompa di calore", "prevádzkový režim tepelného čerpadla", "provozní režim tepelného čerpadla")
MAKE_TRANSLATION(hpoperatingstate, "hpoperatingstate", "heatpump operating state", "WP Betriebszustand", "Huidige modus warmtepomp", "Värmepump drifttillstånd", "aktualny tryb pracy pompy ciepła", "driftstatus varmepumpe", "état fonctionnement pompe à chaleur", "ısı pompası çalışma durumu", "stato funzionamento pompa di calore", "prevádzkový stav tepelného čerpadla", "stav provozu tepelného čerpadla")
MAKE_TRANSLATION(controlmode, "controlmode", "control mode", "Steuermodus", "Comtrolemodus", "Kontrolläge", "tryb sterowania", "kontrollmodus", "mode régulation", "kontrol modu", "modalità di controllo", "kontrolný režim", "způsob regulace")
MAKE_TRANSLATION(control, "control", "control device", "Fernsteuerung", "Afstandsbedieding", "Kontrollenhet", "sterownik", "kontrollenhet", "dispositif régulation", "kontrol cihazı", "dispositivo di comando", "ovládacie zariadenie", "řídící zařízení")
MAKE_TRANSLATION(roomsensor, "roomsensor", "room sensor", "Raumsensor", "Ruimtesensor", "Rumssensor", "czujnik temperatury pomieszczenia", "romsensor", "capteur pièce", "oda sensörü", "sensore ambiente", "izbový snímač", "čidlo místnosti")
MAKE_TRANSLATION(program, "program", "program", "Programm", "Programma", "Program", "program", "program", "programme", "program", "Programma", "program", "program")
MAKE_TRANSLATION(pause, "pause", "pause time", "Pausenzeit", "Pausetijd", "Paustid", "czas przerwy", "pausetid", "temps de pause", "süreyi durdur", "pausa", "prestávka", "doba pauzy")
MAKE_TRANSLATION(party, "party", "party time", "Partyzeit", "Partytijd", "Partytid", "czas przyjęcia", "partytid", "temps de fête", "parti zamanı", "festivo", "čas na párty", "doba party")
MAKE_TRANSLATION(holidaytemp, "holidaytemp", "holiday temperature", "Urlaubstemperatur", "Vakantietemperatuur", "Helgtemperatur", "temperatura w trybie urlopowym", "ferietemperatur", "température vacances", "tatil sıcaklığı", "temperatura festiva", "prázdninová teplota", "teplota během dovolené")
MAKE_TRANSLATION(summermode, "summermode", "summer mode", "Sommerbetrieb", "Zomerbedrijf", "Sommarläge", "aktualny tryb lato/zima", "sommermodus", "mode été", "yaz modu", "funzionamento estivo", "letný režim", "letní režim")
MAKE_TRANSLATION(holidaymode, "holidaymode", "holiday mode", "Urlaubsbetrieb", "Vakantiebedrijf", "Helgläge", "tryb urlopowy", "feriemodus", "mode vacances", "tatil modu", "modalita vacanze", "dovolenkový režim", "režim dovolené")
MAKE_TRANSLATION(flowtempoffset, "flowtempoffset", "flow temperature offset for mixer", "Anhebung Vorlauftemperatur Mischer", "Mixer aanvoertemperatuur offset", "Temperaturkorrigering Flödestemp. Blandningsventil", "korekta temperatury przepływu dla miksera", "temperaturkorrigering av blandingsventil", "décalage température de bascule pour mélangeur", "karıştırıcı için akış sıcaklığı farkı", "aumento della temperatura di ritorno", "Posun teploty prívodu pre zmiešavač", "offset teploty přívodu pro směšovač")
MAKE_TRANSLATION(reducemode, "reducemode", "reduce mode", "Absenkmodus", "Gereduceerde modus", "Reducerat Läge", "tryb zredukowany/obniżony", "redusert modus", "mode réduction", "düşürme modu", "modalità assente", "znížený režim", "redukční režim")
MAKE_TRANSLATION(noreducetemp, "noreducetemp", "no reduce below temperature", "Durchheizen unter", "Reduceermodus onderbreken onder", "Inaktivera reducering under", "bez redukcji poniżej temperatury", "inaktiver redusert nedre temp", "pas de réduction en dessous température", "bu sıcaklığın altına düşürme", "non ridurre temperatura sotto", "žiadne zníženie teploty pod teplotu", "nesnižovat pod teplotu")
MAKE_TRANSLATION(reducetemp, "reducetemp", "off/reduce switch temperature", "Absenkmodus unter", "Onderste afschakeltemperatuur", "Avslag/Reducera under", "tryb zredukowany poniżej temperatury", "nedre avstengningstemperatur", "arrêt/réduction température bascule", "sıcaklık kapama/düşürme modu", "interruttore riduzione temperatura", "vypnúť/znížiť teplotu spínača", "teplota přepnutí na redukci/vypnutí")
MAKE_TRANSLATION(vacreducetemp, "vacreducetemp", "vacations off/reduce switch temperature", "Urlaub Absenkmodus unter", "Vakantiemodus onderste afschakeltemperatuur", "Helg Avslag/Reducering under", "tryb urlopowy poniżej temperatury", "feriemodus nedre utkoblingstemperatur", "vacances arrêt/réduction température bascule", "tatil sıcaklık kapama/düşürme modu", "interruttore riduzione temperatura vacanze", "dovolenky vypnúť/znížiť teplotu spínača", "teplota přepnutí na redukci/vypnutí během dovolené")
MAKE_TRANSLATION(vacreducemode, "vacreducemode", "vacations reduce mode", "Urlaub Absenkmodus", "Vakantie afschakelmodus", "Helg reduceringsläge", "redukcja w trakcie urlopu", "ferieavstengningsmodus", "mode réduction vacances", "tail düşürme modu", "modalita riduzione vacanze", "režim zníženia dovoleniek", "redukční režim během dovolené")
MAKE_TRANSLATION(nofrostmode, "nofrostmode", "nofrost mode", "Frostschutzmodus", "Vorstbeveiligingsmodus", "Frostskyddsläge", "temperatura wiodąca dla ochrony przed zamarzaniem", "frostbeskyttelsesmodus", "mode protection gel", "donma koruması modu", "Modalità protezione antigelo", "nofrost režim", "režim proti zamrznutí")
MAKE_TRANSLATION(remotetemp, "remotetemp", "room temperature from remote", "Raumtemperatur Remote", "Ruimtetemperatuur van afstandsbediening", "Rumstemperatur från fjärr", "temperatura w pomieszczeniu (z termostatu)", "romstemperatur fra fjernbetjening", "température pièce depuis télécommande", "uzaktan oda sıcaklığı", "temperatura ambiente da remoto", "izbová teplota z diaľkového ovládania", "teplota místnosti z dálkového ovladače")
MAKE_TRANSLATION(remotehum, "remotehum", "room humidity from remote", "Raumfeuchte Remote", "", "Rumsluftfuktighet från fjärr", "wilgotność w pomieszczeniu (z termostatu)", "", "", "uzaktan kumandadan oda nemi", "", "Vlhkosť v miestnosti z diaľkového ovládania", "vlhkost místnosti z dálkového ovladače") // TODO translate
MAKE_TRANSLATION(wwHolidays, "holidays", "holiday dates", "Feiertage", "Feestdagen", "Helgdagar", "dni świąteczne", "feriedager varmtvann", "dates vacances", "tatil günleri", "feste pubbliche", "sviatočné termíny", "data pro dovolenou")
MAKE_TRANSLATION(wwVacations, "vacations", "vacation dates", "Urlaubstage", "Vakantiedagen", "Semesterdatum Varmvatten", "dni urlopowe", "ferie dato varmtvann", "dates vacances", "izin günleri", "date vacanze", "termíny dovolenky", "data pro prázdniny")
MAKE_TRANSLATION(holidays, "holidays", "holiday dates", "Feiertage", "Feestdagen", "Helgdatum", "święta", "helligdager", "dates vacances", "tatil günleri", "date feste pubbliche", "sviatočné termíny", "data pro dovolenou")
MAKE_TRANSLATION(vacations, "vacations", "vacation dates", "Urlaubstage", "Vakantiedagen", "Semesterdatum", "urlop", "feriedager", "dates vacances", "izin günleri", "date vacanze", "termíny dovolenky", "data pro prázdniny")
MAKE_TRANSLATION(wwprio, "dhwprio", "dhw priority", "WW-Vorrang", "Prioriteit warm water", "Prioritera Varmvatten", "priorytet dla c.w.u.", "prioroter varmtvann", "priorité ecs", "sıcak kullanım suyu önceliği", "priorita acqua calda", "Priorita TÚV", "přednost ohřevu TUV")
MAKE_TRANSLATION(nofrostmode1, "nofrostmode1", "nofrost mode", "Frostschutz", "Vorstbeveiligingsmodus", "Frostskyddsläge", "ochrona przed zamarzaniem", "frostbeskyttelse", "mode protection gel", "donma koruması modu 1", "modalita protezione antigelo", "nofrost režim", "režim proti zamrznutí")
MAKE_TRANSLATION(reducehours, "reducehours", "duration for nighttemp", "Dauer Nachttemp.", "Duur nachtverlaging", "Timmar Nattsänkning", "czas trwania trybu nocnego", "timer nattsenkning", "durée température nuit", "gece sıcaklığı süresi", "durata temperatura notturna", "trvanie nočnej teploty", "délka trvání noční teploty")
MAKE_TRANSLATION(reduceminutes, "reduceminutes", "remaining time for nightmode", "Restzeit Nachttemp.", "Resterende tijd nachtverlaging", "Återstående Tid Nattläge", "czas do końca trybu nocnego", "gjenværende tid i nattstilling", "temps restant mode nuit", "gece modu için kalan süre", "temperatura notturna residua", "zostávajúci čas pre nočný režim", "zbývající čas nočního režimu")
MAKE_TRANSLATION(switchonoptimization, "switchonoptimization", "switch-on optimization", "Einschaltoptimierung", "Inschakeloptimalisering", "Växlingsoptimering", "optymalizacja załączania", "slå på optimalisering", "optimisation mise en marche", "optimizasyonu aç", "ottimizzazione all'accensione", "optimalizácia pri zapnutí", "optim. zapínání čas.prog.")
MAKE_TRANSLATION(vacations1, "vacations1", "vacation dates 1", "Urlaubstage 1", "Vakantiedagen 1", "Semesterdatum 1", "urlop 1", "feriedager 1", "dates vacances 1", "izin günleri 1", "date vacanze 1", "termíny dovolenky 1", "data prázdnin 1")
MAKE_TRANSLATION(vacations2, "vacations2", "vacation dates 2", "Urlaubstage 2", "Vakantiedagen 2", "Semesterdatum 2", "urlop 2", "feriedager 2", "dates vacances 2", "izin günleri 2", "date vacanze 2", "termíny dovolenky 2", "data prázdnin 2")
MAKE_TRANSLATION(vacations3, "vacations3", "vacation dates 3", "Urlaubstage 3", "Vakantiedagen 3", "Semesterdatum 3", "urlop 3", "feriedager 3", "dates vacances 3", "izin günleri 3", "date vacanze 3", "termíny dovolenky 3", "data prázdnin 3")
MAKE_TRANSLATION(vacations4, "vacations4", "vacation dates 4", "Urlaubstage 4", "Vakantiedagen 4", "Semesterdatum 4", "urlop 4", "feriedager 4", "dates vacances 4", "izin günleri 4", "date vacanze 4", "termíny dovolenky 4", "data prázdnin 4")
MAKE_TRANSLATION(vacations5, "vacations5", "vacation dates 5", "Urlaubstage 5", "Vakantiedagen 5", "Semesterdatum 5", "urlop 5", "feriedager 5", "dates vacances 5", "izin günleri 5", "date vacanze 5", "termíny dovolenky 5", "data prázdnin 5")
MAKE_TRANSLATION(vacations6, "vacations6", "vacation dates 6", "Urlaubstage 6", "Vakantiedagen 6", "Semesterdatum 6", "urlop 6", "feriedager 6", "dates vacances 6", "izin günleri 6", "date vacanze 6", "termíny dovolenky 6", "data prázdnin 6")
MAKE_TRANSLATION(vacations7, "vacations7", "vacation dates 7", "Urlaubstage 7", "Vakantiedagen 7", "Semesterdatum 7", "urlop 7", "feriedager 7", "dates vacances 7", "izin günleri 7", "date vacanze 7", "termíny dovolenky 7", "data prázdnin 7")
MAKE_TRANSLATION(vacations8, "vacations8", "vacation dates 8", "Urlaubstage 8", "Vakantiedagen 8", "Semesterdatum 8", "urlop 8", "feriedager 8", "dates vacances 8", "izin günleri 8", "date vacanze 8", "termíny dovolenky 8", "data prázdnin 8")
MAKE_TRANSLATION(absent, "absent", "absent", "Abwesend", "", "Frånvarande", "", "", "", "", "", "chýnajúci", "") // TODO translate
MAKE_TRANSLATION(redthreshold, "redthreshold", "reduction threshold", "Absenkschwelle", "", "Tröskel för sänkning", "", "", "", "", "", "zníženie tresholdu", "") // TODO translate
MAKE_TRANSLATION(hpmode, "hpmode", "HP Mode", "WP-Modus", "Modus warmtepomp", "Värmepumpsläge", "tryb pracy pompy ciepła", "", "", "yüksek güç modu", "Modalità Termopompa", "Režim TČ", "režim tepelného čerpadla") // TODO translate
MAKE_TRANSLATION(dewoffset, "dewoffset", "dew point offset", "Taupunktdifferenz", "Offset dauwpunt", "Daggpunktsförskjutning", "przesunięcie punktu rosy", "", "", "çiğ noktası göreli", "differenza del punto di rugiada", "posun rosného bodu", "offset rosného bodu") // TODO translate
MAKE_TRANSLATION(roomtempdiff, "roomtempdiff", "room temp difference", "Raumtemperaturdifferenz", "Verschiltemperatuur kamertemp", "Rumstemperaturskillnad", "różnica temp. pomieszczenia", "", "", "oda sıcaklığı farkı", "differenza temperatura ambiente", "rozdiel izbovej teploty", "rozdíl teploty místnosti") // TODO translate
MAKE_TRANSLATION(hpminflowtemp, "hpminflowtemp", "HP min. flow temp.", "WP minimale Vorlauftemperatur", "Minimale aanvoertemperatuur WP", "VP min flödestemperatur", "pompa ciepła, min. temperatura przepływu", "", "yüksek güç minimum akış sıcaklığı", "temperatura minima di mandata", "VT min. teplota prietoku.", "minimální teplota přívodu tepelného čerpadla") // TODO translate
MAKE_TRANSLATION(hpcooling, "hpcooling", "hp cooling", "WP Kühlen", "WP koelbedrijf", "VP Kyla", "pompa ciepła, chłodzenie", "vp kjøling", "hp soğuyor", "raffreddamento pompa calore", "chladenie TČ", "chlazení tepelného čerpadla")
MAKE_TRANSLATION(coolstart, "coolstart", "cooling starttemp", "Kühlbetrieb ab", "", "Kyla starttemperatur", "", "", "", "", "", "teplota spustenia chladenia", "teplota spuštění chlazení") // TODO translate
MAKE_TRANSLATION(coolondelay, "coolondelay", "cooling on delay", "Einschaltverzögerung Kühlen", "", "Inkopplingsfördröjning Kyla", "", "", "", "", "", "oneskorenie zapnutie chladenia", "zpoždění zapnutí chlazení") // TODO translate
MAKE_TRANSLATION(cooloffdelay, "cooloffdelay", "cooling off delay", "Ausschaltverzögerung Kühlen", "", "Frånkopplingsfördröjning Kyla", "", "", "", "", "", "oneskorenie vypnutie chladenia", "zpoždění vypnutí chlazení") // TODO translate
MAKE_TRANSLATION(switchProgMode, "switchprogmode", "switch program mode", "Schaltprogrammmodus", "", "Byt programläge", "", "", "", "", "", "prepnúť režim programu", "přepínací režim programu") // TODO translate
// heatpump and RC100H
MAKE_TRANSLATION(airHumidity, "airhumidity", "relative air humidity", "relative Luftfeuchte", "Relatieve luchtvochtigheid", "Relativ Luftfuktighet", "wilgotność względna w pomieszczeniu", "luftfuktighet", "humidité relative air", "havadaki bağıl nem", "umidità relativa aria", "relatívna vlhkosť vzduchu", "relativní vlhkost vzduchu")
MAKE_TRANSLATION(dewTemperature, "dewtemperature", "dew point temperature", "Taupunkttemperatur", "Dauwpunttemperatuur", "Daggpunkt", "punkt rosy w pomieszczeniu", "duggtemperatur", "température point rosée", "çiğ noktası sıcaklığı", "temperatura del punto di rugiada", "teplota rosného bodu", "teplota rosného bodu")
MAKE_TRANSLATION(battery, "battery", "battery", "Batterie", "", "Batteri", "bateria", "", "", "", "", "batéria", "baterie") // TODO translate
// mixer
MAKE_TRANSLATION(flowSetTemp, "flowsettemp", "setpoint flow temperature", "Sollwert Vorlauftemperatur", "Streefwaarde aanvoertemperatuur", "Vald flödestemperatur", "zadana temperatura zasilania", "valgt turtemperatur", "consigne température flux", "akış sıcaklığı ayarı", "Setpoint temperatura di mandata", "požadovaná hodnota výstupnej teploty", "cílová teplota vstupní vody")
MAKE_TRANSLATION(flowTempHc, "flowtemphc", "flow temperature (TC1)", "Vorlauftemperatur HK (TC1)", "Aanvoertemperatuut circuit (TC1)", "Flödestemperatur (TC1)", "temperatura zasilania (TC1)", "turtemperatur (TC1)", "température flux (TC1)", "akış sıcaklığı (TC1)", "temperatura di mandata (TC1)", "teplota prívodu (TC1)", "teplota přívodu (TC1)")
MAKE_TRANSLATION(pumpStatus, "pumpstatus", "pump status (PC1)", "Pumpenstatus HK (PC1)", "pompstatus circuit (PC1)", "Pumpstatus (PC1)", "status pompy (PC1)", "pumpestatus (PC1)", "état pompe (PC1)", "pompa durumu (PC1)", "stato pompa (PC1)", "stav čerpadla (PC1)", "stav čerpadla (PC1)")
MAKE_TRANSLATION(mixerStatus, "valvestatus", "mixing valve actuator (VC1)", "Mischerventilposition (VC1)", "positie mixerklep (VC1)", "Shuntventil Status (VC1)", "siłownik zaworu mieszającego (VC1)", "shuntventil status (VC1)", "actionnement vanne mélangeur (VC1)", "karışım vanası aktüatörü (VC1)", "posizione valvola miscela (VC1)", "pohon zmiešavacieho ventilu (VC1)", "pohon směšovacího ventilu (VC1)")
MAKE_TRANSLATION(flowTempVf, "flowtempvf", "flow temperature in header (T0/Vf)", "Vorlauftemperatur am Verteiler (T0/Vf)", "aanvoertemperatuur verdeler (T0/Vf)", "Flödestemperatur Fördelare (T0/Vf)", "temperatura zasilania na rozdzielaczu (T0/Vf)", "turtemperatur ved fordeleren (T0/Vf)", "température départ collecteur (T0/Vf)", "başlıkta akış sıcaklığı", "Temperatura di mandata al distributore (T0/Vf)", "teplota prívodu v zberači (T0/Vf)", "teplota přívodu v hlavici (T0/Vf)")
MAKE_TRANSLATION(mixerSetTime, "valvesettime", "time to set valve", "Zeit zum einstellen des Ventils", "Inschakeltijd mengklep", "Inställningstid Shuntventil", "czas na ustawienie zaworu", "instillningstid ventil", "délai activation vanne", "vana ayar zamanı", "ritardo attivazione valvola", "čas na nastavenie ventilu", "čas pro nastavení ventilu")
// mixer pool
MAKE_TRANSLATION(poolSetTemp, "poolsettemp", "pool set temperature", "Sollwert Pooltemperatur", "Streeftemperatuur zwembad", "Pool Temperatur Börvärde", "zadana temperatura basenu", "valgt temp basseng", "température consigne piscine", "hedef havuz sıcaklığı", "temperatura nominale piscina", "nastavená teplota bazéna", "cílová teplota bazénu")
MAKE_TRANSLATION(poolTemp, "pooltemp", "pool temperature", "Pooltemperatur", "Zwembadtemperatuur", "Pooltemperatur", "temperatura basenu", "bassengtemperatur", "température piscine", "havuz sıcaklığı", "temperatura piscina", "teplota bazéna", "teplota bazénu")
MAKE_TRANSLATION(poolShuntStatus, "poolshuntstatus", "pool shunt status opening/closing", "Poolventil öffnen/schließen", "Zwembadklep status openen/sluiten", "Pool Shunt-status öppen/stängd", "status bocznika basenu", "bassengshunt-status åpen/stengt", "état shunt piscine ouvert/fermé", "havuz şant durumu açılıyor/kapanıyor", "aprire/chiudere valvola regolazione piscina", "stav bazénového bočníka otváranie/zatváranie", "stav šoupátka bazénu (otevírání/zavírání)")
MAKE_TRANSLATION(poolShunt, "poolshunt", "pool shunt open/close (0% = pool / 100% = heat)", "Poolventil Öffnung", "Mengklep zwembad stand", "Pool Shunt Öppen/Stängd", "bocznik basenu (0% = basen / 100% = grzanie)", "bassengshunt åpen/stengt (0% = basseng / 100% = varme)", "ouverture/fermeture shunt piscine (0% = piscine / 100% = chaleur).", "havuz şant açık/kapalı (0% = havuz / 100% = ısıtma)", "valvola regolazione piscina (0% = piscina / 100% = caldo)", "pohyb bazéna otvoriť/zatvoriť (0 % = bazén / 100 % = teplo)", "šoupátko bazénu (0% = bazén / 100% = vytápění)")
MAKE_TRANSLATION(hydrTemp, "hydrTemp", "hydraulic header temperature", "Verteilertemperatur", "Temperatuur open verdeler", "Fördelartemperatur", "temperatura kolektora hydraulicznego", "Fordelertemperatur", "température collecteur hydraulique", "hidrolik başlık sıcaklığı", "temperatura del collettore", "teplota hydraulickej hlavice", "teplota hydraulické hlavice")
// solar
MAKE_TRANSLATION(cylMiddleTemp, "cylmiddletemp", "cylinder middle temperature (TS3)", "Speichertemperatur Mitte (TS3)", "Zonneboilertemperatuur midden (TS3)", "Cylindertemperatur Mitten (TS3)", "temperatura w środku zasobnika (TS3)", "beredertemperatur i midten (TS3)", "température moyenne cylindre (TS3)", "orta depolama sıcaklığı (TS3)", "temperatura di conservazione media accumulo (TS3)", "stredná teplota valca (TS3)", "teplota středu válce (TS3)")
MAKE_TRANSLATION(retHeatAssist, "retheatassist", "return temperature heat assistance (TS4)", "Anhebung Rücklauftemp. (TS4)", "Retourtemperatuur verwarmingsassistentie (TS4)", "Returtemperatur värmestöd (TS4)", "temperatura powrotu wspomagania grzania (TS4)", "returtemperatur varmestøtte (TS4)", "température retour de assistance thermique (TS4)", "geri dönüş sıcaklığı artışı (TS4)", "temperatura ritorno scambiatore (TS4)", "pomoc pri teplote spiatočky (TS4)", "teplota zpátečky pomocného topení (TS4)")
MAKE_TRANSLATION(ts8, "ts8", "return temperature heat assistance (TS8)", "Anhebung Rücklauftemp. (TS8)", "Retourtemperatuur verwarmingsassistentie (TS8)", "Returtemperatur värmestöd (TS8)", "temperatura powrotu wspomagania grzania (TS8)", "returtemperatur varmestøtte (TS8)", "température retour de assistance thermique (TS8)", "geri dönüş sıcaklığı artışı (TS8)", "temperatura ritorno scambiatore (TS8)", "pomoc pri teplote spiatočky (TS8)", "teplota zpátečky pomocného topení (TS8)")
MAKE_TRANSLATION(m1Valve, "heatassistvalve", "heat assistance valve (M1)", "Ventil Heizungsunterstützung (M1)", "Klep verwarmingsassistentie (M1)", "Uppvärmningsstöd Ventil (M1)", "zawór wspomagania grzania (M1)", "varmehjelpsventil (M1)", "vanne assistance thermique (M1)", "ısıtma yardım vanası (M1)", "valvola scambiatore (M1)", "tepelný asistenčný ventil (M1)", "ventil pomocného topení (M1)")
MAKE_TRANSLATION(m1Power, "heatassistpower", "heat assistance valve power (M1)", "Ventilleistung Heizungsunterstützung (M1)", "Vermogen klep verwarmingsassistentie (M1)", "Uppvärmningsstöd Ventil Effekt (M1)", "moc zaworu wspomagania grzania (M1)", "varmehjelpsventileffekt (M1)", "puissance vanne assistance thermique (M1)", "ısıtma yardım vanası gücü (M1)", "potenza valvola scambiatore (M1)", "výkon ventilu tepelného asistenta (M1)", "výkon ventilu pomocného topení (M1)")
MAKE_TRANSLATION(pumpMinMod, "pumpminmod", "minimum pump modulation", "minimale Pumpenmodulation", "Minimale pompmodulatie", "Min Pumpmodulering", "minimalna modulacja pompy", "minimum pumpmodulering", "modulation minimale pompe", "minimum pompa modülasyonu", "modulazione minima pompa", "minimálna modulácia čerpadla", "minimální modulace čerpadla")
MAKE_TRANSLATION(maxFlow, "maxflow", "maximum solar flow", "maximaler Durchfluss", "Maximale doorstroom solar", "Max Flöde Solpanel", "maksymalny przepływ solarów", "maks strømming solpanel", "débit solaire maximum", "minimum G.E. akışı", "portata massima solare", "maximálny solárny prietok", "maximální průtok soláru")
MAKE_TRANSLATION(solarPower, "solarpower", "actual solar power", "aktuelle Solarleistung", "Huidig solar vermogen", "Aktuellt Sol-effekt", "aktualna moc solarów", "aktuell soleffekt", "puissance solaire réelle", "gerçek G.E. gücü", "potenza attuale solare", "skutočná slnečná energia", "aktuální solární výkon")
MAKE_TRANSLATION(solarPumpTurnonDiff, "turnondiff", "pump turn on difference", "Einschalthysterese Pumpe", "Inschakelhysterese pomp", "Aktiveringshysteres Pump", "histereza załączenia pompy", "slå på hysteresepumpe", "différence activation pompe", "pompa devreye alma farkı", "isteresi di accensione pompa", "rozdiel v zapnutí čerpadla", "rozdíl pro zapnutí čerpadla")
MAKE_TRANSLATION(solarPumpTurnoffDiff, "turnoffdiff", "pump turn off difference", "Ausschalthysterese Pumpe", "Uitschakelhysterese pomp", "Avslagshysteres Pump", "histereza włączenia pompy", "slå av hysteresepumpe", "différence arrêt pompe", "pompa kapama farkı", "isteresi di spegnimento pompa", "rozdiel vypnutia čerpadla", "rozdíl pro vypnutí čerpadla")
MAKE_TRANSLATION(pump2MinMod, "pump2minmod", "minimum pump 2 modulation", "minimale Modulation Pumpe 2", "Minimale modulatie pomp 2", "Min Modulering Pump 2", "minimalna modulacja pompy 2", "minimum pumpmodulering 2", "modulation minimale pompe 2", "minimum pompa 2 modülasyonu", "modulazione minima pompa 2", "minimálna modulácia čerpadla 2", "minimální modulace čerpadla 2")
MAKE_TRANSLATION(solarPump2TurnonDiff, "turnondiff2", "pump 2 turn on difference", "Einschalthysterese Pumpe 2", "Inschakelhysterese pomp 2", "Aktiveringshysteres Pump 2", "histereza załączenia pompy 2", "slå på hysteresepumpe 2", "différence activation pompe 2", "pompa 2 devreye alma farkı", "isteresi di accensione pompa 2", "rozdiel v zapnutí čerpadla 2", "rozdíl pro zapnutí čerpadla 2")
MAKE_TRANSLATION(solarPump2TurnoffDiff, "turnoffdiff2", "pump 2 turn off difference", "Ausschalthysterese Pumpe 2", "Uitschakelhysterese pomp 2", "Avslagshysteres Pump 2", "histereza wyłączenia pompy 2", "slå av hysteresepumpe 2", "différence arrêt pompe 2", "pompa 2 kapama farkı", "isteresi di spegnimento pompa", "rozdiel vypnutia čerpadla 2", "rozdíl pro vypnutí čerpadla 2")
MAKE_TRANSLATION(collectorTemp, "collectortemp", "collector temperature (TS1)", "Kollektortemperatur (TS1)", "Collectortemperatuur (TS1)", "Kollektor Temperatur (TS1)", "temperatura kolektora (TS1)", "kollektor temperatur (TS1)", "température collecteur (TS1)", "kollektör sıcaklığı (TS1)", "temperatura collettore (TS1)", "teplota kolektora (TS1)", "teplota kolektoru (TS1)")
MAKE_TRANSLATION(collector2Temp, "collector2temp", "collector 2 temperature (TS7)", "Kollectortemperatur 2 (TS7)", "Collector 2 temperatuur (TS7)", "Kollektor 2 Temperatur (TS7)", "temperatura kolektora 2 (TS7)", "kollektor 2 temperatur (TS7)", "température collecteur 2 (TS7)", "kollektör 2 sıcaklığı (TS2)", "temperatura collettore 2 (TS7)", "teplota kolektora 2 (TS7)", "teplota kolektoru 2 (TS7)")
MAKE_TRANSLATION(cylBottomTemp, "cylbottomtemp", "cylinder bottom temperature (TS2)", "Speicherbodentemperatur (TS2)", "Bodemtemperatuur zonneboiler (TS2)", "Cylindertemperatur Botten (TS2)", "temperatura na spodzie zasobnika (TS2)", "beredertemp i bunn (TS2)", "température fond de cylindre (TS2)", "alt depolama sıcaklığıc(TS2)", "temperatura inferiore accumulo (TS2)", "teplota dna valca (TS2)", "teplota dna válce (TS2)")
MAKE_TRANSLATION(cyl2BottomTemp, "cyl2bottomtemp", "second cylinder bottom temperature (TS5)", "Speicherbodentemperatur 2 (TS5)", "Bodemtemperatuur 2e boiler", "Sekundär Cylindertemperatur Botten (TS5)", "temperatura na spodzie drugiego zasobnika (TS5)", "skundær beredertemp i bunn (TS5)", "température fond de cylindre (TS5)", "ikinci alt depolama sıcaklığıc(TS5)", "temperatura inferiore 2° accumulo (TS5)", "teplota dna druhého valca (TS5)", "teplota dna druhého válce (TS5)")
MAKE_TRANSLATION(heatExchangerTemp, "heatexchangertemp", "heat exchanger temperature (TS6)", "Wärmetauschertemperatur (TS6)", "Temperatuur warmtewisselaar (TS6)", "Värmeväxlare Temperatur (TS6)", "temperatura wymiennika ciepła (TS6)", "Varmeveksler temperatur (TS6)", "température échangeur de chaleur (TS6)", "eşanjör sıcaklığı (TS6)", "temperatura scambiatore calore (TS6)", "teplota výmenníka tepla (TS6)", "teplota výměníku tepla (TS6)")
MAKE_TRANSLATION(collectorMaxTemp, "collectormaxtemp", "maximum collector temperature", "maximale Kollektortemperatur", "Maximale collectortemperatuur", "Max. Kollektortemperatur", "maksymalna temperatura kolektora", "maks kollektortemperatur", "température max. collecteur", "maksimum kollektör sıcaklığı", " temperatura massima scambiatore calore", "maximálna teplota kolektora", "maximální teplota kolektoru")
MAKE_TRANSLATION(collectorMinTemp, "collectormintemp", "minimum collector temperature", "minimale Kollektortemperatur", "Minimale collectortemperatuur", "Min. Kollektortemperatur", "minimalna temperatura kolektora", "min. kollektortemperatur", "température min. collecteur", "minimum kollektör sıcaklığı", "temperatura minima scambiatore calore", "minimálna teplota kolektora", "minimální teplota kolektoru")
MAKE_TRANSLATION(cylMaxTemp, "cylmaxtemp", "maximum cylinder temperature", "maximale Speichertemperatur", "maximale temperatuur zonneboiler", "Max Cylindertemperatur", "maksymalna temperatura zasobnika", "maks beredertemperatur", "température max. cylindre", "maksimum silindir sıcaklığı", "temperatura massima vaso accumulo", "maximálna teplota valca", "maximální teplota válce")
MAKE_TRANSLATION(solarPumpMod, "solarpumpmod", "pump modulation (PS1)", "Pumpenmodulation (PS1)", "Pompmodulatie (PS1)", "Pumpmodulering (PS1)", "modulacja pompy solarnej (PS1)", "solpumpmodulering (PS1)", "modulation pompe (PS1)", "pompa modülasyonu (PS1)", "modulazione pompa (PS1)", "modulácia čerpadla (PS1)", "modulace čerpadla (PS1)")
MAKE_TRANSLATION(cylPumpMod, "cylpumpmod", "cylinder pump modulation (PS5)", "Speicherpumpenmodulation (PS5)", "Modulatie zonneboilerpomp (PS5)", "Cylinderpumpmodulering (PS5)", "modulacja pompy zasobnika (PS5)", "sylinderpumpemodulering (P55)", "modulation pompe cylindre (PS5)", "silindir pompa modülasyonu (PS5)", "pompa modulazione accumulo (PS5)", "modulácia čerpadla valca (PS5)", "modulace čerpadla válce (PS5)")
MAKE_TRANSLATION(solarPump, "solarpump", "pump (PS1)", "Pumpe (PS1)", "Pomp (PS1)", "Pump (PS1)", "pompa solarna (PS1)", "solpumpe (PS1)", "pompe solaire (PS1)", "pompa (PS1)", "pompa solare (PS1)", "čerpadlo (PS1)", "čerpadlo (PS1)")
MAKE_TRANSLATION(solarPump2, "solarpump2", "pump 2 (PS4)", "Pumpe 2 (PS4)", "Pomp 2 (PS4)", "Pump 2 (PS4)", "pompa solarna 2 (PS4)", "solpumpe 2 (PS4)", "pompe 2 (PS4)", "pompa 2 (PS4)", "pompa solare 2 (PS4)", "čerpadlo 2 (PS4)", "čerpadlo 2 (PS4)")
MAKE_TRANSLATION(solarPump2Mod, "solarpump2mod", "pump 2 modulation (PS4)", "Modulation Pumpe 2 (PS4)", "Modulatie pomp 2 (PS4)", "Pump 2 Modulering (PS4)", "modulacja pompy solarnej 2 (PS4)", "solpumpe2modulering (PS4)", "modulation pompe solaire 2 (PS4)", "pompa2 modülasyonu(PS1)", "pompa modulazione 2 (PS4)", "modulácia pumpy 2 (PS4)", "modulace čerpadla 2 (PS4)")
MAKE_TRANSLATION(valveStatus, "valvestatus", "valve status", "Ventilstatus", "Klepstatus", "Ventilstatus", "stan zaworu", "ventilstatus", "statut valve", "vana durumu", "stato valvola", "stav ventilu", "stav ventilu")
MAKE_TRANSLATION(vs1Status, "vs1status", "valve status VS1", "Ventilstatus VS1", "Klepstatus VS1", "Ventilstatus VS1", "stan zaworu VS1", "ventilstatus VS1", "statut valve VS1", "vana durumu VS1", "stato valvola VS1", "stav ventilu VS1", "stav ventilu VS1")
MAKE_TRANSLATION(vs3Status, "vs3status", "valve status VS3", "Ventilstatus VS3", "Klepstatus VS3", "Ventilstatus VS3", "stan zaworu VS3", "ventilstatus VS3", "statut valve VS3", "vana durumu VS3", "stato valvola VS3", "stav ventilu VS3", "stav ventilu VS3")
MAKE_TRANSLATION(cylHeated, "cylheated", "cyl heated", "Speichertemperatur erreicht", "Boilertemperatuur behaald", "Värmepanna Uppvärmd", "zasobnik został nagrzany", "bereder oppvarmt", "cylindre chauffé", "depolama sıcakllığına ulaşıldı", "temperatura richiesta vaso accumulo raggiunta", "Dosiahnutá teplota zásobníka", "ohřívání válce")
MAKE_TRANSLATION(collectorShutdown, "collectorshutdown", "collector shutdown", "Kollektorabschaltung", "Collector afschakeling", "Kollektor Avstängning", "wyłączenie kolektora", "kollektor stengt", "arrêt collecteur", "kollektör kapalı", "spegnimento del collettore", "vypnutie kolektora", "vypnutí kolektoru")
MAKE_TRANSLATION(pumpWorkTime, "pumpworktime", "pump working time", "Pumpenlaufzeit", "Pomplooptijd", "Pump Drifttid", "czas pracy pompy", "driftstid pumpe", "durée fonctionnement pompe", "pompa çalışma süresi", "tempo funzionamento pompa", "pracovný čas čerpadla", "pracovní doba čerpadla")
MAKE_TRANSLATION(pump2WorkTime, "pump2worktime", "pump 2 working time", "Laufzeit Pumpe 2", "Looptijd pomp 2", "Pump 2 Drifttid", "czas pracy pompy 2", "driftstid pumpe2", "durée fonctionnement pompe 2", "pompa 2 çalışma süresi", "tempo funzionamento pompa 2", "pracovný čas čerpadla 2", "pracovní doba čerpadla 2")
MAKE_TRANSLATION(m1WorkTime, "m1worktime", "differential control working time", "Differenzregelung Arbeitszeit", "Verschilregeling arbeidstijd", "Differentialreglering Drifttid", "czas pracy regulacji różnicowej", "differentialreguleringssrifttid", "durée fonctionnement contrôle différentiel", "çalışma saatlerinin farklı düzenlenmesi", "controllo differenziale durata funzionamento", "pracovný čas diferenciálnej kontroly", "pracovní doba diferenciální regulace")
MAKE_TRANSLATION(energyLastHour, "energylasthour", "energy last hour", "Energie letzte Std", "Energie laatste uur", "Energi Senaste Timmen", "energia w ciągu ostatniej godziny", "energi siste time", "énergie dernière heure", "son saat enerji", "Eenergia ultima ora", "energia za poslednú hodinu", "energie za poslední hodinu")
MAKE_TRANSLATION(energyTotal, "energytotal", "total energy", "Gesamtenergie", "Totale energie", "Total Energi", "energia całkowita", "total energi", "énergie totale", "toplam enerji", "energia totale", "celková energia", "celková energie")
MAKE_TRANSLATION(energyToday, "energytoday", "total energy today", "Energie heute", "Energie vandaag", "Total Energi Idag", "energia całkowita dzisiaj", "total energi i dag", "énergie totale aujourd'hui", "bugün toplam enerji", "totale energia giornaliera", "celková energia dnes", "celková energie dnes")
MAKE_TRANSLATION(cyl3BottomTemp, "cyl3bottomtemp", "third cylinder bottom temperature (TS11)", "Speichertemperatur unten (TS11)", "", "Tredje Cylindertemperatur Botten (TS11)", "", "", "", "", "", "spodná teplota tretieho valca (TS11)", "") // TODO translate
MAKE_TRANSLATION(cylTopTemp, "cyltoptemp", "cylinder top temperature (TS10)", "Speichertemperatur oben (TS10)", "", "Cylindertemperatur Toppen (TS10)", "", "", "", "", "", "horná teplota valca (TS10)", "") // TODO translate
MAKE_TRANSLATION(transferPumpMod, "transferpumpmod", "transfer pump modulation", "Transferpumpenmodulation", "", "Överföringspumpmodulering", "", "", "", "", "", "modulácia prenosového čerpadla", "") // TODO translate
MAKE_TRANSLATION(transferPump, "transferpump", "transfer pump", "Transferpumpe", "", "Överföringspump", "", "", "", "", "", "prenosové čerpadlo", "") // TODO translate
// solar dhw
MAKE_TRANSLATION(wwColdTemp, "coldtemp", "cold water", "Kaltwasser", "", "kallvatten", "zimna woda", "", "", "", "", "studená voda", "studená voda") // TODO translate
MAKE_TRANSLATION(wwTemp5, "temp5", "temperature 5", "Temperatur 5", "Temperatuur 5", "Temperatur 5", "temperatura 5", "Temperatur 5", "température 5", "sıcaklık 5", "Temperatura 5", "teplota 5", "teplota 5")
MAKE_TRANSLATION(wwTemp6, "temp6", "temperature 6", "Temperatur 6", "Temperatuur 6", "Temperatur 6", "temperatura 6", "temperatur 6", "température 6", "sıcaklık 6", "Temperatura 6", "teplota 6", "teplota 6")
// MAKE_TRANSLATION(wwTemp7, "temp7", "temperature 7", "Temperatur 7", "Temperatuur 7", "Temperatur 7", "temperatura 7", "Temperatur 7", "température 7", "sıcaklık 7", "Temperatura 7", "teplota 7", "teplota 7")
MAKE_TRANSLATION(wwPump, "pump", "pump", "Pumpe", "Pomp", "Pump", "pompa", "pumpe", "pompe", "pompa", "Pompa", "čerpadlo", "čerpadlo")
MAKE_TRANSLATION(wwCircTc, "circtc", "circulation time controled", "zeitgesteuerte Zirkulation", "", "Tidsstyrd cirkulation", "", "", "", "", "", "riadená doba cirkulácie", "řízený čas cirkulace") // TODO translate
MAKE_TRANSLATION(errorDisp, "errordisp", "error display", "Fehleranzeige", "", "Feldisplay", "wyświetlanie błędów", "", "", "", "", "zobrazenie chyby", "zobrazení chyb") // TODO translate
MAKE_TRANSLATION(deltaTRet, "deltatret", "temp. diff. return valve", "Temperaturdifferenz Rücklaufventil", "", "Temperaturskillnad returventil", "różnica temp. zaworu powrotnego", "", "", "", "", "rozdiel teplôt spätného ventilu", "rozdíl teploty zpátečního ventilu") // TODO translate
// solar dhw and mixer dhw
MAKE_TRANSLATION(wwMinTemp, "mintemp", "minimum temperature", "minimale Temperatur", "Minimale temperatuur", "Min. Temperatur", "temperatura minimalna", "min. temperatur", "température min.", "minimum sıcaklık", "temperatura minima", "minimálna teplota", "minimální teplota")
MAKE_TRANSLATION(wwRedTemp, "redtemp", "reduced temperature", "reduzierte Temperatur", "Gereduceerde temperatuur", "Reducerad Temperatur", "temperatura zredukowana", "reducert temperatur", "température réduite", "düşürülmüş sıcaklık", "temperatura ridotta", "znížená teplota", "snížená teplota")
MAKE_TRANSLATION(wwDailyTemp, "dailytemp", "daily temperature", "tägl. Temperatur", "Dagelijkse temperatuur", "Daglig temperatur", "temperatura dzienna", "dagtemperatur", "température en journée", "günlük sıcaklık", "temperatura giornaliera", "denná teplota", "denní teplota")
MAKE_TRANSLATION(wwHotTemp, "hottemp", "extra hot temperature", "sehr heiße Temperatur", "", "Mycket varm temperatur", "", "", "", "", "", "extra vysoká teplota", "teplota extra horká") // TODO translate
MAKE_TRANSLATION(wwKeepWarm, "keepwarm", "keep warm", "Warmhalten", "Warm houde", "Varmhållning", "utrzymywanie ciepła", "holde varmen", "maintenir chaleur", "ılık tut", "mantenimento calore", "udržovať v teple", "udržení tepla")
MAKE_TRANSLATION(wwStatus2, "status2", "status 2", "Status 2", "Status 2", "Status 2", "status 2", "status 2", "statut 2", "durum 2", "Status 2", "stav 2", "stav 2")
MAKE_TRANSLATION(wwPumpMod, "pumpmod", "pump modulation", "Pumpenmodulation", "Pompmodulatie", "Pumpmodulering", "modulacja pompy", "pumpemodulering", "modulation de pompe", "pompa modülasyonu", "modulazione pompa", "modulácia čerpadla", "modulace čerpadla")
MAKE_TRANSLATION(wwFlow, "flow", "flow rate", "Volumenstrom", "Doorstroomsnelheid", "Flöde", "przepływ", "strømningshastighet", "débit", "akış hızı", "portata flusso", "prietok", "průtok")
// MAKE_TRANSLATION(wwRetValve, "retvalve", "return valve", "Rücklauf Ventil", "", "Returventil", "", "", "", "", "", "spätný ventil", "zpětný ventil") // TODO translate
// extra mixer dhw
MAKE_TRANSLATION(wwRequiredTemp, "requiredtemp", "required temperature", "benötigte Temperatur", "Benodigde temperatuur", "Nödvändig Temperatur", "temperatura wymagana", "nødvendig temperatur", "température requise", "gerekli sıcaklık", "temperatura richiesta", "požadovaná teplota", "požadovaná teplota")
MAKE_TRANSLATION(wwDiffTemp, "difftemp", "start differential temperature", "Start Differenztemperatur", "Start differentiele temperatuur", "Start Skillnadstemperatur", "start temperatury różnicowej", "start differensialtemperatur", "température différentielle de départ", "diferansiyel sıcaklık", "avvia temperatura differenziale", "začiatok diferenciálnej teploty", "rozdílová teplota pro spuštění")
MAKE_TRANSLATION(wwPumpStatus, "pumpstatus", "pump status in assigned dhw (PC1)", "Pumpenstatus des WWK (PC1)", "Pompstatus in WW circuit (PC1)", "Pumpstatus i VV-krets (PC1)", "stan pompy w obwodzie c.w.u. (PC1)", "Pumpestatus i VV-krets (PC1)", "état pompe dhw (PC1)", "Kullanım suyu devresindeki(PC1) pompa durumu", "stato pompa assegnato nel ciruito WW (PC1)", "stav čerpadla v pridelenom TÚV (PC1)", "stav čerpadla přiřazeného k TUV (PC1)")
MAKE_TRANSLATION(wwTempStatus, "tempstatus", "temperature switch in assigned dhw (MC1)", "Temperaturschalter des WWK (MC1)", "Temperatuurschakeling in WW circuit (MC1)", "Temperaturventil i VV-krets (MC1)", "temperatura w obwodzie c.w.u. (MC1)", "temperaturventil i VV-krets (MC1)", "température bascule dhw (MC1).", "atanmış sıcak su devresinde sıcaklık", "interruttore di temperatura del wwk (MC1)", "teplotný spínač v priradenej TÚV (MC1)", "přepínací teplota v přiřazeném TUV (MC1)")
MAKE_TRANSLATION(wwTemp, "temp", "current temperature", "aktuelle Temperatur", "huidige temperatuur", "Aktuell Temperatur", "temperatura c.w.u.", "aktuell temperatur", "température actuelle", "güncel sıcaklık", "temperatura attuale", "aktuálna teplota", "aktuální teplota")
// SM100
MAKE_TRANSLATION(heatTransferSystem, "heattransfersystem", "heattransfer system", "Wärmeübertragungssystem", "Warmteoverdrachtssysteem", "Värmeöverföringssystem", "system wymiany ciepła", "varmeoverføringssystem", "système de transfert de chaleur", "ıs transfer sistemi", "sistema di trasferimento del calore", "systém prenosu tepla", "systém přenosu tepla")
MAKE_TRANSLATION(externalCyl, "externalcyl", "external cylinder", "Externer Speicher", "Externe boiler", "Extern Cylinder", "zbiornik zewnętrzny", "ekstern bereder", "cylindre externe", "dış silindir", "vaso accumulo esterno", "vonkajší valec", "vnější válec")
MAKE_TRANSLATION(thermalDisinfect, "thermaldisinfect", "thermal disinfection", "Thermische Desinfektion", "Thermische desinfectie", "Termisk Desinfektion", "dezynfekcja termiczna", "termisk desinfeksjon", "désinfection thermique", "ısıl temizlik", "disinfezione termica", "tepelná dezinfekcia", "teplotní dezinfekce")
MAKE_TRANSLATION(heatMetering, "heatmetering", "heatmetering", "Wärmemessung", "warmtemeting", "Värmemätning", "pomiar ciepła", "varmemåling", "mesure de chaleur", "ısı ölçümü", "misurazione del calore", "meranie tepla", "měření tepla")
MAKE_TRANSLATION(solarIsEnabled, "solarenabled", "solarmodule enabled", "Solarmodul aktiviert", "Solarmodule geactiveerd", "Solmodul Aktiverad", "system solarny", "solmodul aktivert", "module solaire activé", "güneş modu etkinleştirildi", "modulo solare attivato", "solárny modul povolený", "solární modul povolen")
// telegram 0x035A
MAKE_TRANSLATION(solarPumpMode, "solarpumpmode", "solar pump mode", "Solarpumpenmodus", "Modus zonneboilerpomp", "Sol Pumpläge", "tryb pracy pompy", "solpumpemodus", "mode pompe solaire", "pompa modu", "modalità pompa solare", "režim čerpadla", "režim solárního čerpadla")
MAKE_TRANSLATION(solarPumpKick, "solarpumpkick", "solar pump kick", "Röhrenkollektorfunktion", "Modus buizencollector", "Sol Kollektorfunktion", "wspomaganie startu pompy", "solkllektorfunksjon", "démarrage boost pompe solaire", "pompa zorunlu çalıştırma", "avvio forzato pompa", "kopnutie pumpy", "kop solárního čerpadla")
MAKE_TRANSLATION(plainWaterMode, "plainwatermode", "plain water mode", "Südeuropafunktion", "Modus Zuid-Europa", "Sydeuropa-funktion", "tylko woda (dla Europy Południowej)", "vanlig vannmodus", "mode eau ordinaire", "sadece su modu", "modalità acqua normale", "režim čistej vody", "režim čisté vody")
MAKE_TRANSLATION(doubleMatchFlow, "doublematchflow", "doublematchflow", "Double Match Flow", "Double Match Flow", "Dubbelmatchning Flöde", "przepływ podwójnie dopasowany", "dobbelmatch flow", "double match flow", "doublematch akışı", "carico ottimizzato dell'accumulatore ad effetto termosifone", "Tok dvojitej zhody", "dvojitý průtok")
MAKE_TRANSLATION(solarPump2Mode, "pump2mode", "pump 2 mode", "Modus Pumpe 2", "Modus pomp 2", "Pump 2 Läge", "tryb pracy pompy 2", "pump 2 modus", "mode pompe 2", "pompa 2 modu", "modalità pompa 2", "režim čerpadla 2", "režim čerpadla 2")
MAKE_TRANSLATION(solarPump2Kick, "pump2kick", "pump kick 2", "Startboost Pumpe 2", "Startboost pomp 2", "Pump 2 Kollektorfunktion", "wspomaganie startu pompy 2", "startboost pumpe 2", "démarrage boost pompe 2", "pompa 2 zorunlu çalıştırma", "avvio forzato pompa 2", "nútený štart čerpadla 2", "kop čerpadla 2")
// telegram 0x035F
MAKE_TRANSLATION(cylPriority, "cylpriority", "cylinder priority", "Speicherpriorität", "Prioriteit boiler", "Cylinderprioritering", "priorytet cylindra", "berederprioritering", "priorité de cylindre", "silindir önceliği", "priorità vaso accumulo", "Priorita valca", "přednost válce")
// telegram 0x380
MAKE_TRANSLATION(climateZone, "climatezone", "climate zone", "Klimazone", "klimaatzone", "Klimatzon", "strefa klimatyczna", "klimasone", "zone de climat", "iklim alanı", "zona clima", "klimatická zóna", "klimatická zóna")
MAKE_TRANSLATION(collector1Area, "collector1area", "collector 1 area", "Kollektorfläche 1", "oppervlakte collector 1", "Kollektor 1 Area", "powierzchnia kolektora 1", "kollektor 1 område", "zone collecteur 1", "kollektör 1 alan", "area collettore 1", "oblasť kolektora 1", "plocha kolektoru 1")
MAKE_TRANSLATION(collector1Type, "collector1type", "collector 1 type", "Kollektortyp 1", "Type collector 1", "Kollektor 1 Typ", "typ kolektora 1", "kollektor 1 type", "type collecteur 1", "kollektör 1 tip", "tipo collettore 1", "kolektor 1 typ", "typ kolektoru 1")
MAKE_TRANSLATION(collector2Area, "collector2area", "collector 2 area", "Kollektorfläche 2", "Oppervlakte collector 2", "Kollektor 2 Area", "powierzchnia kolektora 2", "kollektor 2 område", "zone collecteur 2", "kollektör 2 alan", "area collettore 2", "oblasť kolektora 2", "plocha kolektoru 2")
MAKE_TRANSLATION(collector2Type, "collector2type", "collector 2 type", "Kollektortyp 2", "Type collector 2", "Kollektor 2 Typ", "typ kolektora 2", "kollektor 2 type", "type collecteur 2", "kollektör 2 tip", "tipo collettore 2", "kolektor 2 typ", "typ kolektoru 2")
// telegram 0x0363 heatCounter
MAKE_TRANSLATION(heatCntFlowTemp, "heatcntflowtemp", "heat counter flow temperature", "Wärmezähler Vorlauftemperatur", "Aanvoertemperatuur warmteenergiemeter", "Värmeräknare Flödestemperatur", "temperatura zasilania ciepłomierza", "varmeenergimåler turtemperatur", "température flux compteur chaleur", "ısı sayacı akış sıcaklığı", "Temperatura di mandata del contatore di calore", "teplota prúdu počítadla tepla", "počítadlo teploty průtoku")
MAKE_TRANSLATION(heatCntRetTemp, "heatcntrettemp", "heat counter return temperature", "Wärmezähler Rücklauftemperatur", "Retourtemperatuur warmteenergiemeter", "Värmeräknare Returtemperatur", "temperatura powrotu ciepłomierza", "varmeenergimåler returtemperatur", "température retour compteur chaleur", "ısı sayacı dönüş sıcaklığı", "Temperatura di ritorno del contatore di calore", "teplota spiatočky počítadla tepla", "počítadlo teploty zpátečky")
MAKE_TRANSLATION(heatCnt, "heatcnt", "heat counter impulses", "Wärmezähler Impulse", "Warmteenergiemeter pulsen", "Värmeräknare Impuls", "liczba impulsów ciepłomierza", "varmemåler impuls", "impulsions compteur chaleur", "ısı sayacı atış adedi", "contacalore a impulsi", "Impulzy počítadla tepla", "počítadlo impulzů")
MAKE_TRANSLATION(swapFlowTemp, "swapflowtemp", "swap flow temperature (TS14)", "Austausch Vorlauftemperatur (TS14)", "Aanvoertemperatuur verwisselaar (TS14)", "Växlingstemperatur Flöde (TS14)", "temperatura zasilania wymiennika", "veksler turledningstemperatur (TS14)", "température flux échangeur (TS14)", "değişim akış sıcaklığı(TS14)", "Scambiare la temperatura di mandata (TS14)", "swap flow temperature (TS14)", "přepínací průtoková teplota (TS14)")
MAKE_TRANSLATION(swapRetTemp, "swaprettemp", "swap return temperature (TS15)", "Austausch Rücklauftemperatur (TS15)", "Retourtemperatuur verwisselaar (TS15)", "Växlingstemperatur Returflöde (TS15)", "temperatura powrotu wymiennika", "veksler returledningstemperatur (TS15)", "température retour échangeur (TS15)", "değişim dönüş sıcaklığı(TS15)", "Scambiare la temperatura di ritorno (TS15)", "výmena teploty spiatočky (TS15)", "přepínací teplota zpátečky (TS15)")
// switch
MAKE_TRANSLATION(activated, "activated", "activated", "Aktiviert", "Geactiveerd", "Aktiverad", "aktywowany", "aktivert", "activé", "başladı", "attivato", "aktivovaný", "aktivováno")
MAKE_TRANSLATION(status, "status", "status", "Status", "Status", "Status", "status", "status", "statut", "durum", "Stato", "stav", "stav")
// RF sensor, id 0x40, telegram 0x435
MAKE_TRANSLATION(RFTemp, "rftemp", "RF room temperature sensor", "RF Raumtemperatursensor", "RF ruimtetemperatuur sensor", "RF Rumsgivare Temperatur", "bezprzewodowy czujnik temperatury pomieszczenia", "RF romsgiver temp", "capteur de température de pièce RF", "RF oda sıcaklık sensörü", "Sensore di temperatura ambiente RF", "RF snímač izbovej teploty", "RF senzor teploty místnosti")
// ventilation
MAKE_TRANSLATION(outFresh, "outfresh", "outdoor fresh air", "Außenlufttemp.", "temperatuur buitenlucht", "Utelufttemperatur", "świeże powietrze z zewnątrz", "", "", "dış ortam taze hava", "aria fresca esterna", "čerstvý vzduch vonku", "venkovní čerstvý vzduch") // TODO translate
MAKE_TRANSLATION(inFresh, "infresh", "indoor fresh air", "Zulufttemp.", "temperatuur aanvoer", "Tillufttemperatur", "nawiew", "", "", "iç ortam taze hava", "aria fresca interna", "čerstvý vzduch v interiéri", "vnitřní čerstvý vzduch") // TODO translate
MAKE_TRANSLATION(outEx, "outexhaust", "outdoor exhaust air", "Fortlufttemp.", "uitlaatemperatuur buiten", "Utgående lufttemperatur", "zużyte powietrze z wewnątrz", "", "", "dış ortam egsoz", "aria di scarico esterna", "vonkajší odpadový vzduch", "venkovní odpadní vzduch") // TODO translate
MAKE_TRANSLATION(inEx, "inexhaust", "indoor exhaust air", "Ablufttemp.", "uitlaattemperatuur binnen", "Frånlufttemperatur", "wywiew", "", "", "iç ortam egsoz", "aria di scarico interna", "vnútorný odpadový vzduch", "vnitřní odpadní vzduch") // TODO translate
MAKE_TRANSLATION(ventMode, "ventmode", "ventilation mode", "Belüftungsmodus", "ventilatiemodus", "ventilationsläge", "tryb wentylacji", "", "", "havalandırma modu", "modalità di ventilazione", "režim vetrania", "režim ventilace") // TODO translate
MAKE_TRANSLATION(ventInSpeed, "ventinspeed", "in blower speed", "Zuluftdrehzahl", "toerental aanvoerventilator", "Tilluftflöde", "prędkość wentylatora nawiewu", "", "", "iç fan hızı", "velocità aria di alimentazione", "rýchlosť ventilátora", "rychlost vnitřního ventilátoru") // TODO translate
MAKE_TRANSLATION(ventOutSpeed, "ventoutspeed", "out blower speed", "Abluftdrehzahl", "toerental afvoerventilator", "Frånluftflöde", "prędkość wentylatora wywiewu", "", "", "dış fan hızı", "velocità aria di scarico", "rýchlosť výstupného ventilátora", "rychlost venkovního ventilátoru") // TODO translate
MAKE_TRANSLATION(airquality, "airquality", "air quality (voc)", "Luftqualität (VOC)", "luchtkwaliteit (VOC)", "Luftkvalitet (VOC)", "jakość powietrza", "", "", "hava kalitesi(voc)", "qualità aria (VOC)", "kvalita vzduchu (voc)", "kvalita vzduchu (VOC)") // TODO translate
// EM100
MAKE_TRANSLATION(minV, "minv", "min volt.", "min. Spannung", "", "min. spänning", "minimalne napięcie", "", "", "", "", "min. napätie", "minimální napětí") // TODO translate
MAKE_TRANSLATION(maxV, "maxv", "max volt.", "max. Spannung", "", "max. spänning", "maksymalne napięcie", "", "", "", "", "max. napätie", "maximální napětí") // TODO translate
MAKE_TRANSLATION(minT, "mint", "min temp.", "min. Temperatur", "", "min. temperatur", "minimalna temperatura", "", "", "", "", "min. tepl.", "minimální teplota") // TODO translate
MAKE_TRANSLATION(maxT, "maxt", "max temp.", "max. Temperatur", "", "max. temperatur", "maksymalna temperatura", "", "", "", "", "max. tepl.", "maximální teplota") // TODO translate
MAKE_TRANSLATION(setPoint, "setpoint", "set temp.", "Solltemperatur", "", "Börvärde", "temperatura nastawu", "", "", "", "", "pož. teplota", "nastavená teplota") // TODO translate
MAKE_TRANSLATION(setPower, "setpower", "request power", "Sollleistung", "", "Böreffekt", "zadana moc", "", "", "", "", "pož. výkon", "požadovaný výkon") // TODO translate
MAKE_TRANSLATION(dip, "dip", "dip switch", "DIP-Schalter", "", "DIP-strömställare", "przełącznik DIP", "", "", "", "", "dip prepínač", "přepínač DIP") // TODO translate
MAKE_TRANSLATION(outPower, "outpow", "output IO1", "Ausgang IO1", "", "Utgångsffekt", "wyjście IO1", "", "", "", "", "výstup IO1", "výstup IO1") // TODO translate
MAKE_TRANSLATION(input, "input", "input", "Eingang", "", "Ingång", "wejście", "", "", "", "", "vstup", "vstup") // TODO translate
/*
// unknown fields to track (SM10), only for testing
// **** NO TRANSLATION NEEDED ****
MAKE_TRANSLATION(data11, "data11", "unknown datafield 11", "", "", "okänt datafält 11", "nieznane pole danych 11", "", "", "", "", "neznáme dátové pole 11", "") // TODO translate
MAKE_TRANSLATION(data12, "data12", "unknown datafield 12", "", "", "okänt datafält 12", "nieznane pole danych 12", "", "", "", "", "neznáme dátové pole 12", "") // TODO translate
MAKE_TRANSLATION(data8, "data8", "unknown datafield 8", "", "", "okänt datafält 8", "nieznane pole danych 8", "", "", "", "", "neznáme dátové pole 8", "") // TODO translate
MAKE_TRANSLATION(data0, "data0", "unknown datafield 0", "", "", "okänt datafält 0", "nieznane pole danych 0", "", "", "", "", "neznáme dátové pole 0", "") // TODO translate
MAKE_TRANSLATION(data1, "data1", "unknown datafield 1", "", "", "okänt datafält 1", "nieznane pole danych 1", "", "", "", "", "neznáme dátové pole 1", "") // TODO translate
MAKE_TRANSLATION(setting3, "setting3", "unknown setting 3", "", "", "okänd inställning 3", "nieznane ustawienie 3", "", "", "", "", "neznáme dátové pole 3", "") // TODO translate
MAKE_TRANSLATION(setting4, "setting4", "unknown setting 4", "", "", "okänd inställning 4", "nieznane ustawienie 4", "", "", "", "", "neznáme dátové pole 4", "") // TODO translate
*/
// clang-format on

31
src/core/main.cpp Normal file
View File

@@ -0,0 +1,31 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "emsesp.h"
using namespace emsesp;
static EMSESP application; // the main application
void setup() {
application.start();
}
void loop() {
application.loop();
}

531
src/core/modbus.cpp Normal file
View File

@@ -0,0 +1,531 @@
/**
* TODO: verwendete libs in readme hinzufügen
*/
#include "modbus.h"
#include "modbus_entity_parameters.hpp"
#include "emsesp.h"
#include "emsdevice.h"
#include <string>
#include <sstream>
#include <iomanip>
namespace emsesp {
#ifdef EMSESP_STANDALONE
// no eModbus lib in standalone build
enum FunctionCode : uint8_t { WRITE_HOLD_REGISTER = 0x06, WRITE_MULT_REGISTERS = 0x10 };
#endif
uuid::log::Logger Modbus::logger_{F_(modbus), uuid::log::Facility::DAEMON};
void Modbus::start(uint8_t systemServerId, uint16_t port, uint8_t max_clients, uint32_t timeout) {
#ifndef EMSESP_STANDALONE
if (!check_parameter_order()) {
LOG_ERROR("Unable to enable Modbus - the parameter list order is corrupt. This is a firmware bug.");
return;
}
modbusServer_ = new ModbusServerTCPasync();
modbusServer_->registerWorker(systemServerId, READ_INPUT_REGISTER, [this](auto && request) { return handleSystemRead(request); });
for (uint8_t i = EMSdevice::DeviceType::BOILER; i < EMSdevice::DeviceType::UNKNOWN; i++) {
if (i != systemServerId) {
modbusServer_->registerWorker(i, READ_INPUT_REGISTER, [this](auto && request) { return handleRead(request); });
modbusServer_->registerWorker(i, READ_HOLD_REGISTER, [this](auto && request) { return handleRead(request); });
modbusServer_->registerWorker(i, WRITE_HOLD_REGISTER, [this](auto && request) { return handleWrite(request); });
modbusServer_->registerWorker(i, WRITE_MULT_REGISTERS, [this](auto && request) { return handleWrite(request); });
}
}
modbusServer_->start(port, max_clients, timeout);
LOG_INFO("Starting Modbus service (ID %d, port %d)", systemServerId, port);
#else
if (!check_parameter_order()) {
LOG_ERROR("Unable to enable Modbus - the parameter list order is corrupt. This is a firmware bug.");
}
LOG_INFO("Modbus deactivated in standalone build.");
#endif
}
// this is currently never called, just for good measure
void Modbus::stop() {
#ifndef EMSESP_STANDALONE
modbusServer_->stop();
// virtual ~ModbusServerTCPasync() = default;
// delete modbusServer_;
modbusServer_ = nullptr;
#endif
}
// Check that the Modbus parameters defined in modbus_entity_parameters.cpp are correctly ordered
bool Modbus::check_parameter_order() {
EntityModbusInfo const * prev = nullptr;
bool isFirst = true;
for (const auto & mi : modbus_register_mappings) {
if (isFirst) {
isFirst = false;
} else if (prev == nullptr) {
LOG_ERROR("Error checking modbus parameters %s.", mi.short_name);
return false;
} else if (!prev->isLessThan(mi)) {
LOG_ERROR("Error in modbus parameters: %s must be listed before %s.", mi.short_name, prev->short_name);
return false;
}
prev = &mi;
}
return true;
}
int8_t Modbus::tag_to_type(int8_t tag) {
// this coulod even be an array
switch (tag) {
case DeviceValue::TAG_NONE:
return TAG_TYPE_NONE;
case DeviceValue::TAG_DEVICE_DATA:
return TAG_TYPE_DEVICE_DATA;
case DeviceValue::TAG_HC1:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC2:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC3:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC4:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC5:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC6:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC7:
return TAG_TYPE_HC;
case DeviceValue::TAG_HC8:
return TAG_TYPE_HC;
case DeviceValue::TAG_DHW1:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW2:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW3:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW4:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW5:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW6:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW7:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW8:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW9:
return TAG_TYPE_DHW;
case DeviceValue::TAG_DHW10:
return TAG_TYPE_DHW;
case DeviceValue::TAG_AHS1:
return TAG_TYPE_AHS;
case DeviceValue::TAG_HS1:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS2:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS3:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS4:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS5:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS6:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS7:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS8:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS9:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS10:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS11:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS12:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS13:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS14:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS15:
return TAG_TYPE_HS;
case DeviceValue::TAG_HS16:
return TAG_TYPE_HS;
default:
return INVALID_TAG_TYPE;
}
}
/* DEBUG
template <typename TInputIter>
std::string make_hex_string(TInputIter first, TInputIter last) {
std::ostringstream ss;
ss << std::hex << std::setfill('0');
while (first != last)
ss << std::setw(2) << static_cast<int>(*first++);
return ss.str();
}
*/
/**
*
* @param request
* @return
*/
ModbusMessage Modbus::handleSystemRead(const ModbusMessage & request) {
ModbusMessage response;
uint16_t start_address = 0;
uint16_t num_words = 0;
request.get(2, start_address);
request.get(4, num_words);
LOG_DEBUG("Got request for serverId %d, startAddress %d, numWords %d", request.getServerID(), start_address, num_words);
if (start_address < 1000) {
switch (start_address) {
case 1:
response.add(request.getServerID());
response.add(request.getFunctionCode());
response.add((uint8_t)2);
response.add((uint16_t)EMSESP::emsdevices.size());
break;
default:
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
}
} else if (start_address < 1000 + EMSESP::emsdevices.size()) {
const auto & dev = EMSESP::emsdevices[start_address - 1000];
response.add(request.getServerID());
response.add(request.getFunctionCode());
response.add((uint8_t)2);
response.add(dev->device_type());
response.add(dev->device_id());
} else {
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
}
return response;
}
ModbusMessage Modbus::handleRead(const ModbusMessage & request) {
ModbusMessage response;
uint8_t device_type = request.getServerID(); // the server ID is the same as the device type
uint16_t start_address = 0;
uint16_t num_words = 0;
request.get(2, start_address);
request.get(4, num_words);
LOG_DEBUG("Got read request for serverId %d, startAddress %d, numWords %d", device_type, start_address, num_words);
// each register block corresponds to a device value tag
auto tag = (uint8_t)(start_address / REGISTER_BLOCK_SIZE);
auto tag_type = tag_to_type(tag);
if (tag_type == INVALID_TAG_TYPE) {
// invalid register block, does not correspond to an existing tag
LOG_ERROR("invalid register block, does not correspond to an existing tag");
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
auto register_offset = start_address - tag * REGISTER_BLOCK_SIZE;
// binary search in modbus infos
auto key = EntityModbusInfoKey(device_type, tag_type, register_offset);
const auto & modbusInfo = std::lower_bound(std::begin(modbus_register_mappings),
std::end(modbus_register_mappings),
key,
[](const EntityModbusInfo & a, const EntityModbusInfoKey & b) { return a.isLessThan(b); });
if (modbusInfo == std::end(modbus_register_mappings) || !modbusInfo->equals(key)) {
// combination of device_type/tag_type/register_offset does not exist
LOG_ERROR("combination of device_type/tag_type/register_offset does not exist");
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
// only reading a single value at a time is supported for now
if (num_words != modbusInfo->registerCount) {
// number of registers requested does not match actual register count for entity
LOG_ERROR("number of registers requested does not match actual register count for entity");
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
auto buf = std::vector<uint16_t>(num_words);
int error_code = -1;
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice->device_type() == device_type) {
error_code = emsdevice->get_modbus_value(tag, modbusInfo->short_name, buf);
if (!error_code) {
break;
}
}
}
if (error_code) {
if (uuid::get_uptime_sec() > 60 || error_code < -2) { // suppress not found messages for the first minute
LOG_ERROR("Unable to read raw device value %s for tag=%d - error_code = %d", modbusInfo->short_name, (int)tag, error_code);
}
response.setError(request.getServerID(), request.getFunctionCode(), SERVER_DEVICE_FAILURE);
return response;
}
response.add(request.getServerID());
response.add(request.getFunctionCode());
response.add((uint8_t)(num_words * 2));
for (auto & value : buf)
response.add(value);
return response;
}
ModbusMessage Modbus::handleWrite(const ModbusMessage & request) {
ModbusMessage response;
uint8_t device_type = request.getServerID(); // the server ID is the same as the device type
uint8_t function_code = request.getFunctionCode();
uint16_t start_address = 0;
uint16_t num_words = 0;
uint8_t byte_count = 0;
std::vector<uint8_t> data;
if (function_code == WRITE_MULT_REGISTERS) {
request.get(2, start_address);
request.get(4, num_words);
request.get(6, byte_count);
request.get(7, data, byte_count);
} else if (function_code == WRITE_HOLD_REGISTER) {
num_words = 1;
byte_count = 2;
request.get(2, start_address);
request.get(4, data, byte_count);
} else {
LOG_ERROR("Function code %d is not implemented", function_code);
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_FUNCTION);
return response;
}
LOG_DEBUG("Got write request for serverId %d, startAddress %d, numWords %d, byteCount %d", device_type, start_address, num_words, byte_count);
// each register block corresponds to a device value tag
auto tag = (uint8_t)(start_address / REGISTER_BLOCK_SIZE);
auto tag_type = tag_to_type(tag);
if (tag_type == INVALID_TAG_TYPE) {
// invalid register block, does not correspond to an existing tag
LOG_ERROR("invalid register block (%d), does not correspond to an existing tag", tag);
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
auto register_offset = start_address - tag * REGISTER_BLOCK_SIZE;
LOG_DEBUG("Tag %d, offset %d", tag, register_offset);
// binary search in modbus infos
auto key = EntityModbusInfoKey(device_type, tag_type, register_offset);
auto modbusInfo = std::lower_bound(std::begin(modbus_register_mappings),
std::end(modbus_register_mappings),
key,
[](const EntityModbusInfo & mi, const EntityModbusInfoKey & k) { return mi.isLessThan(k); });
if (modbusInfo == std::end(modbus_register_mappings) || !modbusInfo->equals(key)) {
// combination of device_type/tag_type/register_offset does not exist
LOG_ERROR("combination of device_type (%d)/tag_type (%d)/register_offset (%d) does not exist",
key.device_type,
key.device_value_tag_type,
key.registerOffset);
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
LOG_DEBUG("Writing to entity %s", modbusInfo->short_name);
// only writing a single value at a time is supported for now
if (num_words != modbusInfo->registerCount) {
// number of registers requested does not match actual register count for entity
LOG_ERROR("number of registers (%d) requested does not match actual register count (%d) for entity", num_words, modbusInfo->registerCount);
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
JsonDocument input_doc;
JsonObject input = input_doc.to<JsonObject>();
int error_code = -1;
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice->device_type() == device_type) {
error_code = emsdevice->modbus_value_to_json(tag, modbusInfo->short_name, data, input);
if (!error_code) {
break;
}
}
}
if (error_code) {
// error getting modbus value as json
LOG_ERROR("error getting modbus value as json, error code = %d", error_code);
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_ADDRESS);
return response;
}
std::string path;
if (tag < DeviceValueTAG::TAG_HC1) {
path = std::string("api/") + std::string(EMSdevice::device_type_2_device_name(device_type)) + "/" + modbusInfo->short_name;
} else {
path = std::string("api/") + std::string(EMSdevice::device_type_2_device_name(device_type)) + "/" + EMSdevice::tag_to_mqtt(tag) + "/"
+ modbusInfo->short_name;
}
LOG_DEBUG("path: %s\n", path.c_str());
std::string inputStr;
serializeJson(input, inputStr);
LOG_DEBUG("input: %s\n", inputStr.c_str());
JsonDocument output_doc;
JsonObject output = output_doc.to<JsonObject>();
uint8_t return_code = Command::process(path.c_str(), true, input, output); // modbus is always authenticated
if (return_code != CommandRet::OK) {
char error[100];
if (output.size()) {
snprintf(error,
sizeof(error),
"Modbus write command failed with error %s (%s)",
(const char *)output["message"],
Command::return_code_string(return_code));
} else {
snprintf(error, sizeof(error), "Modbus write command failed with error code (%s)", Command::return_code_string(return_code));
}
LOG_ERROR(error);
response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_VALUE);
} else {
// all good
response.add(request.getServerID());
response.add(request.getFunctionCode());
response.add(start_address);
response.add(num_words);
}
return response;
}
#if defined(EMSESP_STANDALONE)
// return the relative register start offset for a DeviceValue, i.e. the address within the
// register block corresponding to the value's tag type.
int Modbus::getRegisterOffset(const DeviceValue & dv) {
auto it = std::find_if(std::begin(modbus_register_mappings), std::end(modbus_register_mappings), [&](const EntityModbusInfo & mi) {
return mi.device_type == dv.device_type && mi.device_value_tag_type == tag_to_type(dv.tag) && !strcmp(mi.short_name, dv.short_name);
});
if (it != std::end(modbus_register_mappings)) {
return it->registerOffset;
}
return -1;
}
// return the number of registers
int Modbus::getRegisterCount(const DeviceValue & dv) {
auto it = std::find_if(std::begin(modbus_register_mappings), std::end(modbus_register_mappings), [&](const EntityModbusInfo & mi) {
return mi.device_type == dv.device_type && mi.device_value_tag_type == tag_to_type(dv.tag) && !strcmp(mi.short_name, dv.short_name);
});
if (it != std::end(modbus_register_mappings)) {
// look up actual size
return it->registerCount;
} else {
// guess based on type
switch (dv.type) {
case DeviceValue::BOOL: // 8 bit
case DeviceValue::INT8:
case DeviceValue::UINT8:
case DeviceValue::INT16:
case DeviceValue::UINT16:
case DeviceValue::ENUM: // 8 bit
return 1;
case DeviceValue::UINT24:
case DeviceValue::UINT32:
case DeviceValue::TIME: // 32 bit
return 2;
case DeviceValue::CMD: {
// calculate a sensible register size from min, max and numeric_operator
uint32_t num_values = std::max(dv.max, (uint32_t)abs(dv.min));
int num_registers = 0;
if (num_values <= (1L << 8))
num_registers = 1;
else if (num_values <= (1L << 16))
num_registers = 2;
else if (num_values <= (1L << 32))
num_registers = 4;
else
LOG_ERROR("num_registers is too big to be encoded with modbus registers");
LOG_DEBUG("Value for CMD '%s' can take on %ld values and is encoded in %d registers", dv.short_name, num_values, num_registers);
return num_registers;
}
case DeviceValue::STRING:
break; // impossible to guess, needs to be hardcoded
}
}
return 0;
}
// return the absolute register start address for a DeviceValue
int Modbus::getRegisterStartAddress(const DeviceValue & dv) {
return dv.tag * REGISTER_BLOCK_SIZE + getRegisterOffset(dv);
}
#endif
} // namespace emsesp

110
src/core/modbus.h Normal file
View File

@@ -0,0 +1,110 @@
//
// Created by Michael Heyse on 08.02.24.
//
#ifndef EMSESP_MODBUS_H_
#define EMSESP_MODBUS_H_
#include "helpers.h"
#include "emsdevice.h"
#include "emsdevicevalue.h"
#include <string>
#include <map>
#include <utility>
#if defined(EMSESP_STANDALONE)
#include <../test/test_modbus/modbus_test.h>
#endif
#ifndef EMSESP_STANDALONE
#include <ModbusServerTCPasync.h>
#endif
namespace emsesp {
class Modbus {
public:
static const int REGISTER_BLOCK_SIZE = 1000;
void start(uint8_t systemServerId, uint16_t port, uint8_t max_clients, uint32_t timeout);
void stop();
#if defined(EMSESP_STANDALONE)
int getRegisterOffset(const DeviceValue & dv);
int getRegisterCount(const DeviceValue & dv);
int getRegisterStartAddress(const DeviceValue & dv);
#endif
private:
static uuid::log::Logger logger_;
struct EntityModbusInfoKey {
const uint8_t device_type;
const uint8_t device_value_tag_type;
const uint16_t registerOffset;
EntityModbusInfoKey(uint8_t deviceType, uint8_t deviceValueTagType, uint16_t registerOffset)
: device_type(deviceType)
, device_value_tag_type(deviceValueTagType)
, registerOffset(registerOffset) {
}
bool equals(const EntityModbusInfoKey & other) const {
return device_type == other.device_type && device_value_tag_type == other.device_value_tag_type && registerOffset == other.registerOffset;
}
};
struct EntityModbusInfo {
const uint8_t device_type;
const uint8_t device_value_tag_type;
const char * const short_name;
const uint16_t registerOffset;
const uint16_t registerCount;
bool equals(const EntityModbusInfoKey & other) const {
return device_type == other.device_type && device_value_tag_type == other.device_value_tag_type && registerOffset == other.registerOffset;
}
bool isLessThan(const EntityModbusInfoKey & other) const {
return device_type < other.device_type || ((device_type == other.device_type) && (device_value_tag_type < other.device_value_tag_type))
|| ((device_type == other.device_type) && (device_value_tag_type == other.device_value_tag_type) && (registerOffset < other.registerOffset));
}
bool isLessThan(const EntityModbusInfo & other) const {
return device_type < other.device_type || ((device_type == other.device_type) && (device_value_tag_type < other.device_value_tag_type))
|| ((device_type == other.device_type) && (device_value_tag_type == other.device_value_tag_type) && (registerOffset < other.registerOffset));
}
};
enum DeviceValueTAGType : int8_t {
TAG_TYPE_NONE = DeviceValue::DeviceValueTAG::TAG_NONE,
TAG_TYPE_DEVICE_DATA = DeviceValue::DeviceValueTAG::TAG_DEVICE_DATA,
TAG_TYPE_HC = DeviceValue::DeviceValueTAG::TAG_HC1,
TAG_TYPE_DHW = DeviceValue::DeviceValueTAG::TAG_DHW1,
TAG_TYPE_AHS = DeviceValue::DeviceValueTAG::TAG_AHS1,
TAG_TYPE_HS = DeviceValue::DeviceValueTAG::TAG_HS1,
INVALID_TAG_TYPE = -2
};
static const std::initializer_list<EntityModbusInfo> modbus_register_mappings;
static int8_t tag_to_type(int8_t tag);
static bool check_parameter_order();
#ifndef EMSESP_STANDALONE
ModbusServerTCPasync * modbusServer_;
#endif
#if defined(EMSESP_STANDALONE) || defined(EMSESP_TEST)
public:
#endif
static ModbusMessage handleSystemRead(const ModbusMessage & request);
static ModbusMessage handleRead(const ModbusMessage & request);
static ModbusMessage handleWrite(const ModbusMessage & request);
};
} // namespace emsesp
#endif //EMSESP_MODBUS_H_

View File

@@ -0,0 +1,576 @@
#include "modbus.h"
#include "emsdevice.h"
/*
* This file is auto-generated. Do not modify.
*/
// clang-format off
namespace emsesp {
using dt = EMSdevice::DeviceType;
#define REGISTER_MAPPING(device_type, device_value_tag_type, long_name, modbus_register_offset, modbus_register_count) \
{ device_type, device_value_tag_type, long_name[0], modbus_register_offset, modbus_register_count }
// IMPORTANT: This list MUST be ordered by keys "device_type", "device_value_tag_type" and "modbus_register_offset" in this order.
const std::initializer_list<Modbus::EntityModbusInfo> Modbus::modbus_register_mappings = {
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(reset), 0, 1), // reset
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(forceHeatingOff), 1, 1), // heatingoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingActive), 2, 1), // heatingactive
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(tapwaterActive), 3, 1), // tapwateractive
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(selFlowTemp), 4, 1), // selflowtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingPumpMod), 5, 1), // heatingpumpmod
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(outdoorTemp), 6, 1), // outdoortemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(curFlowTemp), 7, 1), // curflowtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(retTemp), 8, 1), // rettemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(switchTemp), 9, 1), // switchtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(sysPress), 10, 1), // syspress
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(boilTemp), 11, 1), // boiltemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(headertemp), 12, 1), // headertemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingActivated), 13, 1), // heatingactivated
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingTemp), 14, 1), // heatingtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingPump), 15, 1), // heatingpump
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pumpModMax), 16, 1), // pumpmodmax
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pumpModMin), 17, 1), // pumpmodmin
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pumpMode), 18, 1), // pumpmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pumpCharacter), 19, 1), // pumpcharacter
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pumpDelay), 20, 1), // pumpdelay
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pumpOnTemp), 21, 1), // pumpontemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(selBurnPow), 22, 1), // selburnpow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(curBurnPow), 23, 1), // curburnpow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnStarts), 24, 2), // burnstarts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnWorkMin), 26, 2), // burnworkmin
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burn2WorkMin), 28, 2), // burn2workmin
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatWorkMin), 30, 2), // heatworkmin
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatStarts), 32, 2), // heatstarts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(UBAuptime), 34, 2), // ubauptime
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(lastCode), 36, 28), // lastcode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(serviceCode), 64, 2), // servicecode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(serviceCodeNumber), 66, 1), // servicecodenumber
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(maintenanceMessage), 67, 2), // maintenancemessage
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(maintenanceType), 69, 1), // maintenance
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(maintenanceTime), 70, 1), // maintenancetime
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(maintenanceDate), 71, 6), // maintenancedate
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(emergencyOps), 77, 1), // emergencyops
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(emergencyTemp), 78, 1), // emergencytemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgTotal), 79, 2), // nrgtotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgHeat), 81, 2), // nrgheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgCool), 83, 2), // nrgcool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(meterTotal), 85, 2), // metertotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(meterComp), 87, 2), // metercomp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(meterEHeat), 89, 2), // metereheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(meterHeat), 91, 2), // meterheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(meterCool), 93, 2), // metercool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(upTimeTotal), 95, 2), // uptimetotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(upTimeControl), 97, 2), // uptimecontrol
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(upTimeCompHeating), 99, 2), // uptimecompheating
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(upTimeCompCooling), 101, 2), // uptimecompcooling
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(upTimeCompPool), 103, 2), // uptimecomppool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(totalCompStarts), 105, 2), // totalcompstarts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingStarts), 107, 2), // heatingstarts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(coolingStarts), 109, 2), // coolingstarts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(poolStarts), 111, 2), // poolstarts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgConsTotal), 113, 2), // nrgconstotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgConsCompTotal), 115, 2), // nrgconscomptotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgConsCompHeating), 117, 2), // nrgconscompheating
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgConsCompCooling), 119, 2), // nrgconscompcooling
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgConsCompPool), 121, 2), // nrgconscomppool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxElecHeatNrgConsTotal), 123, 2), // auxelecheatnrgconstotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxElecHeatNrgConsHeating), 125, 2), // auxelecheatnrgconsheating
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxElecHeatNrgConsPool), 127, 2), // auxelecheatnrgconspool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgSuppTotal), 129, 2), // nrgsupptotal
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgSuppHeating), 131, 2), // nrgsuppheating
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgSuppCooling), 133, 2), // nrgsuppcooling
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgSuppPool), 135, 2), // nrgsupppool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpPower), 137, 1), // hppower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpMaxPower), 138, 1), // hpmaxpower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pvMaxComp), 139, 1), // pvmaxcomp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(powerReduction), 140, 1), // powerreduction
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpSetDiffPress), 141, 1), // hpsetdiffpress
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpCompOn), 142, 1), // hpcompon
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpActivity), 143, 1), // hpactivity
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpBrinePumpSpd), 144, 1), // hpbrinepumpspd
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpSwitchValve), 145, 1), // hpswitchvalve
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpCompSpd), 146, 1), // hpcompspd
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpCircSpd), 147, 1), // hpcircspd
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpBrineIn), 148, 1), // hpbrinein
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpBrineOut), 149, 1), // hpbrineout
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTc0), 150, 1), // hptc0
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTc1), 151, 1), // hptc1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTc3), 152, 1), // hptc3
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTr1), 153, 1), // hptr1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTr3), 154, 1), // hptr3
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTr4), 155, 1), // hptr4
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTr5), 156, 1), // hptr5
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTr6), 157, 1), // hptr6
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTr7), 158, 1), // hptr7
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTl2), 159, 1), // hptl2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpPl1), 160, 1), // hppl1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpPh1), 161, 1), // hpph1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTa4), 162, 1), // hpta4
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpTw1), 163, 1), // hptw1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(poolSetTemp), 164, 1), // poolsettemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hp4wayValve), 165, 1), // hp4way
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpInput1), 166, 1), // hpin1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpIn1Opt), 167, 8), // hpin1opt
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpInput2), 175, 1), // hpin2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpIn2Opt), 176, 8), // hpin2opt
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpInput3), 184, 1), // hpin3
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpIn3Opt), 185, 8), // hpin3opt
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpInput4), 193, 1), // hpin4
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpIn4Opt), 194, 8), // hpin4opt
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(maxHeatComp), 202, 1), // maxheatcomp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(maxHeatHeat), 203, 1), // maxheatheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(manDefrost), 204, 1), // mandefrost
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pvCooling), 205, 1), // pvcooling
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxHeaterOnly), 206, 1), // auxheateronly
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxHeaterOff), 207, 1), // auxheateroff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxHeaterStatus), 208, 1), // auxheaterstatus
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxHeaterLevel), 209, 1), // auxheaterlevel
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxHeaterDelay), 210, 1), // auxheaterdelay
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxMaxLimit), 211, 1), // auxmaxlimit
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxLimitStart), 212, 1), // auxlimitstart
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxHeatMode), 213, 1), // auxheatrmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpHystHeat), 214, 1), // hphystheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpHystCool), 215, 1), // hphystcool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpHystPool), 216, 1), // hphystpool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(silentMode), 217, 1), // silentmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(silentFrom), 218, 1), // silentfrom
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(silentTo), 219, 1), // silentto
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(minTempSilent), 220, 1), // mintempsilent
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(tempParMode), 221, 1), // tempparmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(auxHeatMixValve), 222, 1), // auxheatmix
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(tempDiffHeat), 223, 1), // tempdiffheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(tempDiffCool), 224, 1), // tempdiffcool
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(vp_cooling), 225, 1), // vpcooling
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatCable), 226, 1), // heatcable
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(VC0valve), 227, 1), // vc0valve
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(primePump), 228, 1), // primepump
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(primePumpMod), 229, 1), // primepumpmod
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hp3wayValve), 230, 1), // hp3way
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(elHeatStep1), 231, 1), // elheatstep1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(elHeatStep2), 232, 1), // elheatstep2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(elHeatStep3), 233, 1), // elheatstep3
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpEA0), 234, 1), // hpea0
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpPumpMode), 235, 1), // hppumpmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpFan), 236, 1), // fan
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpShutdown), 237, 1), // shutdown
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpCurrPower), 238, 1), // hpcurrpower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(hpPowerLimit), 239, 1), // hppowerlimit
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pc0Flow), 240, 1), // pc0flow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pc1Flow), 241, 1), // pc1flow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pc1On), 242, 1), // pc1on
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(pc1Rate), 243, 1), // pc1rate
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(exhaustTemp), 244, 1), // exhausttemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnGas), 245, 1), // burngas
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnGas2), 246, 1), // burngas2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(flameCurr), 247, 1), // flamecurr
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(fanWork), 248, 1), // fanwork
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(ignWork), 249, 1), // ignwork
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(oilPreHeat), 250, 1), // oilpreheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnMinPower), 251, 1), // burnminpower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnMaxPower), 252, 1), // burnmaxpower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(burnMinPeriod), 253, 1), // burnminperiod
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(absBurnPow), 254, 1), // absburnpow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatblock), 255, 1), // heatblock
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(boilHystOn), 256, 1), // boilhyston
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(boilHystOff), 257, 1), // boilhystoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(boil2HystOn), 258, 1), // boil2hyston
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(boil2HystOff), 259, 1), // boil2hystoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(curveOn), 260, 1), // curveon
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(curveBase), 261, 1), // curvebase
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(curveEnd), 262, 1), // curveend
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(summertemp), 263, 1), // summertemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nofrostmode), 264, 1), // nofrostmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nofrosttemp), 265, 1), // nofrosttemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(gasMeterHeat), 266, 2), // gasmeterheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nrgHeat2), 268, 2), // nrgheat2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(nomPower), 270, 1), // nompower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(netFlowTemp), 271, 1), // netflowtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatValve), 272, 1), // heatvalve
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(keepWarmTemp), 273, 1), // keepwarmtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(setReturnTemp), 274, 1), // setreturntemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DEVICE_DATA, FL_(heatingOn), 275, 1), // heating
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(nrgWw), 0, 2), // nrg
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(meterWw), 2, 2), // meter
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(upTimeCompWw), 4, 2), // uptimecomp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStartsHp), 6, 2), // startshp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(nrgConsCompWw), 8, 2), // nrgconscomp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(auxElecHeatNrgConsWw), 10, 2), // auxelecheatnrgcons
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(nrgSuppWw), 12, 2), // nrgsupp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(maxHeatDhw), 14, 1), // maxheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwAlternatingOper), 15, 1), // alternatingop
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwAltOpPrioHeat), 16, 1), // altopprioheat
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwAltOpPrioWw), 17, 1), // altopprio
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwComfOffTemp), 18, 1), // comfoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwEcoOffTemp), 19, 1), // ecooff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwEcoPlusOffTemp), 20, 1), // ecoplusoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwComfDiffTemp), 21, 1), // comfdiff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwEcoDiffTemp), 22, 1), // ecodiff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwEcoPlusDiffTemp), 23, 1), // ecoplusdiff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwComfStopTemp), 24, 1), // comfstop
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwEcoStopTemp), 25, 1), // ecostop
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwEcoPlusStopTemp), 26, 1), // ecoplusstop
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(hpCircPumpWw), 27, 1), // hpcircpump
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwtapactivated), 28, 1), // tapactivated
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwSetTemp), 29, 1), // settemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(selRoomTemp), 30, 1), // seltemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwSelTempLow), 31, 1), // seltemplow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwSelTempEco), 32, 1), // tempecoplus
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwSelTempOff), 33, 1), // seltempoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwSelTempSingle), 34, 1), // seltempsingle
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwSolarTemp), 35, 1), // solartemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwType), 36, 1), // type
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwComfort), 37, 1), // comfort
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwComfort1), 38, 1), // comfort1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(flowtempoffset), 39, 1), // flowtempoffset
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwChargeOptimization), 40, 1), // chargeoptimization
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwMaxPower), 41, 1), // maxpower
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwMaxTemp), 42, 1), // maxtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCircPump), 43, 1), // circpump
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwChargeType), 44, 1), // chargetype
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwHystOn), 45, 1), // hyston
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwHystOff), 46, 1), // hystoff
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwDisinfectionTemp), 47, 1), // disinfectiontemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCircMode), 48, 1), // circmode
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCirc), 49, 1), // circ
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCurTemp), 50, 1), // curtemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCurTemp2), 51, 1), // curtemp2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCurFlow), 52, 1), // curflow
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStorageTemp1), 53, 1), // storagetemp1
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStorageTemp2), 54, 1), // storagetemp2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(activated), 55, 1), // activated
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwOneTime), 56, 1), // onetime
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwDisinfecting), 57, 1), // disinfecting
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwCharging), 58, 1), // charging
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwRecharging), 59, 1), // recharging
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwTempOK), 60, 1), // tempok
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwActive), 61, 1), // active
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(ww3wayValve), 62, 1), // 3wayvalve
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwMixerTemp), 63, 1), // mixertemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(cylMiddleTemp), 64, 1), // cylmiddletemp
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwStarts), 65, 2), // starts
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwWorkM), 67, 2), // workm
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(gasMeterWw), 69, 2), // gasmeter
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(nrgWw2), 71, 2), // nrg2
REGISTER_MAPPING(dt::BOILER, TAG_TYPE_DHW, FL_(wwValve), 73, 1), // dhwvalve
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(errorCode), 0, 8), // errorcode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(lastCode), 8, 25), // lastcode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(dateTime), 33, 13), // datetime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(ibaCalIntTemperature), 46, 1), // intoffset
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(floordrystatus), 47, 1), // floordry
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(dampedoutdoortemp), 48, 1), // dampedoutdoortemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(floordrytemp), 49, 1), // floordrytemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(ibaBuildingType), 50, 1), // building
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(ibaMinExtTemperature), 51, 1), // minexttemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(damping), 52, 1), // damping
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(hasSolar), 53, 1), // solar
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(hybridStrategy), 54, 1), // hybridstrategy
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(switchOverTemp), 55, 1), // switchovertemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(energyCostRatio), 56, 1), // energycostratio
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(fossileFactor), 57, 1), // fossilefactor
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(electricFactor), 58, 1), // electricfactor
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(delayBoiler), 59, 1), // delayboiler
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(tempDiffBoiler), 60, 1), // tempdiffboiler
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(pvEnableWw), 61, 1), // pvenabledhw
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(pvRaiseHeat), 62, 1), // pvraiseheat
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(pvLowerCool), 63, 1), // pvlowercool
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(absent), 64, 1), // absent
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(ibaMainDisplay), 65, 1), // display
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(ibaLanguage), 66, 1), // language
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(ibaClockOffset), 67, 1), // clockoffset
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(tempsensor1), 68, 1), // inttemp1
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(tempsensor2), 69, 1), // inttemp2
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(autodst), 70, 1), // autodst
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(backlight), 71, 1), // backlight
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(brightness), 72, 1), // brightness
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(mixingvalves), 73, 1), // mixingvalves
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(heatingPID), 74, 1), // heatingpid
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(preheating), 75, 1), // preheating
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DEVICE_DATA, FL_(vacations), 76, 13), // vacations
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(selRoomTemp), 0, 1), // seltemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(roomTemp), 1, 1), // currtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(haclimate), 2, 1), // haclimate
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(mode), 3, 1), // mode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(modetype), 4, 1), // modetype
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(ecotemp), 5, 1), // ecotemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(manualtemp), 6, 1), // manualtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(comforttemp), 7, 1), // comforttemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(summertemp), 8, 1), // summertemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(designtemp), 9, 1), // designtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(offsettemp), 10, 1), // offsettemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(minflowtemp), 11, 1), // minflowtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(maxflowtemp), 12, 1), // maxflowtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(roominfluence), 13, 1), // roominfluence
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(roominfl_factor), 14, 1), // roominflfactor
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(curroominfl), 15, 1), // curroominfl
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(nofrostmode), 16, 1), // nofrostmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(nofrosttemp), 17, 1), // nofrosttemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(targetflowtemp), 18, 1), // targetflowtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(heatingtype), 19, 1), // heatingtype
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(summersetmode), 20, 1), // summersetmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(hpoperatingmode), 21, 1), // hpoperatingmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(summermode), 22, 1), // summermode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(hpoperatingstate), 23, 1), // hpoperatingstate
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(controlmode), 24, 1), // controlmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(program), 25, 1), // program
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(tempautotemp), 26, 1), // tempautotemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(remoteseltemp), 27, 1), // remoteseltemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(fastheatup), 28, 1), // fastheatup
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(switchonoptimization), 29, 1), // switchonoptimization
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(reducemode), 30, 1), // reducemode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(noreducetemp), 31, 1), // noreducetemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(reducetemp), 32, 1), // reducetemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(wwprio), 33, 1), // dhwprio
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(hpcooling), 34, 1), // hpcooling
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(coolingOn), 35, 1), // coolingon
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(hpmode), 36, 1), // hpmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(dewoffset), 37, 1), // dewoffset
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(roomtempdiff), 38, 1), // roomtempdiff
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(hpminflowtemp), 39, 1), // hpminflowtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(control), 40, 1), // control
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(remotetemp), 41, 1), // remotetemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(remotehum), 42, 1), // remotehum
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(heatondelay), 43, 1), // heatondelay
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(heatoffdelay), 44, 1), // heatoffdelay
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(instantstart), 45, 1), // instantstart
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(boost), 46, 1), // boost
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(boosttime), 47, 1), // boosttime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(coolstart), 48, 1), // coolstart
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(coolondelay), 49, 1), // coolondelay
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(cooloffdelay), 50, 1), // cooloffdelay
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(switchProgMode), 51, 1), // switchprogmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(redthreshold), 52, 1), // redthreshold
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(daytemp), 53, 1), // daytemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(nighttemp2), 54, 1), // nighttemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(holidaytemp), 55, 1), // holidaytemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(holidaymode), 56, 1), // holidaymode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(flowtempoffset), 57, 1), // flowtempoffset
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(holidays), 58, 13), // holidays
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations), 71, 13), // vacations
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(pause), 84, 1), // pause
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(party), 85, 1), // party
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacreducetemp), 86, 1), // vacreducetemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacreducemode), 87, 1), // vacreducemode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(switchtime1), 88, 8), // switchtime1
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(switchtime2), 96, 8), // switchtime2
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(offtemp), 104, 1), // offtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(daylowtemp), 105, 1), // daytemp2
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(daymidtemp), 106, 1), // daytemp3
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(dayhightemp), 107, 1), // daytemp4
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(switchtime), 108, 8), // switchtime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations1), 116, 11), // vacations1
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations2), 127, 11), // vacations2
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations3), 138, 11), // vacations3
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations4), 149, 11), // vacations4
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations5), 160, 11), // vacations5
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations6), 171, 11), // vacations6
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(vacations7), 182, 11), // vacations7
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(reducehours), 193, 1), // reducehours
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(reduceminutes), 194, 1), // reduceminutes
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(heattemp), 195, 1), // heattemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(roomsensor), 196, 1), // roomsensor
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_HC, FL_(heatup), 197, 1), // heatup
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(mode), 0, 1), // mode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwSetTemp), 1, 1), // settemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwSetTempLow), 2, 1), // settemplow
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCircMode), 3, 1), // circmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwChargeDuration), 4, 1), // chargeduration
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCharge), 5, 1), // charge
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwExtra), 6, 1), // extra
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfecting), 7, 1), // disinfecting
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectDay), 8, 1), // disinfectday
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectTime), 9, 1), // disinfecttime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDailyHeating), 10, 1), // dailyheating
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDailyHeatTime), 11, 1), // dailyheattime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwProgMode), 12, 1), // progmode
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwCircProg), 13, 1), // circprog
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwDisinfectHour), 14, 1), // disinfecthour
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwMaxTemp), 15, 1), // maxtemp
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwOneTimeKey), 16, 1), // onetimekey
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwswitchtime), 17, 11), // switchtimeWW
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwcircswitchtime), 28, 8), // circswitchtime
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(holidays), 36, 13), // holidays
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(vacations), 49, 13), // vacations
REGISTER_MAPPING(dt::THERMOSTAT, TAG_TYPE_DHW, FL_(wwWhenModeOff), 62, 1), // whenmodeoff
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(flowTempHc), 0, 1), // flowtemphc
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(valveStatus), 1, 1), // valvestatus
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(flowSetTemp), 2, 1), // flowsettemp
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(wwPumpStatus), 3, 1), // pumpstatus
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(activated), 4, 1), // activated
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(mixerSetTime), 5, 1), // valvesettime
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(flowTempVf), 6, 1), // flowtempvf
REGISTER_MAPPING(dt::MIXER, TAG_TYPE_HC, FL_(flowtempoffset), 7, 1), // flowtempoffset
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collectorTemp), 0, 1), // collectortemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cylBottomTemp), 1, 1), // cylbottomtemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPump), 2, 1), // solarpump
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(pumpWorkTime), 3, 2), // pumpworktime
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cylMaxTemp), 5, 1), // cylmaxtemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collectorShutdown), 6, 1), // collectorshutdown
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cylHeated), 7, 1), // cylheated
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPumpMod), 8, 1), // solarpumpmod
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(pumpMinMod), 9, 1), // pumpminmod
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPumpTurnonDiff), 10, 1), // turnondiff
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPumpTurnoffDiff), 11, 1), // turnoffdiff
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPower), 12, 1), // solarpower
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(energyLastHour), 13, 2), // energylasthour
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(maxFlow), 15, 1), // maxflow
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarIsEnabled), 16, 1), // solarenabled
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cylMiddleTemp), 17, 1), // cylmiddletemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(retHeatAssist), 18, 1), // retheatassist
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(m1Valve), 19, 1), // heatassistvalve
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collector2Temp), 20, 1), // collector2temp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(m1Power), 21, 1), // heatassistpower
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPump2), 22, 1), // solarpump2
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPump2Mod), 23, 1), // solarpump2mod
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cyl2BottomTemp), 24, 1), // cyl2bottomtemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cyl3BottomTemp), 25, 1), // cyl3bottomtemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cylTopTemp), 26, 1), // cyltoptemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatExchangerTemp), 27, 1), // heatexchangertemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cylPumpMod), 28, 1), // cylpumpmod
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(valveStatus), 29, 1), // valvestatus
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(vs1Status), 30, 1), // vs1status
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(vs3Status), 31, 1), // vs3status
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(transferPump), 32, 1), // transferpump
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(transferPumpMod), 33, 1), // transferpumpmod
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collectorMaxTemp), 34, 1), // collectormaxtemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collectorMinTemp), 35, 1), // collectormintemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(energyToday), 36, 2), // energytoday
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(energyTotal), 38, 2), // energytotal
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(pump2WorkTime), 40, 2), // pump2worktime
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(m1WorkTime), 42, 2), // m1worktime
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatTransferSystem), 44, 1), // heattransfersystem
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(externalCyl), 45, 1), // externalcyl
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(thermalDisinfect), 46, 1), // thermaldisinfect
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatMetering), 47, 1), // heatmetering
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(activated), 48, 1), // activated
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPumpMode), 49, 1), // solarpumpmode
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPumpKick), 50, 1), // solarpumpkick
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(plainWaterMode), 51, 1), // plainwatermode
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(doubleMatchFlow), 52, 1), // doublematchflow
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(pump2MinMod), 53, 1), // pump2minmod
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPump2TurnonDiff), 54, 1), // turnondiff2
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPump2TurnoffDiff), 55, 1), // turnoffdiff2
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(solarPump2Kick), 56, 1), // pump2kick
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(climateZone), 57, 1), // climatezone
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collector1Area), 58, 1), // collector1area
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collector1Type), 59, 1), // collector1type
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collector2Area), 60, 1), // collector2area
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(collector2Type), 61, 1), // collector2type
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(cylPriority), 62, 1), // cylpriority
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatCntFlowTemp), 63, 1), // heatcntflowtemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatCntRetTemp), 64, 1), // heatcntrettemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatCnt), 65, 1), // heatcnt
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(swapFlowTemp), 66, 1), // swapflowtemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(swapRetTemp), 67, 1), // swaprettemp
REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DHW, FL_(wwMinTemp), 0, 1), // mintemp
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(airHumidity), 0, 1), // airhumidity
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(dewTemperature), 1, 1), // dewtemperature
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(curFlowTemp), 2, 1), // curflowtemp
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(retTemp), 3, 1), // rettemp
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(sysRetTemp), 4, 1), // sysrettemp
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTa4), 5, 1), // hpta4
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTr1), 6, 1), // hptr1
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTr3), 7, 1), // hptr3
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTr4), 8, 1), // hptr4
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTr5), 9, 1), // hptr5
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTr6), 10, 1), // hptr6
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpTl2), 11, 1), // hptl2
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpPl1), 12, 1), // hppl1
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpPh1), 13, 1), // hpph1
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(heatingPumpMod), 14, 1), // heatingpumpmod
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hpCompSpd), 15, 1), // hpcompspd
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(hybridStrategy), 16, 1), // hybridstrategy
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(lowNoiseMode), 17, 1), // lownoisemode
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(lowNoiseStart), 18, 1), // lownoisestart
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(lowNoiseStop), 19, 1), // lownoisestop
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(energyPriceGas), 20, 1), // energypricegas
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(energyPriceEl), 21, 1), // energypriceel
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(energyPricePV), 22, 1), // energyfeedpv
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(switchOverTemp), 23, 1), // switchovertemp
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(airPurgeMode), 24, 1), // airpurgemode
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(heatPumpOutput), 25, 1), // heatpumpoutput
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(coolingCircuit), 26, 1), // coolingcircuit
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(compStartMod), 27, 1), // compstartmod
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(heatDrainPan), 28, 1), // heatdrainpan
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(heatCable), 29, 1), // heatcable
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(nrgTotal), 30, 2), // nrgtotal
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(nrgHeat), 32, 2), // nrgheat
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(meterTotal), 34, 2), // metertotal
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(meterComp), 36, 2), // metercomp
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(meterEHeat), 38, 2), // metereheat
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(meterHeat), 40, 2), // meterheat
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(heatingStarts), 42, 2), // heatingstarts
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(fuelHeat), 44, 2), // fuelheat
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(elHeat), 46, 2), // elheat
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(elGenHeat), 48, 2), // elgenheat
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DHW, FL_(hybridDHW), 0, 1), // hybriddhw
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DHW, FL_(nrgWw), 1, 2), // nrg
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DHW, FL_(meterWw), 3, 2), // meter
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DHW, FL_(wwStartsHp), 5, 2), // startshp
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DHW, FL_(fuelDhw), 7, 2), // fueldhw
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DHW, FL_(elDhw), 9, 2), // eldhw
REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DHW, FL_(elGenDhw), 11, 2), // elgendhw
REGISTER_MAPPING(dt::SWITCH, TAG_TYPE_DEVICE_DATA, FL_(activated), 0, 1), // activated
REGISTER_MAPPING(dt::SWITCH, TAG_TYPE_DEVICE_DATA, FL_(flowTempHc), 1, 1), // flowtemphc
REGISTER_MAPPING(dt::SWITCH, TAG_TYPE_DEVICE_DATA, FL_(status), 2, 1), // status
REGISTER_MAPPING(dt::CONTROLLER, TAG_TYPE_DEVICE_DATA, FL_(dateTime), 0, 13), // datetime
REGISTER_MAPPING(dt::ALERT, TAG_TYPE_DEVICE_DATA, FL_(setFlowTemp), 0, 1), // setflowtemp
REGISTER_MAPPING(dt::ALERT, TAG_TYPE_DEVICE_DATA, FL_(setBurnPow), 1, 1), // setburnpow
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(flowTempVf), 0, 1), // flowtempvf
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(input), 1, 1), // input
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(outPower), 2, 1), // outpow
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(setPower), 3, 1), // setpower
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(setPoint), 4, 1), // setpoint
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(minV), 5, 1), // minv
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(maxV), 6, 1), // maxv
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(minT), 7, 1), // mint
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(maxT), 8, 1), // maxt
REGISTER_MAPPING(dt::EXTENSION, TAG_TYPE_DEVICE_DATA, FL_(mode), 9, 1), // mode
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(outFresh), 0, 1), // outfresh
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(inFresh), 1, 1), // infresh
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(outEx), 2, 1), // outexhaust
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(inEx), 3, 1), // inexhaust
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(ventInSpeed), 4, 1), // ventinspeed
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(ventOutSpeed), 5, 1), // ventoutspeed
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(ventMode), 6, 1), // ventmode
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(airquality), 7, 1), // airquality
REGISTER_MAPPING(dt::VENTILATION, TAG_TYPE_DEVICE_DATA, FL_(airHumidity), 8, 1), // airhumidity
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(selRoomTemp), 0, 1), // seltemp
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwTemp), 1, 1), // temp
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwCurTemp2), 2, 1), // curtemp2
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(hydrTemp), 3, 1), // hydrTemp
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwPump), 4, 1), // pump
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(flowtempoffset), 5, 1), // flowtempoffset
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwHystOn), 6, 1), // hyston
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwHystOff), 7, 1), // hystoff
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwDisinfectionTemp), 8, 1), // disinfectiontemp
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwCirc), 9, 1), // circ
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwCircMode), 10, 1), // circmode
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwTempStatus), 11, 1), // tempstatus
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwMaxTemp), 12, 1), // maxtemp
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwDiffTemp), 13, 1), // difftemp
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwRedTemp), 14, 1), // redtemp
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwRequiredTemp), 15, 1), // requiredtemp
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwStorageTemp1), 16, 1), // storagetemp1
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwColdTemp), 17, 1), // coldtemp
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwTemp5), 18, 1), // temp5
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(retTemp), 19, 1), // rettemp
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwHotTemp), 20, 1), // hottemp
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwDailyTemp), 21, 1), // dailytemp
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwCircTc), 22, 1), // circtc
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwKeepWarm), 23, 1), // keepwarm
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwStatus2), 24, 1), // status2
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwPumpMod), 25, 1), // pumpmod
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(wwFlow), 26, 1), // flow
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(valveReturn), 27, 1), // valvereturn
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(deltaTRet), 28, 1), // deltatret
REGISTER_MAPPING(dt::WATER, TAG_TYPE_DHW, FL_(errorDisp), 29, 1), // errordisp
};
} // namespace emsesp
// clang-format off

1444
src/core/mqtt.cpp Normal file

File diff suppressed because it is too large Load Diff

317
src/core/mqtt.h Normal file
View File

@@ -0,0 +1,317 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_MQTT_H_
#define EMSESP_MQTT_H_
#include <espMqttClient.h>
#include "helpers.h"
#include "system.h"
#include "console.h"
#include "command.h"
#include "emsdevicevalue.h"
using uuid::console::Shell;
namespace emsesp {
using mqtt_sub_function_p = std::function<bool(const char * message)>;
class Mqtt {
public:
enum discoveryType : uint8_t { HOMEASSISTANT = 0, DOMOTICZ, DOMOTICZ_LATEST };
// SINGLE_SHORT (1) and MULTI_SHORT (2) are the latest. Default is SINGLE_SHORT.
// SINGLE_LONG (0) is v3.4 only
// SINGLE_OLD (3) and MULTI_OLD (4) are for backwards compatibility with the older v3.6 style. https://github.com/emsesp/EMS-ESP32/issues/1714
enum entityFormat : uint8_t { SINGLE_LONG = 0, SINGLE_SHORT, MULTI_SHORT, SINGLE_OLD, MULTI_OLD };
void loop();
void start();
static void load_settings();
void set_publish_time_boiler(uint16_t publish_time);
void set_publish_time_thermostat(uint16_t publish_time);
void set_publish_time_solar(uint16_t publish_time);
void set_publish_time_mixer(uint16_t publish_time);
void set_publish_time_water(uint16_t publish_time);
void set_publish_time_other(uint16_t publish_time);
void set_publish_time_sensor(uint16_t publish_time);
void set_publish_time_heartbeat(uint16_t publish_time);
bool get_publish_onchange(uint8_t device_type);
enum Operation : uint8_t { PUBLISH, SUBSCRIBE, UNSUBSCRIBE };
enum NestedFormat : uint8_t { NESTED = 1, SINGLE };
static constexpr uint8_t MQTT_TOPIC_MAX_SIZE = 128; // fixed, not a user setting anymore
static constexpr uint16_t MQTT_QUEUE_MAX_SIZE = 300;
static void on_connect();
static void on_disconnect(espMqttClientTypes::DisconnectReason reason);
static void on_message(const char * topic, const uint8_t * payload, size_t len);
static void subscribe(const uint8_t device_type, const std::string & topic, mqtt_sub_function_p cb);
static void subscribe(const std::string & topic);
static void resubscribe();
static bool queue_publish(const std::string & topic, const std::string & payload);
static bool queue_publish(const char * topic, const char * payload);
static bool queue_publish(const std::string & topic, const JsonObjectConst payload);
static bool queue_publish(const char * topic, const JsonObjectConst payload);
static bool queue_publish(const char * topic, const std::string & payload);
static bool queue_publish_retain(const std::string & topic, const JsonObjectConst payload, const bool retain);
static bool queue_publish_retain(const char * topic, const std::string & payload, const bool retain);
static bool queue_publish_retain(const char * topic, const JsonObjectConst payload, const bool retain);
static bool queue_ha(const char * topic, const JsonObjectConst payload);
static bool queue_remove_topic(const char * topic);
static bool publish_ha_sensor_config(DeviceValue & dv, const char * model, const char * brand, const bool remove, const bool create_device_config = false);
static bool publish_ha_sensor_config(uint8_t type,
int8_t tag,
const char * const fullname,
const char * const en_name,
const uint8_t device_type,
const char * const entity,
const uint8_t uom,
const bool remove,
const bool has_cmd,
const char * const ** options,
uint8_t options_size,
const int16_t dv_set_min,
const uint32_t dv_set_max,
const int8_t num_op,
const JsonObjectConst dev_json);
static bool publish_system_ha_sensor_config(uint8_t type, const char * name, const char * entity, const uint8_t uom);
static bool publish_ha_climate_config(const int8_t tag, const bool has_roomtemp, const bool remove = false, const int16_t min = 5, const uint32_t max = 30);
static void show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_type);
static void show_mqtt(uuid::console::Shell & shell);
static void ha_status();
#if defined(EMSESP_TEST)
void incoming(const char * topic, const char * payload = ""); // for testing only
#endif
static bool connected() {
return mqttClient_ ? mqttClient_->connected() : false;
}
static MqttClient * client() {
return mqttClient_;
}
static bool enabled() {
return mqtt_enabled_;
}
static void enabled(bool mqtt_enabled) {
mqtt_enabled_ = mqtt_enabled;
}
static std::string base() {
return mqtt_base_;
}
static std::string basename() {
return mqtt_basename_;
}
// returns the discovery MQTT topic prefix and adds a /
static std::string discovery_prefix() {
if (discovery_prefix_.empty()) {
return std::string{};
}
return discovery_prefix_ + "/";
}
static uint32_t publish_count() {
return mqtt_message_id_;
}
static uint32_t publish_fails() {
return mqtt_publish_fails_;
}
static uint32_t publish_queued() {
return queuecount_;
}
static uint8_t connect_count() {
return connectcount_;
}
static void reset_mqtt();
static bool is_nested() {
return nested_format_ == NestedFormat::NESTED;
}
static uint8_t entity_format() {
return entity_format_;
}
static void entity_format(uint8_t n) {
entity_format_ = n;
}
static uint8_t discovery_type() {
return discovery_type_;
}
static void nested_format(uint8_t nested_format) {
nested_format_ = nested_format;
}
static bool publish_single() {
return mqtt_enabled_ && publish_single_;
}
static bool publish_single2cmd() {
return publish_single2cmd_;
}
static void publish_single(bool publish_single) {
publish_single_ = publish_single;
}
static bool ha_enabled() {
return mqtt_enabled_ && ha_enabled_;
}
static void ha_enabled(bool ha_enabled) {
ha_enabled_ = ha_enabled;
}
static bool ha_climate_reset() {
return ha_climate_reset_;
}
static void ha_climate_reset(bool reset) {
ha_climate_reset_ = reset;
}
static std::string get_response() {
return lastresponse_;
}
static void clear_response() {
lastresponse_.clear();
}
void set_qos(uint8_t mqtt_qos) const {
mqtt_qos_ = mqtt_qos;
}
void set_retain(bool mqtt_retain) const {
mqtt_retain_ = mqtt_retain;
}
static std::string tag_to_topic(uint8_t device_type, int8_t tag);
static void add_ha_uom(JsonObject doc, const uint8_t type, const uint8_t uom, const char * entity = nullptr);
static void add_ha_sections_to_doc(const char * name,
const char * state_t,
JsonDocument & config,
const bool is_first = false,
const char * cond1 = nullptr,
const char * cond2 = nullptr,
const char * negcond = nullptr);
static void add_ha_bool(JsonDocument & config);
private:
static uuid::log::Logger logger_;
static MqttClient * mqttClient_;
static uint32_t mqtt_message_id_;
static bool queue_message(const uint8_t operation, const std::string & topic, const std::string & payload, const bool retain);
static bool queue_publish_message(const std::string & topic, const std::string & payload, const bool retain);
static void queue_subscribe_message(const std::string & topic);
static void queue_unsubscribe_message(const std::string & topic);
void on_publish(uint16_t packetId) const;
// function handlers for MQTT subscriptions
struct MQTTSubFunction {
uint8_t device_type_; // which device type, from DeviceType::
const std::string topic_; // short topic name
mqtt_sub_function_p mqtt_subfunction_; // can be empty
// replaced &&topic with &topic in 3.7.0-dev.43, so we prevent the std:move later
MQTTSubFunction(uint8_t device_type, const std::string & topic, mqtt_sub_function_p mqtt_subfunction)
: device_type_(device_type)
, topic_(topic)
, mqtt_subfunction_(mqtt_subfunction) {
}
};
static std::vector<MQTTSubFunction> mqtt_subfunctions_; // list of mqtt subscribe callbacks for all devices
uint32_t last_publish_boiler_ = 0;
uint32_t last_publish_thermostat_ = 0;
uint32_t last_publish_solar_ = 0;
uint32_t last_publish_mixer_ = 0;
uint32_t last_publish_water_ = 0;
uint32_t last_publish_other_ = 0;
uint32_t last_publish_sensor_ = 0;
uint32_t last_publish_heartbeat_ = 0;
// uint32_t last_publish_queue_ = 0;
static bool connecting_;
static bool initialized_;
static uint32_t mqtt_publish_fails_;
static uint16_t queuecount_;
static uint8_t connectcount_;
static bool ha_climate_reset_;
static std::string lasttopic_;
static std::string lastpayload_;
static std::string lastresponse_;
// settings, copied over
static std::string mqtt_base_;
static std::string mqtt_basename_;
static uint8_t mqtt_qos_;
static bool mqtt_retain_;
static uint32_t publish_time_;
static uint32_t publish_time_boiler_;
static uint32_t publish_time_thermostat_;
static uint32_t publish_time_solar_;
static uint32_t publish_time_mixer_;
static uint32_t publish_time_water_;
static uint32_t publish_time_other_;
static uint32_t publish_time_sensor_;
static uint32_t publish_time_heartbeat_;
static bool mqtt_enabled_;
static bool ha_enabled_;
static uint8_t nested_format_;
static uint8_t entity_format_;
static std::string discovery_prefix_;
static uint8_t discovery_type_;
static bool publish_single_;
static bool publish_single2cmd_;
static bool send_response_;
};
} // namespace emsesp
#endif

426
src/core/roomcontrol.cpp Normal file
View File

@@ -0,0 +1,426 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "roomcontrol.h"
namespace emsesp {
// init statics
bool Roomctrl::switch_off_[HCS] = {false, false, false, false};
uint32_t Roomctrl::send_time_[HCS] = {0, 0, 0, 0};
uint32_t Roomctrl::receive_time_[HCS] = {0, 0, 0, 0};
int16_t Roomctrl::remotetemp_[HCS] = {EMS_VALUE_INT16_NOTSET, EMS_VALUE_INT16_NOTSET, EMS_VALUE_INT16_NOTSET, EMS_VALUE_INT16_NOTSET};
uint8_t Roomctrl::remotehum_[HCS] = {EMS_VALUE_UINT8_NOTSET, EMS_VALUE_UINT8_NOTSET, EMS_VALUE_UINT8_NOTSET, EMS_VALUE_UINT8_NOTSET};
uint8_t Roomctrl::sendtype_[HCS] = {SendType::TEMP, SendType::TEMP, SendType::TEMP, SendType::TEMP};
uint8_t Roomctrl::type_[HCS] = {RemoteType::NONE, RemoteType::NONE, RemoteType::NONE, RemoteType::NONE};
uint32_t Roomctrl::timeout_ = 0;
/**
* set the temperature,
*/
void Roomctrl::set_timeout(uint8_t t) {
timeout_ = t * 3600000; // ms
}
void Roomctrl::set_remotetemp(const uint8_t type, const uint8_t hc, const int16_t temp) {
if (!type_[hc] && !type) {
return;
}
if (remotetemp_[hc] != EMS_VALUE_INT16_NOTSET && temp == EMS_VALUE_INT16_NOTSET) { // switch remote off
remotetemp_[hc] = EMS_VALUE_INT16_NOTSET;
switch_off_[hc] = true;
send_time_[hc] = uuid::get_uptime() - SEND_INTERVAL; // send now
sendtype_[hc] = SendType::TEMP;
return;
}
if (hc >= HCS || !type) {
return;
}
if (remotetemp_[hc] != temp) {
send_time_[hc] = uuid::get_uptime() - SEND_INTERVAL; // send now
sendtype_[hc] = SendType::TEMP;
}
type_[hc] = type;
remotetemp_[hc] = temp;
receive_time_[hc] = uuid::get_uptime();
}
// set humidity for RC100H emulation
void Roomctrl::set_remotehum(const uint8_t type, const uint8_t hc, const int8_t hum) {
if (hc >= HCS || type != type_[hc]) {
return;
}
if (remotehum_[hc] != hum) {
send_time_[hc] = uuid::get_uptime() - SEND_INTERVAL; // send now
sendtype_[hc] = SendType::HUMI;
}
remotehum_[hc] = hum;
}
uint8_t Roomctrl::get_hc(uint8_t addr) {
addr &= 0x7F;
if (addr >= 0x40 && addr <= 0x44 && type_[addr - 0x40] == SENSOR) {
return addr - 0x40; // SENSOR
} else if (addr >= 0x38 && addr <= 0x3B
&& (type_[addr - 0x38] == RC100H || type_[addr - 0x38] == RC200 || type_[addr - 0x38] == RC100 || type_[addr - 0x38] == RT800)) {
return addr - 0x38; // RC100H, RC200
} else if (addr >= 0x18 && addr <= 0x1B && (type_[addr - 0x18] == RC20 || type_[addr - 0x18] == FB10)) {
return addr - 0x18; // RC20, FB10
}
return 0xFF; // invalid
}
/**
* if remote control is active send the temperature every 15 seconds
*/
void Roomctrl::send(uint8_t addr) {
if (addr & 0x80) {
return;
}
uint8_t hc = get_hc(addr);
// check address, reply only on addresses 0x18..0x1B or 0x40..0x43
if (hc >= HCS) {
return;
}
// no reply if the temperature is not set
if (!switch_off_[hc] && remotetemp_[hc] == EMS_VALUE_INT16_NOTSET && remotehum_[hc] == EMS_VALUE_UINT8_NOTSET) {
return;
}
if (!switch_off_[hc] && timeout_ && (uuid::get_uptime() - receive_time_[hc]) > timeout_) {
remotetemp_[hc] = EMS_VALUE_INT16_NOTSET;
switch_off_[hc] = true;
sendtype_[hc] = SendType::TEMP;
EMSESP::logger().warning("remotetemp timeout hc%d, stop sending roomtemperature to thermostat", hc);
}
if (switch_off_[hc] || (uuid::get_uptime() - send_time_[hc]) > SEND_INTERVAL) { // check interval
if (type_[hc] == RC100H || type_[hc] == RT800) {
if (sendtype_[hc] == SendType::HUMI) { // send humidity
if (switch_off_[hc]) {
remotehum_[hc] = EMS_VALUE_UINT8_NOTSET;
}
send_time_[hc] = uuid::get_uptime();
humidity(addr, 0x10, hc);
sendtype_[hc] = SendType::TEMP;
} else { // temperature telegram
if (remotehum_[hc] != EMS_VALUE_UINT8_NOTSET) {
sendtype_[hc] = SendType::HUMI;
} else {
send_time_[hc] = uuid::get_uptime();
}
temperature(addr, 0x10, hc); // send to master-thermostat
}
} else if (type_[hc] == RC200 || type_[hc] == RC100 || type_[hc] == FB10) {
send_time_[hc] = uuid::get_uptime();
temperature(addr, 0x10, hc); // send to master-thermostat (https://github.com/emsesp/EMS-ESP32/issues/336)
} else { // type==RC20 or SENSOR
send_time_[hc] = uuid::get_uptime();
temperature(addr, 0x00, hc); // send to all
}
if (remotehum_[hc] == EMS_VALUE_UINT8_NOTSET && switch_off_[hc]) {
switch_off_[hc] = false;
type_[hc] = RemoteType::NONE;
}
} else {
// acknowledge every poll
EMSuart::send_poll(addr | EMSbus::ems_mask());
}
}
/**
* check if there is a message for the remote room controller
*/
void Roomctrl::check(uint8_t addr, const uint8_t * data, const uint8_t length) {
uint8_t hc = get_hc(addr);
if (hc >= HCS || length < 5) {
return;
}
if (type_[hc] == SENSOR) {
return;
}
// no reply if the temperature is not set
if (remotetemp_[hc] == EMS_VALUE_INT16_NOTSET) {
return;
}
// reply to writes with write nack byte
if ((addr & 0x80) == 0) { // it's a write to us
ack_write(); // accept writes, don't care.
return;
}
addr &= 0x7F;
// reads: for now we only reply to version and remote temperature
// empty message back if temperature not set or unknown message type
if (data[2] == EMSdevice::EMS_TYPE_VERSION) {
version(addr, data[0], hc);
} else if (data[2] == 0xAF && data[3] == 0) {
temperature(addr, data[0], hc);
} else if (length == 6) { // all other ems queries
unknown(addr, data[0], data[2], data[3]);
} else if (length == 8 && data[2] == 0xFF && data[3] == 0 && data[5] == 0 && data[6] == 0x23) { // Junkers
temperature(addr, data[0], hc);
} else if (length == 8 && data[2] == 0xFF && data[3] == 0 && data[5] == 3 && data[6] == 0x2B + hc) { // EMS+ temperature
temperature(addr, data[0], hc);
} else if (length == 8 && data[2] == 0xFF && data[3] == 0 && data[5] == 3 && data[6] == 0x7B + hc && remotehum_[hc] != EMS_VALUE_UINT8_NOTSET) { // EMS+ humidity
humidity(addr, data[0], hc);
} else if (length == 8 && data[2] == 0xFF) { // ems+ query
unknown(addr, data[0], data[3], data[5], data[6]);
} else if (data[2] == 0xF7) { // ems+ query with 3 bytes type src dst 7F offset len=FF FF HIGH LOW
replyF7(addr, data[0], data[3], data[5], data[6], data[7], hc);
} else if (length == 8) {
unknown(addr, data[0], data[3], data[5], data[6]);
}
}
/**
* send version info
*/
void Roomctrl::version(uint8_t addr, uint8_t dst, uint8_t hc) {
uint8_t data[20];
data[0] = addr | EMSbus::ems_mask();
data[1] = dst & 0x7F;
data[2] = 0x02;
data[3] = 0;
data[4] = type_[hc]; // set RC20 id 113, Ver 02.01 or Junkers FB10 id 109, Ver 16.05, RC100H id 200 ver 40.04
if (type_[hc] == RC20) {
data[5] = 2; // version 2.01
data[6] = 1;
data[7] = EMSbus::calculate_crc(data, 7); // append CRC
EMSuart::transmit(data, 8);
return;
} else if (type_[hc] == FB10) {
data[5] = 16; // version 16.05
data[6] = 5;
data[7] = 0;
data[8] = 0;
data[9] = 0;
data[10] = 0;
data[11] = 0;
data[12] = 0;
data[13] = 0;
data[14] = EMSbus::calculate_crc(data, 14); // append CRC
EMSuart::transmit(data, 15);
return;
} else if (type_[hc] == RC200) {
data[5] = 32; // version 32.02 see #1611
data[6] = 2;
data[7] = 0;
data[8] = 0xFF;
data[9] = 0;
data[10] = 0;
data[11] = 0;
data[12] = 0;
data[13] = 0;
data[14] = EMSbus::calculate_crc(data, 14); // append CRC
EMSuart::transmit(data, 15);
return;
} else if (type_[hc] == RC100H) {
data[5] = 40; // version 40.04
data[6] = 4;
data[7] = 0;
data[8] = 0xFF;
data[9] = EMSbus::calculate_crc(data, 9); // append CRC
EMSuart::transmit(data, 10);
return;
} else if (type_[hc] == RC100) {
data[5] = 40; // version 40.03
data[6] = 3;
data[7] = 0;
data[8] = 0xFF;
data[9] = EMSbus::calculate_crc(data, 9); // append CRC
EMSuart::transmit(data, 10);
return;
} else if (type_[hc] == RT800) {
data[5] = 21; // version 21.03
data[6] = 3;
data[7] = EMSbus::calculate_crc(data, 7); // append CRC
EMSuart::transmit(data, 8);
return;
}
}
/**
* unknown message id, we reply with empty message
*/
void Roomctrl::unknown(uint8_t addr, uint8_t dst, uint8_t type, uint8_t offset) {
uint8_t data[10];
data[0] = addr | EMSbus::ems_mask();
data[1] = dst & 0x7F;
data[2] = type;
data[3] = offset;
data[4] = EMSbus::calculate_crc(data, 4); // append CRC
EMSuart::transmit(data, 5);
}
void Roomctrl::unknown(uint8_t addr, uint8_t dst, uint8_t offset, uint8_t typeh, uint8_t typel) {
uint8_t data[10];
data[0] = addr | EMSbus::ems_mask();
data[1] = dst & 0x7F;
data[2] = 0xFF;
data[3] = offset;
data[4] = typeh;
data[5] = typel;
data[6] = EMSbus::calculate_crc(data, 6); // append CRC
EMSuart::transmit(data, 7);
}
/**
* send the room temperature in message 0xAF
*/
void Roomctrl::temperature(uint8_t addr, uint8_t dst, uint8_t hc) {
uint8_t data[14];
data[0] = addr | EMSbus::ems_mask();
data[1] = dst & 0x7F;
if (type_[hc] == RC20) { // RC20, telegram 0xAF
data[2] = 0xAF;
data[3] = 0;
data[4] = (uint8_t)(remotetemp_[hc] >> 8);
data[5] = (uint8_t)(remotetemp_[hc] & 0xFF);
data[6] = 0;
data[7] = EMSbus::calculate_crc(data, 7); // append CRC
EMSuart::transmit(data, 8);
} else if (type_[hc] == FB10) { // Junkers FB10, telegram 0x0123
data[2] = 0xFF;
data[3] = 0;
data[4] = 0;
data[5] = 0x23; // fixed for all hc
data[6] = (uint8_t)(remotetemp_[hc] >> 8);
data[7] = (uint8_t)(remotetemp_[hc] & 0xFF);
data[8] = EMSbus::calculate_crc(data, 8); // append CRC
EMSuart::transmit(data, 9);
} else if (type_[hc] == RC200) { // RC200, telegram 42B, ff
data[2] = 0xFF;
data[3] = 0;
data[4] = 3;
data[5] = 0x2B + hc;
data[6] = (uint8_t)(remotetemp_[hc] >> 8);
data[7] = (uint8_t)(remotetemp_[hc] & 0xFF);
uint16_t t1 = remotetemp_[hc] * 10 + 3;
data[8] = (uint8_t)(t1 >> 8);
data[9] = (uint8_t)(t1 & 0xFF);
data[10] = 1; // not sure what this is and if we need it, maybe mode?
data[11] = EMSbus::calculate_crc(data, 11); // append CRC
EMSuart::transmit(data, 12);
} else if (type_[hc] == RC100H || type_[hc] == RC100) { // RC100H, telegram 42B, ff
data[2] = 0xFF;
data[3] = 0;
data[4] = 3;
data[5] = 0x2B + hc;
data[6] = (uint8_t)(remotetemp_[hc] >> 8);
data[7] = (uint8_t)(remotetemp_[hc] & 0xFF);
data[8] = EMSbus::calculate_crc(data, 8); // append CRC
EMSuart::transmit(data, 9);
} else if (type_[hc] == SENSOR) { // wireless sensor, broadcast id 435
data[2] = 0xFF;
data[3] = 0;
data[4] = 3;
data[5] = 0x35 + hc;
data[6] = (uint8_t)(remotetemp_[hc] >> 8);
data[7] = (uint8_t)(remotetemp_[hc] & 0xFF);
data[8] = EMSbus::calculate_crc(data, 8); // append CRC
EMSuart::transmit(data, 9);
} else if (type_[hc] == RT800) { // RC200, telegram 42B, ff
data[2] = 0xFF;
data[3] = 0;
data[4] = 3;
data[5] = 0x2B + hc;
data[6] = (uint8_t)(remotetemp_[hc] >> 8);
data[7] = (uint8_t)(remotetemp_[hc] & 0xFF);
uint16_t t1 = remotetemp_[hc] * 10 + 3;
data[8] = (uint8_t)(t1 >> 8);
data[9] = (uint8_t)(t1 & 0xFF);
data[10] = 1; // not sure what this is and if we need it, maybe mode?
data[11] = 9; // not sure what this is and if we need it, maybe mode?
data[12] = EMSbus::calculate_crc(data, 12); // append CRC
EMSuart::transmit(data, 13);
}
}
// send telegram 0x047B only for RC100H
void Roomctrl::humidity(uint8_t addr, uint8_t dst, uint8_t hc) {
uint8_t data[11];
data[0] = addr | EMSbus::ems_mask();
data[1] = dst & 0x7F;
uint16_t dew = calc_dew(remotetemp_[hc], remotehum_[hc]);
data[2] = 0xFF;
data[3] = 0;
data[4] = 3;
data[5] = 0x7B + hc;
data[6] = dew == EMS_VALUE_INT16_NOTSET ? EMS_VALUE_INT8_NOTSET : (uint8_t)((dew + 5) / 10);
data[7] = remotehum_[hc];
data[8] = (uint8_t)(dew << 8);
data[9] = (uint8_t)(dew & 0xFF);
data[10] = EMSbus::calculate_crc(data, 10); // append CRC
EMSuart::transmit(data, 11);
}
/**
* send a nack if someone want to write to us.
*/
void Roomctrl::nack_write() {
uint8_t data[1];
data[0] = TxService::TX_WRITE_FAIL;
EMSuart::transmit(data, 1);
}
/**
* send a ack if someone want to write to us.
*/
void Roomctrl::ack_write() {
uint8_t data[1];
data[0] = TxService::TX_WRITE_SUCCESS;
EMSuart::transmit(data, 1);
}
void Roomctrl::replyF7(uint8_t addr, uint8_t dst, uint8_t offset, uint8_t typehh, uint8_t typeh, uint8_t typel, uint8_t hc) {
uint8_t data[12];
data[0] = addr | EMSbus::ems_mask();
data[1] = dst & 0x7F;
data[2] = 0xF7;
data[3] = offset;
data[4] = typehh;
data[5] = typeh;
data[6] = typel;
if (typehh == 0x02) {
if (type_[hc] == RC200 || type_[hc] == FB10) {
data[7] = 0xFF;
data[8] = 0x01;
} else {
data[7] = 0x0F;
data[8] = 0x00;
}
} else {
data[7] = 0;
data[8] = 0;
}
data[9] = EMSbus::calculate_crc(data, 9); // append CRC
EMSuart::transmit(data, 10);
}
int16_t Roomctrl::calc_dew(int16_t temp, uint8_t humi) {
if (humi == EMS_VALUE_UINT8_NOTSET || temp == EMS_VALUE_INT16_NOTSET) {
return EMS_VALUE_INT16_NOTSET;
}
const float k2 = 17.62;
const float k3 = 243.12;
const float t = (float)temp / 10;
const float h = (float)humi / 100;
int16_t dt = (10 * k3 * (((k2 * t) / (k3 + t)) + log(h)) / (((k2 * k3) / (k3 + t)) - log(h)));
return dt;
}
} // namespace emsesp

67
src/core/roomcontrol.h Normal file
View File

@@ -0,0 +1,67 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_ROOMCONTROL_H
#define EMSESP_ROOMCONTROL_H
#include "emsesp.h"
namespace emsesp {
class Roomctrl {
public:
// Product-Id of the remote
enum RemoteType : uint8_t { NONE = 0, RC20 = 113, FB10 = 109, RC100H = 200, SENSOR = 0x40, RC200 = 157, RC100 = 165, RT800 = 3 };
static void send(uint8_t addr);
static void check(uint8_t addr, const uint8_t * data, const uint8_t length);
static void set_remotetemp(const uint8_t type, const uint8_t hc, const int16_t temp);
static void set_remotehum(const uint8_t type, const uint8_t hc, const int8_t hum);
static bool is_remote(const uint8_t hc) {
return (hc < 4 && remotetemp_[hc] != EMS_VALUE_INT16_NOTSET);
}
static void set_timeout(uint8_t t);
private:
static constexpr uint32_t SEND_INTERVAL = 15000; // 15 sec
static constexpr uint8_t HCS = 4; // max 4 heating circuits
enum SendType : uint8_t { TEMP, HUMI };
static uint8_t get_hc(const uint8_t addr);
static void version(uint8_t addr, uint8_t dst, uint8_t hc);
static void unknown(uint8_t addr, uint8_t dst, uint8_t type, uint8_t offset);
static void unknown(uint8_t addr, uint8_t dst, uint8_t offset, uint8_t typeh, uint8_t typel);
static void temperature(uint8_t addr, uint8_t dst, uint8_t hc);
static void humidity(uint8_t addr, uint8_t dst, uint8_t hc);
static void nack_write();
static void ack_write();
static void replyF7(uint8_t addr, uint8_t dst, uint8_t offset, uint8_t typehh, uint8_t typeh, uint8_t typel, uint8_t hc);
static int16_t calc_dew(int16_t temp, uint8_t hum);
static bool switch_off_[HCS];
static uint32_t send_time_[HCS];
static uint32_t receive_time_[HCS];
static int16_t remotetemp_[HCS];
static uint8_t remotehum_[HCS];
static uint8_t sendtype_[HCS];
static uint8_t type_[HCS]; // type is product-id 113 for RC20 or 109 for Junkers FB10
static uint32_t timeout_;
};
} // namespace emsesp
#endif

261
src/core/shower.cpp Normal file
View File

@@ -0,0 +1,261 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "shower.h"
namespace emsesp {
uuid::log::Logger Shower::logger_{F_(shower), uuid::log::Facility::CONSOLE};
static bool force_coldshot = false;
void Shower::start() {
EMSESP::webSettingsService.read([&](WebSettings const & settings) {
shower_timer_ = settings.shower_timer;
shower_alert_ = settings.shower_alert;
shower_alert_trigger_ = settings.shower_alert_trigger * 60; // convert from minutes to seconds
shower_alert_coldshot_ = settings.shower_alert_coldshot; // in seconds
shower_min_duration_ = settings.shower_min_duration; // in seconds
});
Command::add(
EMSdevice::DeviceType::BOILER,
F_(coldshot),
[&](const char * value, const int8_t id, JsonObject output) {
LOG_INFO("Forcing coldshot...");
if (shower_state_) {
output["message"] = "OK";
force_coldshot = true;
} else {
output["message"] = "Coldshot failed. Shower not active";
LOG_WARNING("Coldshot failed. Shower not active");
force_coldshot = false;
}
return true;
},
FL_(coldshot_cmd),
CommandFlag::ADMIN_ONLY);
if (shower_timer_) {
set_shower_state(false, true); // turns shower to off and creates HA topic if not already done
}
}
void Shower::loop() {
if (!shower_timer_) {
return;
}
auto time_now = uuid::get_uptime_sec(); // in sec
// if already in cold mode, ignore all this logic until we're out of the cold blast
if (!doing_cold_shot_) {
// is the hot water running?
if (EMSESP::tap_water_active()) {
// if heater was previously off, start the timer
if (timer_start_ == 0) {
// hot water just started...
timer_start_ = time_now;
timer_pause_ = 0; // remove any last pauses
doing_cold_shot_ = false;
duration_ = 0;
shower_state_ = false;
next_alert_ = shower_alert_trigger_;
} else {
// hot water has been on for a while
// first check to see if hot water has been on long enough to be recognized as a Shower/Bath
if (!shower_state_ && (time_now - timer_start_) > shower_min_duration_) {
set_shower_state(true);
LOG_DEBUG("hot water still running, starting shower timer");
}
// check if the shower has been on too long
else if ((shower_alert_ && ((time_now - timer_start_) > next_alert_)) || force_coldshot) {
shower_alert_start();
}
}
} else { // hot water is off
// if it just turned off, record the time as it could be a short pause
if (timer_start_ && (timer_pause_ == 0)) {
timer_pause_ = time_now;
}
// if shower has been off for longer than the wait time
if (timer_pause_ && ((time_now - timer_pause_) > SHOWER_PAUSE_TIME)) {
// it is over the wait period, so assume that the shower has finished and calculate the total time and publish
// because its unsigned long, can't have negative so check if length is less than OFFSET_TIME
if ((timer_pause_ - timer_start_) > SHOWER_OFFSET_TIME) {
duration_ = (timer_pause_ - timer_start_ - SHOWER_OFFSET_TIME); // duration in seconds
if (duration_ > shower_min_duration_) {
JsonDocument doc;
// duration in seconds
doc["duration"] = duration_; // seconds
// time_t now = time(nullptr);
// // if NTP enabled, publish timestamp
// if (now > 1576800000) { // year 2020
// // doc["timestamp_s"] = now; // if needed, in seconds
// tm * tm_ = localtime(&now);
// char dt[25];
// strftime(dt, sizeof(dt), "%FT%T%z", tm_);
// doc["timestamp"] = dt;
// LOG_INFO("Shower finished %s (duration %lus)", dt, duration_);
// } else {
// LOG_INFO("Shower finished (duration %lus)", duration_);
// }
LOG_INFO("Shower finished (duration %lus)", duration_);
Mqtt::queue_publish("shower_data", doc.as<JsonObject>());
}
}
// reset everything
timer_start_ = 0;
timer_pause_ = 0;
doing_cold_shot_ = false;
alert_timer_start_ = 0;
set_shower_state(false);
}
}
return;
}
// at this point we're in the shower cold shot (doing_cold_shot_ == true)
// keep repeating until the time is up
if ((time_now - alert_timer_start_) > shower_alert_coldshot_) {
shower_alert_stop();
}
}
// turn off hot water to send a shot of cold
void Shower::shower_alert_start() {
LOG_DEBUG("Shower Alert started");
(void)Command::call(EMSdevice::DeviceType::BOILER, "tapactivated", "false", 9);
doing_cold_shot_ = true;
force_coldshot = false;
alert_timer_start_ = uuid::get_uptime_sec(); // timer starts now
}
// turn back on the hot water for the shower
void Shower::shower_alert_stop() {
if (doing_cold_shot_) {
LOG_DEBUG("Shower Alert stopped");
(void)Command::call(EMSdevice::DeviceType::BOILER, "tapactivated", "true", 9);
doing_cold_shot_ = false;
force_coldshot = false;
next_alert_ += shower_alert_trigger_;
}
}
// send status of shower to MQTT topic called shower_active - which is determined by the state parameter
// and creates the HA config topic if HA enabled
// force is used by EMSESP::publish_all_loop()
void Shower::set_shower_state(bool state, bool force) {
// sets the state
shower_state_ = state;
// only publish if that state has changed
static bool old_shower_state_ = false;
if ((shower_state_ == old_shower_state_) && !force) {
return;
}
old_shower_state_ = shower_state_; // copy current state
// always publish as a string
char s[12];
Mqtt::queue_publish("shower_active", Helpers::render_boolean(s, shower_state_)); // https://github.com/emsesp/EMS-ESP/issues/369
// send out HA MQTT Discovery config topic
if ((Mqtt::ha_enabled()) && (!ha_configdone_ || force)) {
JsonDocument doc;
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
char str[70];
char stat_t[50];
//
// shower active
//
doc["name"] = "Shower Active";
if (Mqtt::entity_format() == Mqtt::entityFormat::MULTI_SHORT) {
snprintf(str, sizeof(str), "%s_shower_active", Mqtt::basename().c_str());
} else {
snprintf(str, sizeof(str), "shower_active"); // v3.4 compatible
}
doc["uniq_id"] = str;
doc["object_id"] = str;
snprintf(stat_t, sizeof(stat_t), "%s/shower_active", Mqtt::base().c_str());
doc["stat_t"] = stat_t;
Mqtt::add_ha_bool(doc);
Mqtt::add_ha_sections_to_doc("shower", stat_t, doc, true); // create first dev & ids, no conditions
snprintf(topic, sizeof(topic), "binary_sensor/%s/shower_active/config", Mqtt::basename().c_str());
ha_configdone_ = Mqtt::queue_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
//
// shower duration
//
doc.clear();
snprintf(str, sizeof(str), "%s_shower_duration", Mqtt::basename().c_str());
doc["uniq_id"] = str;
doc["object_id"] = str;
snprintf(stat_t, sizeof(stat_t), "%s/shower_data", Mqtt::base().c_str());
doc["stat_t"] = stat_t;
doc["name"] = "Shower Duration";
doc["val_tpl"] = "{{value_json.duration if value_json.duration is defined else 0}}";
doc["unit_of_meas"] = "s";
doc["stat_cla"] = "measurement";
doc["dev_cla"] = "duration";
// doc["ent_cat"] = "diagnostic";
Mqtt::add_ha_sections_to_doc("shower", stat_t, doc, false, "value_json.duration is defined");
snprintf(topic, sizeof(topic), "sensor/%s/shower_duration/config", Mqtt::basename().c_str());
Mqtt::queue_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
//
// shower timestamp
//
doc.clear();
snprintf(str, sizeof(str), "%s_shower_timestamp", Mqtt::basename().c_str());
doc["uniq_id"] = str;
doc["object_id"] = str;
snprintf(stat_t, sizeof(stat_t), "%s/shower_data", Mqtt::base().c_str());
doc["stat_t"] = stat_t;
doc["name"] = "Shower Timestamp";
doc["val_tpl"] = "{{value_json.timestamp if value_json.timestamp is defined else 0}}";
// doc["ent_cat"] = "diagnostic";
Mqtt::add_ha_sections_to_doc("shower", stat_t, doc, false, "value_json.timestamp is defined");
snprintf(topic, sizeof(topic), "sensor/%s/shower_timestamp/config", Mqtt::basename().c_str());
Mqtt::queue_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
}
}
} // namespace emsesp

73
src/core/shower.h Normal file
View File

@@ -0,0 +1,73 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_SHOWER_H
#define EMSESP_SHOWER_H
#include "emsesp.h"
namespace emsesp {
class Shower {
public:
void start();
void loop();
void set_shower_state(bool state, bool force = false);
// commands
static bool command_coldshot(const char * value, const int8_t id);
void shower_timer(bool enable) {
shower_timer_ = enable;
}
void shower_alert(bool enable) {
shower_alert_ = enable;
}
private:
static uuid::log::Logger logger_;
static constexpr uint32_t SHOWER_PAUSE_TIME = 15; // 15 seconds, max time if water is switched off & on during a shower
static constexpr uint32_t SHOWER_OFFSET_TIME = 5; // 5 seconds grace time, to calibrate actual time under the shower
void shower_alert_start();
void shower_alert_stop();
bool shower_timer_; // true if we want to report back on shower times
bool shower_alert_; // true if we want the alert of cold water
uint32_t shower_alert_trigger_; // default 7 minutes, before trigger a shot of cold water
uint32_t shower_alert_coldshot_; // default 10 seconds for cold water before turning back hot water
uint32_t shower_min_duration_; // default 3 minutes (180 seconds), before recognizing its a shower
uint32_t next_alert_;
bool ha_configdone_ = false; // for HA MQTT Discovery
bool shower_state_;
uint32_t timer_start_; // sec
uint32_t timer_pause_; // sec
uint32_t duration_; // sec
// cold shot
uint32_t alert_timer_start_; // sec
bool doing_cold_shot_; // true if we've just sent a jolt of cold water
};
} // namespace emsesp
#endif

712
src/core/shuntingYard.hpp Normal file
View File

@@ -0,0 +1,712 @@
// Shunting-yard Algorithm
// https://en.wikipedia.org/wiki/Shunting-yard_algorithm
//
// Implementation notes for unary operators by Austin Taylor
// https://stackoverflow.com/a/5240912
//
// Example:
// https://ideone.com/VocUTq
//
// License:
// This code uses the following materials.
// (1) Wikipedia article [Shunting-yard algorithm](https://en.wikipedia.org/wiki/Shunting-yard_algorithm),
// which is released under the [Creative Commons Attribution-Share-Alike License 3.0](https://creativecommons.org/licenses/by-sa/3.0/).
// (2) [Implementation notes for unary operators in Shunting-Yard algorithm](https://stackoverflow.com/a/5240912) by Austin Taylor
// which is released under the [Creative Commons Attribution-Share-Alike License 2.5](https://creativecommons.org/licenses/by-sa/2.5/).
//
// copy from https://gist.github.com/t-mat/b9f681b7591cdae712f6
// modified MDvP, 06.2024
//
#include <string>
#include <vector>
#include <deque>
#include <math.h>
class Token {
public:
enum class Type : uint8_t {
Unknown,
Number,
String,
Operator,
Compare,
Logic,
Unary,
LeftParen,
RightParen,
};
Token(Type type, const std::string & s, int8_t precedence = -1, bool rightAssociative = false)
: type{type}
, str(s)
, precedence{precedence}
, rightAssociative{rightAssociative} {
}
const Type type;
const std::string str;
const int8_t precedence;
const bool rightAssociative;
};
// find tokens
std::deque<Token> exprToTokens(const std::string & expr) {
std::deque<Token> tokens;
for (const auto * p = expr.c_str(); *p; ++p) {
if (isblank(*p)) {
// do nothing
} else if (*p == '{') { // json is stored as string including {}
const auto * b = p;
++p;
uint8_t i = 1;
while (*p && i > 0) {
i += (*p == '{') ? 1 : (*p == '}') ? -1 : 0;
++p;
}
if (*p) {
++p;
}
const auto s = std::string(b, p);
tokens.emplace_back(Token::Type::String, s, -3);
if (*p == '\0') {
--p;
}
} else if (strncmp(p, "int", 3) == 0) {
p += 2;
tokens.emplace_back(Token::Type::Unary, "i", 5);
} else if (strncmp(p, "round", 5) == 0) {
p += 4;
tokens.emplace_back(Token::Type::Unary, "r", 5);
} else if (strncmp(p, "abs", 3) == 0) {
p += 2;
tokens.emplace_back(Token::Type::Unary, "a", 5);
} else if (strncmp(p, "log", 3) == 0) {
p += 2;
tokens.emplace_back(Token::Type::Unary, "l", 5);
} else if (strncmp(p, "exp", 3) == 0) {
p += 2;
tokens.emplace_back(Token::Type::Unary, "e", 5);
} else if (strncmp(p, "sqrt", 4) == 0) {
p += 3;
tokens.emplace_back(Token::Type::Unary, "s", 5);
} else if (strncmp(p, "pow", 3) == 0) {
p += 2;
tokens.emplace_back(Token::Type::Unary, "p", 5);
} else if (*p >= 'a' && *p <= 'z') {
const auto * b = p;
while ((*p >= 'a' && *p <= 'z') || (*p == '_')) {
++p;
}
const auto s = std::string(b, p);
tokens.emplace_back(Token::Type::String, s, -2);
--p;
} else if (*p == '"') {
++p;
const auto * b = p;
while (*p && *p != '"') {
++p;
}
const auto s = std::string(b, p);
tokens.emplace_back(Token::Type::String, s, -3);
if (*p == '\0') {
--p;
}
} else if (*p == '\'') {
++p;
const auto * b = p;
while (*p && *p != '\'') {
++p;
}
const auto s = std::string(b, p);
tokens.emplace_back(Token::Type::String, s, -3);
if (*p == '\0') {
--p;
}
} else if (isdigit(*p)) {
const auto * b = p;
while (isdigit(*p) || *p == '.') {
++p;
}
const auto s = std::string(b, p);
tokens.emplace_back(Token::Type::Number, s, -4);
--p;
} else {
Token::Type token = Token::Type::Operator;
int8_t precedence = -1;
bool rightAssociative = false;
char c = *p;
switch (c) {
default:
token = Token::Type::Unknown;
break;
case '(':
token = Token::Type::LeftParen;
break;
case ')':
token = Token::Type::RightParen;
break;
case '^':
precedence = 4;
rightAssociative = true;
break;
case '*':
precedence = 3;
break;
case '/':
precedence = 3;
break;
case '%':
precedence = 3;
break;
case '+':
precedence = 2;
break;
case '-':
// If current token is '-'
// and if it is the first token, or preceded by another operator, or left-paren,
if (tokens.empty() || tokens.back().type == Token::Type::Operator || tokens.back().type == Token::Type::Compare
|| tokens.back().type == Token::Type::Logic || tokens.back().type == Token::Type::Unary || tokens.back().type == Token::Type::LeftParen) {
// it's unary '-'
// note#1 : 'm' is a special operator name for unary '-'
// note#2 : It has highest precedence than any of the infix operators
if (!tokens.empty() && tokens.back().str[0] == 'm') { // double unary minus
tokens.pop_back();
continue;
}
token = Token::Type::Unary;
c = 'm';
precedence = 5;
} else {
// otherwise, it's binary '-'
precedence = 2;
}
break;
case '&':
if (p[1] == '&')
++p;
precedence = 0;
token = Token::Type::Logic;
break;
case '|':
if (p[1] == '|')
++p;
precedence = 0;
token = Token::Type::Logic;
break;
case '!':
if (p[1] == '=') {
++p;
precedence = 1;
token = Token::Type::Compare;
} else {
precedence = 1;
token = Token::Type::Unary;
}
break;
case '<':
if (p[1] == '=') {
++p;
c = '{';
}
precedence = 1;
token = Token::Type::Compare;
break;
case '>':
if (p[1] == '=') {
++p;
c = '}';
}
precedence = 1;
token = Token::Type::Compare;
break;
case '=':
if (p[1] == '=')
++p;
precedence = 1;
token = Token::Type::Compare;
break;
}
const auto s = std::string(1, c);
tokens.emplace_back(token, s, precedence, rightAssociative);
}
}
return tokens;
}
// sort tokens to RPN form
std::deque<Token> shuntingYard(const std::deque<Token> & tokens) {
std::deque<Token> queue;
std::vector<Token> stack;
// While there are tokens to be read:
for (auto const & token : tokens) {
// Read a token
switch (token.type) {
case Token::Type::Number:
case Token::Type::String:
// If the token is a number, then add it to the output queue
queue.push_back(token);
break;
case Token::Type::Unary:
case Token::Type::Compare:
case Token::Type::Logic:
case Token::Type::Operator: {
// If the token is operator, o1, then:
const auto o1 = token;
// while there is an operator token,
while (!stack.empty()) {
// o2, at the top of stack, and
const auto o2 = stack.back();
// either o1 is left-associative and its precedence is
// *less than or equal* to that of o2,
// or o1 if right associative, and has precedence
// *less than* that of o2,
if ((!o1.rightAssociative && o1.precedence <= o2.precedence) || (o1.rightAssociative && o1.precedence < o2.precedence)) {
// then pop o2 off the stack,
stack.pop_back();
// onto the output queue;
queue.push_back(o2);
continue;
}
// @@ otherwise, exit.
break;
}
// push o1 onto the stack.
stack.push_back(o1);
} break;
case Token::Type::LeftParen:
// If token is left parenthesis, then push it onto the stack
stack.push_back(token);
break;
case Token::Type::RightParen: {
// If token is right parenthesis:
bool match = false;
// Until the token at the top of the stack
// is a left parenthesis,
while (!stack.empty() && stack.back().type != Token::Type::LeftParen) {
// pop operators off the stack
// onto the output queue.
queue.push_back(stack.back());
stack.pop_back();
match = true;
}
if (!match && stack.empty()) {
// If the stack runs out without finding a left parenthesis,
// then there are mismatched parentheses.
return {};
}
// Pop the left parenthesis from the stack,
// but not onto the output queue.
stack.pop_back();
} break;
case Token::Type::Unknown:
default:
return {};
}
}
// When there are no more tokens to read:
// While there are still operator tokens in the stack:
while (!stack.empty()) {
// If the operator token on the top of the stack is a parenthesis,
// then there are mismatched parentheses.
if (stack.back().type == Token::Type::LeftParen) {
return {};
}
// Pop the operator onto the output queue.
queue.push_back(std::move(stack.back()));
stack.pop_back();
}
return queue;
}
// check if string is a number
bool isnum(const std::string & s) {
if (s.find_first_not_of("0123456789.") == std::string::npos || (s[0] == '-' && s.find_first_not_of("0123456789.", 1) == std::string::npos)) {
return true;
}
return false;
}
// replace commands like "<device>/<hc>/<cmd>" with its value"
std::string commands(std::string & expr, bool quotes = true) {
for (uint8_t device = 0; device < emsesp::EMSdevice::DeviceType::UNKNOWN; device++) {
const char * d = emsesp::EMSdevice::device_type_2_device_name(device);
auto f = expr.find(d);
while (f != std::string::npos) {
// entity names are alphanumeric or _
auto e = expr.find_first_not_of("/._abcdefghijklmnopqrstuvwxyz0123456789", f);
if (e == std::string::npos) {
e = expr.length();
}
while (e > 0 && expr[e - 1] == ' ') { // remove blanks from end
e--;
}
char cmd[COMMAND_MAX_LENGTH];
size_t l = e - f;
if (l >= sizeof(cmd) - 1) {
break;
}
expr.copy(cmd, l, f);
cmd[l] = '\0';
if (strstr(cmd, "/value") == nullptr) {
strlcat(cmd, "/value", sizeof(cmd) - 6);
}
JsonDocument doc_out;
JsonDocument doc_in;
JsonObject output = doc_out.to<JsonObject>();
JsonObject input = doc_in.to<JsonObject>();
std::string cmd_s = "api/" + std::string(cmd);
emsesp::Command::process(cmd_s.c_str(), true, input, output);
if (output["api_data"].is<std::string>()) {
std::string data = output["api_data"];
if (!isnum(data) && quotes) {
data.insert(data.begin(), '"');
data.insert(data.end(), '"');
}
expr.replace(f, l, data);
e = f + data.length();
} else {
return expr = "";
}
f = expr.find(d, e);
}
}
return expr;
}
// checks for logic value
int to_logic(const std::string & s) {
if (s[0] == '1' || s == "on" || s == "ON" || s == "true") {
return 1;
}
if (s[0] == '0' || s == "off" || s == "OFF" || s == "false") {
return 0;
}
return -1;
}
// number to string, remove trailing zeros
std::string to_string(double d) {
std::string s = std::to_string(d);
while (!s.empty() && s.back() == '0') {
s.pop_back();
}
if (!s.empty() && s.back() == '.') {
s.pop_back();
}
return s;
}
// RPN calculator
std::string calculate(const std::string & expr) {
auto expr_new = emsesp::Helpers::toLower(expr);
commands(expr_new);
const auto tokens = exprToTokens(expr_new);
if (tokens.empty()) {
return "";
}
auto queue = shuntingYard(tokens);
if (queue.empty()) {
return "";
}
/*
// debug only print tokens
#ifdef EMSESP_STANDALONE
for (const auto & t : queue) {
emsesp::EMSESP::logger().debug("shunt token: %s(%d)", t.str.c_str(), t.type);
}
#endif
*/
std::vector<std::string> stack;
while (!queue.empty()) {
const auto token = queue.front();
queue.pop_front();
switch (token.type) {
case Token::Type::Number:
case Token::Type::String:
stack.push_back(token.str);
break;
case Token::Type::Unary: {
if (stack.empty()) {
return "";
}
const auto rhs = stack.back();
stack.pop_back();
switch (token.str[0]) {
default:
return "";
break;
case 'm': // Special operator name for unary '-'
if (!isnum(rhs)) {
return "";
}
stack.push_back(to_string(-1 * std::stod(rhs)));
break;
case '!':
if (to_logic(rhs) < 0) {
return "";
}
stack.push_back(to_logic(rhs) == 0 ? "1" : "0");
break;
case 'i':
stack.push_back(to_string(std::stoi(rhs)));
break;
case 'r':
stack.push_back(to_string(std::round(std::stod(rhs))));
break;
case 'a':
stack.push_back(to_string(std::abs(std::stod(rhs))));
break;
case 'e':
stack.push_back(to_string(std::exp(std::stod(rhs))));
break;
case 'l':
stack.push_back(to_string(std::log(std::stod(rhs))));
break;
case 's':
stack.push_back(to_string(std::sqrt(std::stod(rhs))));
break;
case 'p':
stack.push_back(to_string(std::pow(std::stod(rhs), 2)));
break;
}
} break;
case Token::Type::Compare: {
if (stack.size() < 2) {
return "";
}
const auto rhs = stack.back();
stack.pop_back();
const auto lhs = stack.back();
stack.pop_back();
switch (token.str[0]) {
default:
return "";
break;
case '<':
if (isnum(rhs) && isnum(lhs)) {
stack.push_back((std::stod(lhs) < std::stod(rhs)) ? "1" : "0");
break;
}
stack.push_back((lhs < rhs) ? "1" : "0");
break;
case '{':
if (isnum(rhs) && isnum(lhs)) {
stack.push_back((std::stod(lhs) <= std::stod(rhs)) ? "1" : "0");
break;
}
stack.push_back((lhs <= rhs) ? "1" : "0");
break;
case '>':
if (isnum(rhs) && isnum(lhs)) {
stack.push_back((std::stod(lhs) > std::stod(rhs)) ? "1" : "0");
break;
}
stack.push_back((lhs > rhs) ? "1" : "0");
break;
case '}':
if (isnum(rhs) && isnum(lhs)) {
stack.push_back((std::stod(lhs) >= std::stod(rhs)) ? "1" : "0");
break;
}
stack.push_back((lhs >= rhs) ? "1" : "0");
break;
case '=':
if (isnum(rhs) && isnum(lhs)) {
stack.push_back((std::stod(lhs) == std::stod(rhs)) ? "1" : "0");
break;
}
stack.push_back((lhs == rhs) ? "1" : "0");
break;
case '!':
if (isnum(rhs) && isnum(lhs)) {
stack.push_back((std::stod(lhs) != std::stod(rhs)) ? "1" : "0");
break;
}
stack.push_back((lhs != rhs) ? "1" : "0");
break;
}
} break;
case Token::Type::Logic: {
// binary operators
if (stack.size() < 2) {
return "";
}
const auto rhs = to_logic(stack.back());
stack.pop_back();
const auto lhs = to_logic(stack.back());
stack.pop_back();
if (rhs < 0 || lhs < 0) {
return "";
}
switch (token.str[0]) {
default:
return "";
break;
case '&':
stack.push_back((lhs && rhs) ? "1" : "0");
break;
case '|':
stack.push_back((lhs || rhs) ? "1" : "0");
break;
}
} break;
case Token::Type::Operator: {
// binary operators
if (stack.empty() || !isnum(stack.back())) {
return "";
}
const auto rhs = std::stod(stack.back());
stack.pop_back();
if (stack.empty() || !isnum(stack.back())) {
return "";
}
const auto lhs = std::stod(stack.back());
stack.pop_back();
switch (token.str[0]) {
default:
return "";
break;
case '^':
stack.push_back(to_string(pow(lhs, rhs)));
break;
case '*':
stack.push_back(to_string(lhs * rhs));
break;
case '/':
stack.push_back(to_string(lhs / rhs));
break;
case '%':
stack.push_back(std::to_string(static_cast<int>(lhs) % static_cast<int>(rhs)));
break;
case '+':
stack.push_back(to_string(lhs + rhs));
break;
case '-':
stack.push_back(to_string(lhs - rhs));
break;
}
} break;
case Token::Type::LeftParen:
case Token::Type::RightParen:
case Token::Type::Unknown:
default:
return "";
break;
}
}
// concatenate all elements in stack to a single string, separated by spaces and return
std::string result = "";
for (const auto & s : stack) {
result += s;
}
return result;
}
// check for multiple instances of <cond> ? <expr1> : <expr2>
std::string compute(const std::string & expr) {
auto expr_new = emsesp::Helpers::toLower(expr);
// search json with url:
auto f = expr_new.find_first_of('{');
while (f != std::string::npos) {
auto e = f + 1;
for (uint8_t i = 1; i > 0; e++) {
if (e >= expr_new.length()) {
return "";
} else if (expr_new[e] == '}') {
i--;
} else if (expr_new[e] == '{') {
i++;
}
}
std::string cmd = expr_new.substr(f, e - f).c_str();
JsonDocument doc;
if (DeserializationError::Ok == deserializeJson(doc, cmd)) {
HTTPClient http;
std::string url = doc["url"] | "";
if (http.begin(url.c_str())) {
int httpResult = 0;
for (JsonPair p : doc["header"].as<JsonObject>()) {
http.addHeader(p.key().c_str(), p.value().as<std::string>().c_str());
}
std::string value = doc["value"] | "";
std::string method = doc["method"] | "GET"; // default GET
// if there is data, force a POST
if (value.length() || method == "post") {
if (value.find_first_of('{') != std::string::npos) {
http.addHeader("Content-Type", "application/json"); // auto-set to JSON
}
httpResult = http.POST(value.c_str());
} else {
httpResult = http.GET(); // normal GET
}
if (httpResult > 0) {
std::string result = emsesp::Helpers::toLower(http.getString().c_str());
std::string key = doc["key"] | "";
doc.clear();
if (key.length() && DeserializationError::Ok == deserializeJson(doc, result)) {
result = doc[key.c_str()].as<std::string>();
}
expr_new.replace(f, e - f, result.c_str());
}
http.end();
}
}
f = expr_new.find_first_of('{', e);
}
// positions: q-questionmark, c-colon
auto q = expr_new.find_first_of('?');
while (q != std::string::npos) {
// find corresponding colon
auto c1 = expr_new.find_first_of(':', q + 1);
auto q1 = expr_new.find_first_of('?', q + 1);
while (q1 < c1 && q1 != std::string::npos && c1 != std::string::npos) {
q1 = expr_new.find_first_of('?', q1 + 1);
c1 = expr_new.find_first_of(':', c1 + 1);
}
if (c1 == std::string::npos) {
return ""; // error: missing colon
}
std::string cond = calculate(expr_new.substr(0, q));
if (cond.length() == 0) {
return "";
} else if (cond[0] == '1') {
expr_new.erase(c1); // remove second expression after colon
expr_new.erase(0, q + 1); // remove condition before questionmark
} else if (cond[0] == '0') {
expr_new.erase(0, c1 + 1); // remove condition and first expression
} else {
return ""; // error
}
q = expr_new.find_first_of('?'); // search next instance
}
return calculate(expr_new);
}

2147
src/core/system.cpp Normal file

File diff suppressed because it is too large Load Diff

444
src/core/system.h Normal file
View File

@@ -0,0 +1,444 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_SYSTEM_H_
#define EMSESP_SYSTEM_H_
#include <Arduino.h>
#include <ArduinoJson.h>
#include "helpers.h"
#include "console.h"
#include "mqtt.h"
#include "telegram.h"
#ifndef EMSESP_STANDALONE
#include <esp_wifi.h>
#if CONFIG_IDF_TARGET_ESP32
// #include <esp_bt.h>
#endif
#include <ETH.h>
#include <uuid/syslog.h>
#endif
#include <uuid/log.h>
#include <PButton.h>
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
#if ESP_IDF_VERSION_MAJOR < 5
#include "driver/temp_sensor.h"
#else
#include "driver/temperature_sensor.h"
#endif
#endif
using uuid::console::Shell;
#define EMSESP_FS_CONFIG_DIRECTORY "/config"
#define EMSESP_CUSTOMSUPPORT_FILE "/config/customSupport.json"
namespace emsesp {
enum PHY_type : uint8_t { PHY_TYPE_NONE = 0, PHY_TYPE_LAN8720, PHY_TYPE_TLK110 };
class System {
public:
void start();
void loop();
// commands
static bool command_read(const char * value, const int8_t id);
static bool command_send(const char * value, const int8_t id);
static bool command_publish(const char * value, const int8_t id);
static bool command_fetch(const char * value, const int8_t id);
static bool command_restart(const char * value, const int8_t id);
static bool command_format(const char * value, const int8_t id);
static bool command_watch(const char * value, const int8_t id);
static bool command_message(const char * value, const int8_t id);
static bool command_info(const char * value, const int8_t id, JsonObject output);
static bool command_response(const char * value, const int8_t id, JsonObject output);
static bool command_service(const char * cmd, const char * value);
static bool get_value_info(JsonObject root, const char * cmd);
static void get_value_json(JsonObject output, const std::string & circuit, const std::string & name, JsonVariant val);
#if defined(EMSESP_TEST)
static bool command_test(const char * value, const int8_t id);
#endif
std::string reset_reason(uint8_t cpu) const;
void store_nvs_values();
void system_restart(const char * partition = nullptr);
void upload_isrunning(bool in_progress);
bool upload_isrunning();
void show_mem(const char * note);
void reload_settings();
void syslog_init();
bool check_upgrade(bool factory_settings);
bool check_restore();
void heartbeat_json(JsonObject output);
void send_heartbeat();
void send_info_mqtt();
bool syslog_enabled() {
return syslog_enabled_;
}
#ifndef EMSESP_STANDALONE
unsigned long syslog_count() {
return syslog_.message_count();
}
unsigned long syslog_fails() {
return syslog_.message_fails();
}
#endif
String getBBQKeesGatewayDetails();
static bool uploadFirmwareURL(const char * url = nullptr);
void led_init(bool refresh);
void network_init(bool refresh);
void button_init(bool refresh);
void commands_init();
static void extractSettings(const char * filename, const char * section, JsonObject output);
static bool saveSettings(const char * filename, const char * section, JsonObject input);
static bool is_valid_gpio(uint8_t pin, bool has_psram = false);
static bool load_board_profile(std::vector<int8_t> & data, const std::string & board_profile);
static bool readCommand(const char * data);
static void restart_requested(bool restart_requested) {
restart_requested_ = restart_requested;
}
static bool restart_requested() {
return restart_requested_;
}
static void restart_pending(bool restart_pending) {
restart_pending_ = restart_pending;
}
static bool restart_pending() {
return restart_pending_;
}
bool telnet_enabled() {
return telnet_enabled_;
}
bool modbus_enabled() {
return modbus_enabled_;
}
uint16_t modbus_port() {
return modbus_port_;
}
uint8_t modbus_max_clients() {
return modbus_max_clients_;
}
uint32_t modbus_timeout() {
return modbus_timeout_;
}
bool analog_enabled() {
return analog_enabled_;
}
void analog_enabled(bool b) {
analog_enabled_ = b;
}
void hide_led(bool b) {
hide_led_ = b;
}
bool readonly_mode() {
return readonly_mode_;
}
void readonly_mode(bool readonly_mode) {
readonly_mode_ = readonly_mode;
}
bool developer_mode() {
return developer_mode_;
}
void developer_mode(bool developer_mode) {
developer_mode_ = developer_mode;
}
// Boolean Format API/MQTT
uint8_t bool_format() {
return bool_format_;
}
// Boolean Format Web
uint8_t bool_dashboard() {
return bool_dashboard_;
}
// see default_settings.h
// BOOL_FORMAT_ONOFF_STR = 1,
// BOOL_FORMAT_ONOFF_STR_CAP = 2
// BOOL_FORMAT_TRUEFALSE_STR = 3
// BOOL_FORMAT_TRUEFALSE = 4
// BOOL_FORMAT_10_STR = 5
// BOOL_FORMAT_10 = 6
void bool_format(uint8_t format) {
bool_format_ = format;
}
void bool_dashboard(uint8_t format) {
bool_dashboard_ = format;
}
uint8_t enum_format() {
return enum_format_;
}
void enum_format(uint8_t format) {
enum_format_ = format;
}
std::string board_profile() {
return std::string(board_profile_.c_str());
}
std::string hostname() {
return hostname_;
}
void hostname(const std::string hostname) {
hostname_ = hostname;
}
bool ethernet_connected() {
return ethernet_connected_;
}
void ethernet_connected(bool b) {
ethernet_connected_ = b;
}
void has_ipv6(bool b) {
has_ipv6_ = b;
}
bool has_ipv6() {
return has_ipv6_;
}
void ntp_connected(bool b);
bool ntp_connected();
bool network_connected() {
#ifndef EMSESP_STANDALONE
return (ethernet_connected() || WiFi.isConnected());
#else
return true;
#endif
}
void fahrenheit(bool b) {
fahrenheit_ = b;
}
bool fahrenheit() {
return fahrenheit_;
}
uint8_t language_index();
void locale(String locale) {
locale_ = locale;
}
std::string locale() {
return std::string(locale_.c_str());
}
void healthcheck(uint8_t healthcheck) {
healthcheck_ = healthcheck;
}
void show_system(uuid::console::Shell & shell);
void wifi_reconnect();
void show_users(uuid::console::Shell & shell);
uint32_t FStotal() {
return fstotal_;
}
void PSram(uint32_t psram) {
psram_ = psram / 1024;
}
uint32_t PSram() {
return psram_;
}
uint32_t appFree() {
return appfree_;
}
uint32_t appUsed() {
return appused_;
}
// memory in kb
static uint32_t getMaxAllocMem() {
return max_alloc_mem_;
}
static uint32_t getHeapMem() {
return heap_mem_;
}
static void refreshHeapMem() {
#ifndef EMSESP_STANDALONE
max_alloc_mem_ = ESP.getMaxAllocHeap() / 1024;
heap_mem_ = ESP.getFreeHeap() / 1024;
#endif
}
static bool test_set_all_active() {
return test_set_all_active_;
}
static void test_set_all_active(bool n) {
#if defined(EMSESP_TEST)
if (n) {
logger_.debug("Using dummy entity values");
}
#endif
test_set_all_active_ = n;
}
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
float temperature() {
return temperature_;
}
#endif
private:
static uuid::log::Logger logger_;
static bool restart_requested_;
static bool restart_pending_; // used in 2-stage process to call restart from Web API
static bool test_set_all_active_; // force all entities in a device to have a value
static uint32_t max_alloc_mem_;
static uint32_t heap_mem_;
// button
static PButton myPButton_; // PButton instance
static void button_OnClick(PButton & b);
static void button_OnDblClick(PButton & b);
static void button_OnLongPress(PButton & b);
static void button_OnVLongPress(PButton & b);
static constexpr uint32_t BUTTON_Debounce = 40; // Debounce period to prevent flickering when pressing or releasing the button (in ms)
static constexpr uint32_t BUTTON_DblClickDelay = 250; // Max period between clicks for a double click event (in ms)
static constexpr uint32_t BUTTON_LongPressDelay = 750; // Hold period for a long press event (in ms)
static constexpr uint32_t BUTTON_VLongPressDelay = 9000; // Hold period for a very long press event (in ms)
// healthcheck
#ifdef EMSESP_PINGTEST
static constexpr uint32_t SYSTEM_CHECK_FREQUENCY = 500; // do a system check every 1/2 second
#else
static constexpr uint32_t SYSTEM_CHECK_FREQUENCY = 5000; // do a system check every 5 seconds
#endif
static constexpr uint32_t HEALTHCHECK_LED_LONG_DUARATION = 1500;
static constexpr uint32_t HEALTHCHECK_LED_FLASH_DUARATION = 150;
static constexpr uint8_t HEALTHCHECK_NO_BUS = (1 << 0); // 1
static constexpr uint8_t HEALTHCHECK_NO_NETWORK = (1 << 1); // 2
static constexpr uint8_t LED_ON = HIGH; // LED on
#ifndef EMSESP_STANDALONE
static uuid::syslog::SyslogService syslog_;
#endif
void led_monitor();
void system_check();
int8_t wifi_quality(int8_t dBm);
uint8_t healthcheck_ = HEALTHCHECK_NO_NETWORK | HEALTHCHECK_NO_BUS; // start with all flags set, no wifi and no ems bus connection
uint32_t last_system_check_ = 0;
bool upload_isrunning_ = false; // true if we're in the middle of a OTA firmware upload
bool ethernet_connected_ = false;
bool has_ipv6_ = false;
bool ntp_connected_ = false;
uint32_t ntp_last_check_ = 0;
bool eth_present_ = false;
// EMS-ESP settings
// copies from WebSettings class in WebSettingsService.h and loaded with reload_settings()
std::string hostname_;
String locale_;
bool hide_led_;
uint8_t led_gpio_;
bool analog_enabled_;
bool low_clock_;
String board_profile_;
uint8_t pbutton_gpio_;
uint8_t rx_gpio_;
uint8_t tx_gpio_;
uint8_t dallas_gpio_;
bool telnet_enabled_;
bool syslog_enabled_;
int8_t syslog_level_;
uint32_t syslog_mark_interval_;
String syslog_host_;
uint16_t syslog_port_;
bool fahrenheit_;
uint8_t bool_dashboard_;
uint8_t bool_format_;
uint8_t enum_format_;
bool readonly_mode_;
String version_;
bool modbus_enabled_;
uint16_t modbus_port_;
uint8_t modbus_max_clients_;
uint32_t modbus_timeout_;
bool developer_mode_;
// ethernet
uint8_t phy_type_;
int8_t eth_power_;
uint8_t eth_phy_addr_;
uint8_t eth_clock_mode_;
uint32_t fstotal_;
uint32_t psram_;
uint32_t appused_;
uint32_t appfree_;
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
#if ESP_IDF_VERSION_MAJOR >= 5
temperature_sensor_handle_t temperature_handle_ = NULL;
#endif
float temperature_ = 0;
#endif
};
} // namespace emsesp
#endif

686
src/core/telegram.cpp Normal file
View File

@@ -0,0 +1,686 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "telegram.h"
#include "emsesp.h"
namespace emsesp {
// CRC lookup table with poly 12 for faster checking
const uint8_t ems_crc_table[] = {0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E, 0x20, 0x22, 0x24, 0x26,
0x28, 0x2A, 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E, 0x40, 0x42, 0x44, 0x46, 0x48, 0x4A, 0x4C, 0x4E,
0x50, 0x52, 0x54, 0x56, 0x58, 0x5A, 0x5C, 0x5E, 0x60, 0x62, 0x64, 0x66, 0x68, 0x6A, 0x6C, 0x6E, 0x70, 0x72, 0x74, 0x76,
0x78, 0x7A, 0x7C, 0x7E, 0x80, 0x82, 0x84, 0x86, 0x88, 0x8A, 0x8C, 0x8E, 0x90, 0x92, 0x94, 0x96, 0x98, 0x9A, 0x9C, 0x9E,
0xA0, 0xA2, 0xA4, 0xA6, 0xA8, 0xAA, 0xAC, 0xAE, 0xB0, 0xB2, 0xB4, 0xB6, 0xB8, 0xBA, 0xBC, 0xBE, 0xC0, 0xC2, 0xC4, 0xC6,
0xC8, 0xCA, 0xCC, 0xCE, 0xD0, 0xD2, 0xD4, 0xD6, 0xD8, 0xDA, 0xDC, 0xDE, 0xE0, 0xE2, 0xE4, 0xE6, 0xE8, 0xEA, 0xEC, 0xEE,
0xF0, 0xF2, 0xF4, 0xF6, 0xF8, 0xFA, 0xFC, 0xFE, 0x19, 0x1B, 0x1D, 0x1F, 0x11, 0x13, 0x15, 0x17, 0x09, 0x0B, 0x0D, 0x0F,
0x01, 0x03, 0x05, 0x07, 0x39, 0x3B, 0x3D, 0x3F, 0x31, 0x33, 0x35, 0x37, 0x29, 0x2B, 0x2D, 0x2F, 0x21, 0x23, 0x25, 0x27,
0x59, 0x5B, 0x5D, 0x5F, 0x51, 0x53, 0x55, 0x57, 0x49, 0x4B, 0x4D, 0x4F, 0x41, 0x43, 0x45, 0x47, 0x79, 0x7B, 0x7D, 0x7F,
0x71, 0x73, 0x75, 0x77, 0x69, 0x6B, 0x6D, 0x6F, 0x61, 0x63, 0x65, 0x67, 0x99, 0x9B, 0x9D, 0x9F, 0x91, 0x93, 0x95, 0x97,
0x89, 0x8B, 0x8D, 0x8F, 0x81, 0x83, 0x85, 0x87, 0xB9, 0xBB, 0xBD, 0xBF, 0xB1, 0xB3, 0xB5, 0xB7, 0xA9, 0xAB, 0xAD, 0xAF,
0xA1, 0xA3, 0xA5, 0xA7, 0xD9, 0xDB, 0xDD, 0xDF, 0xD1, 0xD3, 0xD5, 0xD7, 0xC9, 0xCB, 0xCD, 0xCF, 0xC1, 0xC3, 0xC5, 0xC7,
0xF9, 0xFB, 0xFD, 0xFF, 0xF1, 0xF3, 0xF5, 0xF7, 0xE9, 0xEB, 0xED, 0xEF, 0xE1, 0xE3, 0xE5, 0xE7};
uint32_t EMSbus::last_bus_activity_ = 0; // timestamp of last time a valid Rx came in
uint32_t EMSbus::bus_uptime_start_ = 0; // timestamp of when the bus was started
bool EMSbus::bus_connected_ = false; // start assuming the bus hasn't been connected
uint8_t EMSbus::ems_mask_ = EMS_MASK_UNSET; // unset so its triggered when booting, the its 0x00=buderus, 0x80=junker/ht3
uint8_t EMSbus::ems_bus_id_ = EMSESP_DEFAULT_EMS_BUS_ID;
uint8_t EMSbus::tx_mode_ = EMSESP_DEFAULT_TX_MODE;
uint8_t EMSbus::tx_state_ = Telegram::Operation::NONE;
uuid::log::Logger EMSbus::logger_{F_(telegram), uuid::log::Facility::CONSOLE};
// Calculates CRC checksum using lookup table for speed
// length excludes the last byte (which mainly is the CRC)
uint8_t EMSbus::calculate_crc(const uint8_t * data, const uint8_t length) {
uint8_t i = 0;
uint8_t crc = 0;
while (i < length) {
crc = ems_crc_table[crc];
crc ^= data[i++];
}
return crc;
}
// creates a telegram object
// stores header in separate member objects and the rest in the message_data block
Telegram::Telegram(const uint8_t operation,
const uint8_t src,
const uint8_t dest,
const uint16_t type_id,
const uint8_t offset,
const uint8_t * data,
const uint8_t message_length)
: operation(operation)
, src(src)
, dest(dest)
, type_id(type_id)
, offset(offset)
, message_length(message_length) {
// copy complete telegram data over, preventing buffer overflow
// faster than using std::move()
for (uint8_t i = 0; ((i < message_length) && (i < EMS_MAX_TELEGRAM_MESSAGE_LENGTH)); i++) {
message_data[i] = data[i];
}
}
// returns telegram as data bytes in hex (excluding CRC)
std::string Telegram::to_string() const {
uint8_t data[EMS_MAX_TELEGRAM_LENGTH];
uint8_t length = 0;
data[0] = this->src ^ RxService::ems_mask();
data[3] = this->offset;
if (this->operation == Telegram::Operation::TX_READ) {
data[1] = this->dest | 0x80;
data[4] = this->message_data[0];
if (this->type_id > 0xFF) {
data[2] = 0xFF;
data[5] = (this->type_id >> 8) - 1;
data[6] = this->type_id & 0xFF;
length = 7;
} else {
data[2] = this->type_id;
length = 5;
}
} else {
data[1] = this->dest;
if (this->type_id > 0xFF) {
data[2] = 0xFF;
data[4] = (this->type_id >> 8) - 1;
data[5] = this->type_id & 0xFF;
length = 6;
} else {
data[2] = this->type_id;
length = 4;
}
for (uint8_t i = 0; i < this->message_length; i++) {
data[length++] = this->message_data[i];
}
}
return Helpers::data_to_hex(data, length);
}
// returns telegram's message body only, in hex
std::string Telegram::to_string_message() const {
if (this->message_length == 0) {
return "<empty>";
}
return Helpers::data_to_hex(this->message_data, this->message_length);
}
// checks if we have an Rx telegram that needs processing
void RxService::loop() {
while (!rx_telegrams_.empty()) {
auto telegram = rx_telegrams_.front().telegram_;
(void)EMSESP::process_telegram(telegram); // further process the telegram
increment_telegram_count(); // increase rx count
rx_telegrams_.pop_front(); // remove it from the queue
}
}
// add a new rx telegram object
// data is the whole telegram, assuming last byte holds the CRC
// length includes the CRC
// for EMS+ the type_id has the value + 256. We look for these type of telegrams with F7, F9 and FF in 3rd byte
void RxService::add(uint8_t * data, uint8_t length) {
if (length < 5) {
return;
}
// validate the CRC. if it fails then increment the number of corrupt/incomplete telegrams and only report to console/syslog
uint8_t crc = calculate_crc(data, length - 1);
if (data[length - 1] != crc) {
if (data[0] != EMSuart::last_tx_src()) { // do not count echos as errors
telegram_error_count_++;
LOG_WARNING("Incomplete Rx: %s", Helpers::data_to_hex(data, length).c_str()); // include CRC
} else {
LOG_TRACE("Incomplete Rx: %s", Helpers::data_to_hex(data, length).c_str()); // include CRC
}
return;
}
// since it's a valid telegram, work out the ems mask
// we check the 1st byte, which assumed is the src ID and see if the MSB (8th bit) is set
// this is used to identify if the protocol should be Junkers/HT3 or Buderus
// this only happens once with the first valid rx telegram is processed
if (ems_mask() == EMS_MASK_UNSET) {
ems_mask(data[0]);
}
// src, dest and offset are always in fixed positions
uint8_t src = data[0] & 0x7F; // strip MSB (HT3 adds it)
uint8_t dest = data[1] & 0x7F; // strip MSB, don't care if its read or write for processing
uint8_t offset = data[3]; // offset is always 4th byte
uint8_t operation = (data[1] & 0x80) ? Telegram::Operation::RX_READ : Telegram::Operation::RX;
uint16_t type_id;
uint8_t * message_data; // where the message block starts
uint8_t message_length; // length of the message block, excluding CRC
// work out depending on the type, where the data message block starts and the message length
// EMS 1 has type_id always in data[2], if it gets a ems+ inquiry it will reply with FF but short length
// i.e. sending 0B A1 FF 00 01 D8 20 CRC to a MM10 Mixer (ems1.0), the reply is 21 0B FF 00 CRC
// see: https://github.com/emsesp/EMS-ESP/issues/380#issuecomment-633663007
if (data[2] != 0xFF || length < 6) {
// EMS 1.0
// also handle F7, F9 as EMS 1.0, see https://github.com/emsesp/EMS-ESP/issues/109#issuecomment-492781044
type_id = data[2];
message_data = data + 4;
message_length = length - 5;
} else if (data[1] & 0x80) {
// EMS 2.0 read request
type_id = (data[5] << 8) + data[6] + 256;
message_data = data + 4; // only data is the requested length
message_length = 1;
} else {
// EMS 2.0 / EMS+
type_id = (data[4] << 8) + data[5] + 256;
message_data = data + 6;
message_length = length - 7;
}
// if we're watching and "raw" print out actual telegram as bytes to the console
// including the CRC at the end
if (EMSESP::watch() == EMSESP::Watch::WATCH_RAW) {
uint16_t trace_watch_id = EMSESP::watch_id();
if ((trace_watch_id == WATCH_ID_NONE) || (type_id == trace_watch_id)
|| ((trace_watch_id < 0x80) && ((src == trace_watch_id) || (dest == trace_watch_id)))) {
LOG_NOTICE("Rx: %s", Helpers::data_to_hex(data, length).c_str());
} else if (EMSESP::trace_raw()) {
LOG_TRACE("Rx: %s", Helpers::data_to_hex(data, length).c_str());
}
} else if (EMSESP::trace_raw()) {
LOG_TRACE("Rx: %s", Helpers::data_to_hex(data, length).c_str());
}
LOG_DEBUG("New Rx telegram, message length %d", message_length);
// if we don't have a type_id exit,
// do not exit on empty message, it is checked for toggle fetch
if (type_id == 0) {
return;
}
// create the telegram
auto telegram = std::make_shared<Telegram>(operation, src, dest, type_id, offset, message_data, message_length);
// check if queue is full, if so remove top item to make space
if (rx_telegrams_.size() >= MAX_RX_TELEGRAMS) {
rx_telegrams_.pop_front();
}
rx_telegrams_.emplace_back(rx_telegram_id_++, std::move(telegram)); // add to queue
}
// add empty telegram to rx-queue
void RxService::add_empty(const uint8_t src, const uint8_t dest, const uint16_t type_id, uint8_t offset) {
auto telegram = std::make_shared<Telegram>(Telegram::Operation::RX, src, dest, type_id, offset, nullptr, 0);
// only if queue is not full
if (rx_telegrams_.size() < MAX_RX_TELEGRAMS) {
rx_telegrams_.emplace_back(rx_telegram_id_++, std::move(telegram)); // add to queue
}
}
// start and initialize Tx
// send out request to EMS bus for all devices
void TxService::start() {
// grab the bus ID and tx_mode
EMSESP::webSettingsService.read([&](WebSettings const & settings) {
ems_bus_id(settings.ems_bus_id);
tx_mode(settings.tx_mode);
});
// reset counters
telegram_read_count(0);
telegram_write_count(0);
telegram_fail_count(0);
// send first Tx request to bus master (boiler) for its registered devices
// this will be added to the queue and sent during the first tx loop()
read_request(EMSdevice::EMS_TYPE_UBADevices, EMSdevice::EMS_DEVICE_ID_BOILER);
}
// sends a 1 byte poll which is our own deviceID
void TxService::send_poll() const {
//LOG_DEBUG("Ack %02X",ems_bus_id() ^ ems_mask());
if (tx_mode()) {
EMSuart::send_poll(ems_bus_id() ^ ems_mask());
}
}
// get src id from next telegram to check poll in emsesp::incoming_telegram
uint8_t TxService::get_send_id() {
static uint32_t count = 0;
if (!tx_telegrams_.empty() && tx_telegrams_.front().telegram_->src != ems_bus_id()) {
if (++count > 500) { // after 500 polls (~3-10 sec) there will be no master poll for this id
tx_telegrams_.pop_front();
count = 0;
return tx_telegrams_.empty() ? ems_bus_id() : tx_telegrams_.front().telegram_->src;
}
return tx_telegrams_.front().telegram_->src;
}
count = 0;
return ems_bus_id();
}
// Process the next telegram on the Tx queue
// This is sent when we receive a poll request
void TxService::send() {
// don't process if we don't have a connection to the EMS bus
if (!bus_connected()) {
return;
}
// if there's nothing in the queue to transmit or sending should be delayed, send back a poll and quit
if (tx_telegrams_.empty() || (delayed_send_ && uuid::get_uptime() < delayed_send_)) {
send_poll();
return;
}
delayed_send_ = 0;
// if we're in read-only mode (tx_mode 0) forget the Tx call
if (tx_mode() != 0) {
send_telegram(tx_telegrams_.front());
}
tx_telegrams_.pop_front(); // remove the telegram from the queue
}
// process a Tx telegram
void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) {
static uint8_t telegram_raw[EMS_MAX_TELEGRAM_LENGTH];
// build the header
auto telegram = tx_telegram.telegram_;
// src - set MSB if it's Junkers/HT3
uint8_t src = telegram->src;
if (ems_mask() != EMS_MASK_UNSET) {
src ^= ems_mask();
}
telegram_raw[0] = src;
// dest - for READ the MSB must be set
// fix the READ or WRITE depending on the operation
uint8_t dest = telegram->dest;
if (telegram->operation == Telegram::Operation::TX_READ) {
dest |= 0x80; // read has 8th bit set for the destination
}
telegram_raw[1] = dest;
uint8_t message_p = 0; // this is the position in the telegram where we want to put our message data
bool copy_data = true; // true if we want to copy over the data message block to the end of the telegram header
if (telegram->type_id > 0xFF) {
// it's EMS 2.0/+
telegram_raw[2] = 0xFF; // fixed value indicating an extended message
telegram_raw[3] = telegram->offset;
// EMS+ has different format for read and write
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
message_p = 6;
} else {
// READ
telegram_raw[4] =
telegram->message_data[0] > 25 ? 25 : telegram->message_data[0]; // #bytes to return, which we assume is the only byte in the message block
telegram_raw[5] = (telegram->type_id >> 8) - 1; // type, 1st byte, high-byte, subtract 0x100
telegram_raw[6] = telegram->type_id & 0xFF; // type, 2nd byte, low-byte
message_p = 7;
copy_data = false; // there are no more data values after the type_id when reading on EMS+
}
} else {
// EMS 1.0
telegram_raw[2] = telegram->type_id;
telegram_raw[3] = telegram->offset;
message_p = 4;
if (telegram->operation == Telegram::Operation::TX_READ) {
telegram_raw[4] = telegram->message_data[0] > 27 ? 27 : telegram->message_data[0];
message_p = 5;
copy_data = false; // there are no more data value
}
}
if (copy_data) {
if (telegram->message_length > EMS_MAX_TELEGRAM_MESSAGE_LENGTH) {
return; // too big
}
// add the data to send to to the end of the header
for (uint8_t i = 0; i < telegram->message_length; i++) {
telegram_raw[message_p++] = telegram->message_data[i];
}
}
// make a copy of the telegram with new dest (without read-flag)
telegram_last_ = std::make_shared<Telegram>(
telegram->operation, telegram->src, dest & 0x7F, telegram->type_id, telegram->offset, telegram->message_data, telegram->message_length);
uint8_t length = message_p;
telegram_raw[length] = calculate_crc(telegram_raw, length); // generate and append CRC to the end
length++; // add one since we want to now include the CRC
// if we're in simulation mode, don't send anything, just quit
if (EMSESP::system_.readonly_mode() && (telegram->operation == Telegram::Operation::TX_WRITE)) {
LOG_INFO("[readonly] Sending write Tx telegram: %s", Helpers::data_to_hex(telegram_raw, length - 1).c_str());
tx_state(Telegram::Operation::NONE);
return;
}
LOG_DEBUG("Sending %s Tx [#%d], telegram: %s",
(telegram->operation != Telegram::Operation::TX_READ) ? ("write") : ("read"),
tx_telegram.id_,
Helpers::data_to_hex(telegram_raw, length - 1).c_str()); // exclude the last CRC byte
set_post_send_query(tx_telegram.validateid_);
//
// this is the core send command to the UART
//
uint16_t status = EMSuart::transmit(telegram_raw, length);
if (status == EMS_TX_STATUS_ERR) {
LOG_ERROR("Failed to transmit Tx via UART.");
if (telegram->operation == Telegram::Operation::TX_READ) {
increment_telegram_read_fail_count(); // another Tx fail
} else {
increment_telegram_write_fail_count(); // another Tx fail
}
tx_state(Telegram::Operation::NONE); // nothing send, tx not in wait state
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
}
void TxService::add(const uint8_t operation,
const uint8_t dest,
const uint16_t type_id,
const uint8_t offset,
uint8_t * message_data,
const uint8_t message_length,
const uint16_t validateid,
const bool front) {
auto telegram = std::make_shared<Telegram>(operation, ems_bus_id(), dest, type_id, offset, message_data, message_length);
LOG_DEBUG("New Tx [#%d] telegram, length %d", tx_telegram_id_, message_length);
// if the queue is full, make room by removing the last one
if (tx_telegrams_.size() >= MAX_TX_TELEGRAMS) {
LOG_WARNING("Tx queue overflow, skip one message");
if (tx_telegrams_.front().telegram_->operation == Telegram::Operation::TX_WRITE) {
telegram_write_fail_count_++;
} else {
telegram_read_fail_count_++;
}
tx_telegrams_.pop_front();
}
if (front) {
tx_telegrams_.emplace_front(tx_telegram_id_++, std::move(telegram), false, validateid); // add to front of queue
} else {
tx_telegrams_.emplace_back(tx_telegram_id_++, std::move(telegram), false, validateid); // add to back of queue
}
if (validateid != 0) {
EMSESP::wait_validate(validateid);
}
}
// builds a Tx telegram and adds to queue
// this is used by the retry() function to put the last failed Tx back into the queue
// format is EMS 1.0 (src, dest, type_id, offset, data)
// length is the length of the whole telegram data, excluding the CRC
void TxService::add(uint8_t operation, const uint8_t * data, const uint8_t length, const uint16_t validateid, const bool front) {
// check length
if (length < 5) {
return;
}
// build header. src, dest and offset have fixed positions
uint8_t src = operation == Telegram::Operation::TX_RAW && data[0] ? data[0] : ems_bus_id();
uint8_t dest = data[1];
uint8_t offset = data[3];
uint16_t validate_id = validateid;
uint16_t type_id;
const uint8_t * message_data; // where the message block starts
uint8_t message_length; // length of the message block, excluding CRC
// work out depending on the type, where the data message block starts and the message length
// same logic as in RxService::add(), but adjusted for no appended CRC
if (data[2] != 0xFF) {
// EMS 1.0
type_id = data[2];
message_data = data + 4;
message_length = length - 4;
} else if (data[1] & 0x80) {
type_id = (data[5] << 8) + data[6] + 256;
message_data = data + 4;
message_length = 1;
} else {
// EMS 2.0 / EMS+
type_id = (data[4] << 8) + data[5] + 256;
message_data = data + 6;
message_length = length - 6;
}
// if we don't have a type_id or empty data block, exit
if ((type_id == 0) || (message_length == 0)) {
LOG_DEBUG("Tx telegram type %d failed, length %d", type_id, message_length);
return;
}
if (operation == Telegram::Operation::TX_RAW) {
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) {
// 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;
}
}
auto telegram = std::make_shared<Telegram>(operation, src, dest, type_id, offset, message_data, message_length); // operation is TX_WRITE or TX_READ
// if the queue is full, make room by removing the last one
if (tx_telegrams_.size() >= MAX_TX_TELEGRAMS) {
LOG_WARNING("Tx queue overflow, skip one message");
if (tx_telegrams_.front().telegram_->operation == Telegram::Operation::TX_WRITE) {
telegram_write_fail_count_++;
} else {
telegram_read_fail_count_++;
}
tx_telegrams_.pop_front();
}
LOG_DEBUG("New Tx [#%d] telegram, length %d", tx_telegram_id_, message_length);
if (front && (operation != Telegram::Operation::TX_RAW || EMSESP::response_id() == 0)) {
tx_telegrams_.emplace_front(tx_telegram_id_++, std::move(telegram), false, validate_id); // add to front of queue
} else {
tx_telegrams_.emplace_back(tx_telegram_id_++, std::move(telegram), false, validate_id); // add to back of queue
}
if (validate_id != 0) {
EMSESP::wait_validate(validate_id);
}
}
// send a Tx telegram to request data from an EMS device
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 = 0xFF;
if (length > 0 && length < message_data) {
message_data = length;
}
add(Telegram::Operation::TX_READ, dest, type_id, offset, &message_data, 1, 0, front);
}
// Send a raw telegram to the bus, telegram is a text string of hex values
bool TxService::send_raw(const char * telegram_data) {
if (telegram_data == nullptr) {
return false;
}
// since the telegram data is a const, make a copy. add 1 to grab the \0 EOS
char telegram[strlen(telegram_data) + 1];
strlcpy(telegram, telegram_data, sizeof(telegram));
uint8_t count = 0;
uint8_t data[2 + strlen(telegram) / 3];
// get values
char * p = strtok(telegram, " ,"); // delimiter
while (p != nullptr) {
data[count++] = (uint8_t)strtol(p, 0, 16);
p = strtok(nullptr, " ,");
}
// check valid length
if (count < 4) {
return false;
}
add(Telegram::Operation::TX_RAW, data, count, 0, true); // add to top/front of Tx queue
return true;
}
// add last Tx to tx queue and increment count
// returns retry count, or 0 if all done
void TxService::retry_tx(const uint8_t operation, const uint8_t * data, const uint8_t length) {
// have we reached the limit? if so, reset count and give up
if (++retry_count_ > MAXIMUM_TX_RETRIES) {
reset_retry_count(); // give up
EMSESP::wait_validate(0); // do not wait for validation
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
} else {
increment_telegram_write_fail_count(); // another Tx fail
}
LOG_ERROR("Last Tx %s operation failed after %d retries. Ignoring request: %s",
(operation == Telegram::Operation::TX_WRITE) ? "Write" : "Read",
MAXIMUM_TX_RETRIES,
telegram_last_->to_string().c_str());
if (operation == Telegram::Operation::TX_READ) {
EMSESP::rxservice_.add_empty(telegram_last_->dest, telegram_last_->src, telegram_last_->type_id, telegram_last_->offset);
}
return;
}
LOG_DEBUG("Last Tx %s operation failed. Retry #%d. sent message: %s, received: %s",
(operation == Telegram::Operation::TX_WRITE) ? ("Write") : ("Read"),
retry_count_,
telegram_last_->to_string().c_str(),
Helpers::data_to_hex(data, length - 1).c_str());
// add to the top of the queue
if (tx_telegrams_.size() >= MAX_TX_TELEGRAMS) {
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());
}
// 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 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 requests and have not_set values intermediate (boiler 0xEA)
// We have the 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;
}
// checks if a telegram is sent to us matches the last Tx request
// incoming Rx src must match the last Tx dest
// and incoming Rx dest must be us (our ems_bus_id)
// for both src and dest we strip the MSB 8th bit
// returns true if the src/dest match the last Tx sent
bool TxService::is_last_tx(const uint8_t src, const uint8_t dest) const {
return (((telegram_last_->dest & 0x7F) == (src & 0x7F)) && ((dest & 0x7F) == ems_bus_id()));
}
// sends a type_id read request to fetch values after a successful Tx write operation
// unless the post_send_query has a type_id of 0
uint16_t TxService::post_send_query() {
uint16_t post_typeid = this->get_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 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
// delay the request if we have a different type_id for post_send_query
delayed_send_ = (this->telegram_last_->type_id == post_typeid) ? 0 : (uuid::get_uptime() + POST_SEND_DELAY);
}
return post_typeid;
}
} // namespace emsesp

458
src/core/telegram.h Normal file
View File

@@ -0,0 +1,458 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESP_TELEGRAM_H
#define EMSESP_TELEGRAM_H
#include <string>
#include <deque>
#include <uuid/log.h>
// UART drivers
#if defined(ESP32)
#include "uart/emsuart_esp32.h"
#elif defined(EMSESP_STANDALONE)
#include "emsuart_standalone.h"
#endif
#include "helpers.h"
#if defined(EMSESP_STANDALONE)
#define MAX_RX_TELEGRAMS 100 // size of Rx queue
#define MAX_TX_TELEGRAMS 200 // size of Tx queue
#else
#define MAX_RX_TELEGRAMS 10 // size of Rx queue
#define MAX_TX_TELEGRAMS 100 // size of Tx queue
#endif
// default values for null values
static constexpr uint8_t EMS_VALUE_BOOL = 0xFF; // used to mark that something is a boolean
static constexpr uint8_t EMS_VALUE_BOOL_OFF = 0x00; // boolean false
static constexpr uint8_t EMS_VALUE_BOOL_ON = 0x01; // boolean true. True can be 0x01 or 0xFF sometimes
static constexpr uint8_t EMS_VALUE_BOOL_NOTSET = 0xFE; // random number for booleans, that's not 0, 1 or FF
static constexpr uint8_t EMS_VALUE_UINT8_NOTSET = 0xFF; // for 8-bit unsigned ints/bytes
static constexpr int8_t EMS_VALUE_INT8_NOTSET = 0x7F; // for signed 8-bit ints/bytes
static constexpr uint16_t EMS_VALUE_UINT16_NOTSET = 0x7D00; // 32000: for 2-byte unsigned shorts
static constexpr int16_t EMS_VALUE_INT16_NOTSET = 0x7D00; // 32000: for 2-byte signed shorts
static constexpr uint32_t EMS_VALUE_UINT24_NOTSET = 0x00FFFFFF; // for 3-byte longs
static constexpr uint32_t EMS_VALUE_UINT32_NOTSET = 0xFFFFFF00; // for 4-byte longs
static constexpr uint8_t EMS_MAX_TELEGRAM_LENGTH = 32; // max length of a complete EMS telegram
static constexpr uint8_t EMS_MAX_TELEGRAM_MESSAGE_LENGTH = 27; // max length of message block, assuming EMS1.0
#define EMS_VALUE_DEFAULT_INT8 EMS_VALUE_INT8_NOTSET
#define EMS_VALUE_DEFAULT_UINT8 EMS_VALUE_UINT8_NOTSET
#define EMS_VALUE_DEFAULT_INT16 EMS_VALUE_INT16_NOTSET
#define EMS_VALUE_DEFAULT_UINT16 EMS_VALUE_UINT16_NOTSET
#define EMS_VALUE_DEFAULT_UINT24 EMS_VALUE_UINT24_NOTSET
#define EMS_VALUE_DEFAULT_UIN32 EMS_VALUE_UINT32_NOTSET
#define EMS_VALUE_DEFAULT_BOOL EMS_VALUE_BOOL_NOTSET
#define EMS_VALUE_DEFAULT_ENUM EMS_VALUE_UINT8_NOTSET
// used when System::test_set_all_active() is set
#define EMS_VALUE_DEFAULT_INT8_DUMMY 11
#define EMS_VALUE_DEFAULT_UINT8_DUMMY -12
#define EMS_VALUE_DEFAULT_INT16_DUMMY -1234
#define EMS_VALUE_DEFAULT_UINT16_DUMMY 1235
#define EMS_VALUE_DEFAULT_UINT24_DUMMY 12456
#define EMS_VALUE_DEFAULT_UINT32_DUMMY 124567
#define EMS_VALUE_DEFAULT_BOOL_DUMMY 1
#define EMS_VALUE_DEFAULT_ENUM_DUMMY 1
namespace emsesp {
class Telegram {
public:
Telegram(const uint8_t operation,
const uint8_t src,
const uint8_t dest,
const uint16_t type_id,
const uint8_t offset,
const uint8_t * message_data,
const uint8_t message_length);
~Telegram() = default;
const uint8_t operation; // is Operation mode
const uint8_t src; // device_id
const uint8_t dest; // device_id
const uint16_t type_id;
const uint8_t offset;
const uint8_t message_length;
uint8_t message_data[EMS_MAX_TELEGRAM_MESSAGE_LENGTH];
enum Operation : uint8_t {
NONE = 0,
RX,
RX_READ,
TX_RAW,
TX_READ,
TX_WRITE,
};
std::string to_string_message() const;
std::string to_string() const;
// reads a bit value from a given telegram position
bool read_bitvalue(uint8_t & value, const uint8_t index, const uint8_t bit) const {
uint8_t abs_index = (index - this->offset);
if ((abs_index >= this->message_length) || (abs_index > EMS_MAX_TELEGRAM_MESSAGE_LENGTH)) {
return false; // out of bounds
}
uint8_t val = value;
value = (uint8_t)(((this->message_data[abs_index]) >> bit) & 0x01);
return (val != value);
}
// read a value from a telegram if its not out of bounds.
// Then we update the value, regardless if its garbage
template <typename Value>
// assuming negative numbers are stored as 2's-complement
// https://medium.com/@LeeJulija/how-integers-are-stored-in-memory-using-twos-complement-5ba04d61a56c
// 2-compliment : https://www.rapidtables.com/convert/number/decimal-to-hex.html
// https://en.wikipedia.org/wiki/Two%27s_complement
// s is to override number of bytes read (e.g. use 3 to simulate a uint24_t)
bool read_value(Value & value, const uint8_t index, uint8_t s = 0) const {
uint8_t num_bytes = (!s) ? sizeof(Value) : s;
// check for out of bounds, if so don't modify the value
auto msg_size = (index - this->offset + num_bytes - 1);
if ((index < this->offset) || (msg_size >= this->message_length) || (msg_size > EMS_MAX_TELEGRAM_MESSAGE_LENGTH)) {
return false;
}
Value val = value;
value = 0;
for (uint8_t i = 0; i < num_bytes; i++) {
value = (value << 8) + this->message_data[index - this->offset + i]; // shift by byte
}
return (val != value);
}
bool read_enumvalue(uint8_t & value, const uint8_t index, int8_t start = 0) const {
if ((index < this->offset) || ((index - this->offset) >= this->message_length)) {
return false;
}
uint8_t val = value;
value = this->message_data[index - this->offset] - start;
return (val != value);
}
private:
int8_t _getDataPosition(const uint8_t index, const uint8_t size) const;
};
class EMSbus {
public:
static uuid::log::Logger logger_;
static constexpr uint8_t EMS_MASK_UNSET = 0xFF; // EMS bus type (budrus/junkers) hasn't been detected yet
static constexpr uint8_t EMS_MASK_HT3 = 0x80; // EMS bus type Junkers/HT3
static constexpr uint8_t EMS_MASK_BUDERUS = 0xFF; // EMS bus type Buderus
static constexpr uint8_t EMS_TX_ERROR_LIMIT = 10; // % limit of failed Tx read/write attempts before showing a warning
static bool is_ht3() {
return (ems_mask_ == EMS_MASK_HT3);
}
static uint8_t ems_mask() {
return ems_mask_;
}
static void ems_mask(uint8_t ems_mask) {
ems_mask_ = ems_mask & 0x80; // only keep the MSB (8th bit)
}
static uint8_t tx_mode() {
return tx_mode_;
}
static void tx_mode(uint8_t tx_mode) {
tx_mode_ = tx_mode;
}
static uint8_t ems_bus_id() {
return ems_bus_id_;
}
static void ems_bus_id(uint8_t ems_bus_id) {
ems_bus_id_ = ems_bus_id;
}
// checks every 30 seconds if the EMS bus is still alive
static bool bus_connected() {
#if defined(EMSESP_STANDALONE) || defined(EMSESP_TEST)
return true;
#else
if ((uuid::get_uptime() - last_bus_activity_) > EMS_BUS_TIMEOUT) {
bus_connected_ = false;
}
return bus_connected_;
#endif
}
// sets the flag for EMS bus connected
static void last_bus_activity(uint32_t timestamp) {
// record the first time we connected to the BUS, as this will be our uptime
if (!last_bus_activity_) {
bus_uptime_start_ = timestamp;
}
last_bus_activity_ = timestamp;
bus_connected_ = true;
}
// return bus uptime in seconds
static uint32_t bus_uptime() {
if (!bus_uptime_start_) {
return 0; // not yet initialized
}
return (uint32_t)((uuid::get_uptime() - bus_uptime_start_) / 1000ULL);
}
static uint8_t tx_state() {
return tx_state_;
}
static void tx_state(uint8_t tx_state) {
tx_state_ = tx_state;
}
static uint8_t calculate_crc(const uint8_t * data, const uint8_t length);
private:
static constexpr uint32_t EMS_BUS_TIMEOUT = 30000; // timeout in ms before recognizing the ems bus is offline (30 seconds)
static uint32_t last_bus_activity_; // timestamp of last time a valid Rx came in
static uint32_t bus_uptime_start_; // timestamp of first time we connected to the bus
static bool bus_connected_; // start assuming the bus hasn't been connected
static uint8_t ems_mask_; // unset=0xFF, buderus=0x00, junkers/ht3=0x80
static uint8_t ems_bus_id_; // the bus id, which configurable and stored in settings
static uint8_t tx_mode_; // local copy of the tx mode
static uint8_t tx_state_; // state of the Tx line (NONE or waiting on a TX_READ or TX_WRITE)
};
class RxService : public EMSbus {
public:
RxService() = default;
~RxService() = default;
void loop();
void add(uint8_t * data, uint8_t length);
void add_empty(const uint8_t src, const uint8_t dst, const uint16_t type_id, uint8_t offset);
uint32_t telegram_count() const {
return telegram_count_;
}
void increment_telegram_count() {
telegram_count_++;
}
uint32_t telegram_error_count() const {
return telegram_error_count_;
}
// returns a %
uint8_t quality() const {
if (telegram_error_count_ == 0) {
return 100; // all good, 100%
}
uint8_t q = (telegram_error_count_ * 100 / (telegram_count_ + telegram_error_count_));
return (q <= EMS_BUS_QUALITY_RX_THRESHOLD ? 100 : 100 - q);
}
struct QueuedRxTelegram {
public:
const uint16_t id_;
const std::shared_ptr<const Telegram> telegram_;
~QueuedRxTelegram() = default;
// removed && from telegram in 3.7.0-dev.43
QueuedRxTelegram(uint16_t id, std::shared_ptr<Telegram> telegram)
: id_(id)
, telegram_(std::move(telegram)) {
}
};
std::deque<QueuedRxTelegram> queue() const {
return rx_telegrams_;
}
private:
static constexpr uint8_t EMS_BUS_QUALITY_RX_THRESHOLD = 5; // % threshold before reporting quality issues
uint8_t rx_telegram_id_ = 0; // queue counter
uint32_t telegram_count_ = 0; // # Rx received
uint32_t telegram_error_count_ = 0; // # Rx CRC errors
std::shared_ptr<const Telegram> rx_telegram; // the incoming Rx telegram
std::deque<QueuedRxTelegram> rx_telegrams_; // the Rx Queue
};
class TxService : public EMSbus {
public:
static constexpr uint8_t TX_WRITE_FAIL = 4; // EMS return code for fail
static constexpr uint8_t TX_WRITE_SUCCESS = 1; // EMS return code for success
TxService() = default;
~TxService() = default;
void start();
void send();
uint8_t get_send_id();
void add(const uint8_t operation,
const uint8_t dest,
const uint16_t type_id,
const uint8_t offset,
uint8_t * message_data,
const uint8_t message_length,
const uint16_t validateid,
const bool front = false);
void add(const uint8_t operation, const uint8_t * data, const uint8_t length, const uint16_t validateid, const bool front = false);
void read_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset = 0, const uint8_t length = 0, const bool readId = false);
bool send_raw(const char * telegram_data);
void send_poll() const;
void retry_tx(const uint8_t operation, const uint8_t * data, const uint8_t length);
bool is_last_tx(const uint8_t src, const uint8_t dest) const;
uint16_t post_send_query();
uint16_t read_next_tx(const uint8_t offset, const uint8_t length);
uint8_t retry_count() const {
return retry_count_;
}
void reset_retry_count() {
retry_count_ = 0;
}
void set_post_send_query(uint16_t type_id) {
telegram_last_post_send_query_ = type_id;
}
uint16_t get_post_send_query() const {
return telegram_last_post_send_query_;
}
uint32_t telegram_read_count() const {
return telegram_read_count_;
}
uint32_t telegram_write_count() const {
return telegram_write_count_;
}
void telegram_read_count(uint32_t telegram_read_count) {
telegram_read_count_ = telegram_read_count;
}
void telegram_write_count(uint32_t telegram_write_count) {
telegram_write_count_ = telegram_write_count;
}
void increment_telegram_read_count() {
telegram_read_count_++;
}
void increment_telegram_write_count() {
telegram_write_count_++;
}
uint32_t telegram_read_fail_count() const {
return telegram_read_fail_count_;
}
uint32_t telegram_write_fail_count() const {
return telegram_write_fail_count_;
}
void telegram_fail_count(uint32_t telegram_fail_count) {
telegram_read_fail_count_ = telegram_fail_count;
telegram_write_fail_count_ = telegram_fail_count;
}
uint8_t read_quality() const {
if (telegram_read_fail_count_ == 0) {
return 100; // all good, 100%
}
return (100 - (uint8_t)(telegram_read_fail_count_ * 100 / (telegram_read_fail_count_ + telegram_read_count_)));
}
uint8_t write_quality() const {
if (telegram_write_fail_count_ == 0) {
return 100; // all good, 100%
}
return (100 - (uint8_t)(telegram_write_fail_count_ * 100 / (telegram_write_fail_count_ + telegram_write_count_)));
}
void increment_telegram_read_fail_count() {
telegram_read_fail_count_++;
}
void increment_telegram_write_fail_count() {
telegram_write_fail_count_++;
}
struct QueuedTxTelegram {
const uint16_t id_;
const std::shared_ptr<const Telegram> telegram_;
const bool retry_; // true if its a retry
const uint16_t validateid_;
~QueuedTxTelegram() = default;
// replaced && im std::shared_ptr<Telegram> telegram in 3.7.0-dev.43
QueuedTxTelegram(uint16_t id, std::shared_ptr<Telegram> telegram, bool retry, uint16_t validateid)
: id_(id)
, telegram_(std::move(telegram))
, retry_(retry)
, validateid_(validateid) {
}
};
std::deque<QueuedTxTelegram> queue() const {
return tx_telegrams_;
}
bool tx_queue_empty() const {
return tx_telegrams_.empty();
}
static constexpr uint8_t MAXIMUM_TX_RETRIES = 3;
static constexpr uint32_t POST_SEND_DELAY = 2000;
private:
std::deque<QueuedTxTelegram> tx_telegrams_; // the Tx queue
uint32_t telegram_read_count_ = 0; // # Tx successful reads
uint32_t telegram_write_count_ = 0; // # Tx successful writes
uint32_t telegram_read_fail_count_ = 0; // # Tx unsuccessful transmits
uint32_t telegram_write_fail_count_ = 0; // # Tx unsuccessful transmits
std::shared_ptr<Telegram> telegram_last_;
uint16_t telegram_last_post_send_query_; // which type ID to query after a successful send, to read back the values just written
uint8_t retry_count_ = 0; // count for # Tx retries
uint32_t delayed_send_ = 0; // manage delay for post send query
uint8_t tx_telegram_id_ = 0; // queue counter
void send_telegram(const QueuedTxTelegram & tx_telegram);
};
} // namespace emsesp
#endif

View File

@@ -0,0 +1,613 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// code originally written by nomis - https://github.com/nomis
#include "temperaturesensor.h"
#include "emsesp.h"
#ifdef ESP32
#define YIELD
#else
#define YIELD yield()
#endif
namespace emsesp {
uuid::log::Logger TemperatureSensor::logger_{F_(temperaturesensor), uuid::log::Facility::DAEMON};
// start the 1-wire
void TemperatureSensor::start() {
reload();
if (!dallas_gpio_) {
sensors_.clear();
return; // disabled if dallas gpio is 0
}
#ifndef EMSESP_STANDALONE
bus_.begin(dallas_gpio_);
LOG_INFO("Starting Temperature Sensor service");
#endif
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
}
// load settings
void TemperatureSensor::reload() {
// load the service settings
EMSESP::webSettingsService.read([&](WebSettings const & settings) {
dallas_gpio_ = settings.dallas_gpio;
parasite_ = settings.dallas_parasite;
});
for (auto & sensor : sensors_) {
remove_ha_topic(sensor.id());
sensor.ha_registered = false; // force HA configs to be re-created
}
}
void TemperatureSensor::loop() {
if (!dallas_gpio_) {
return; // dallas gpio is 0 (disabled)
}
#ifndef EMSESP_STANDALONE
uint32_t time_now = uuid::get_uptime();
if (state_ == State::IDLE) {
if (time_now - last_activity_ >= READ_INTERVAL_MS) {
#ifdef EMSESP_DEBUG_SENSOR
LOG_DEBUG("Read sensor temperature");
#endif
if (bus_.reset() || parasite_) {
YIELD;
bus_.skip();
bus_.write(CMD_CONVERT_TEMP, parasite_ ? 1 : 0);
state_ = State::READING;
scanretry_ = 0;
} else {
// no sensors found
if (sensors_.size()) {
sensorfails_++;
if (++scanretry_ > SCAN_MAX) { // every 30 sec
scanretry_ = 0;
#ifdef EMSESP_DEBUG_SENSOR
LOG_DEBUG("Error: Bus reset failed");
#endif
#ifndef EMSESP_TEST
// don't reset if running in test mode where we simulate sensors
for (auto & sensor : sensors_) {
sensor.temperature_c = EMS_VALUE_INT16_NOTSET;
}
#endif
}
}
}
last_activity_ = time_now;
}
} else if (state_ == State::READING) {
if (temperature_convert_complete() && (time_now - last_activity_ > CONVERSION_MS)) {
#ifdef EMSESP_DEBUG_SENSOR
LOG_DEBUG("Scanning for temperature sensors");
#endif
bus_.reset_search();
state_ = State::SCANNING;
} else if (time_now - last_activity_ > READ_TIMEOUT_MS) {
#ifdef EMSESP_DEBUG_SENSOR
LOG_WARNING("Sensor read timeout");
#endif
state_ = State::IDLE;
sensorfails_++;
}
} else if (state_ == State::SCANNING) {
if (time_now - last_activity_ > SCAN_TIMEOUT_MS) {
#ifdef EMSESP_DEBUG_SENSOR
LOG_ERROR("Sensor scan timeout");
#endif
state_ = State::IDLE;
sensorfails_++;
} else {
uint8_t addr[ADDR_LEN] = {0};
if (bus_.search(addr)) {
if (!parasite_) {
bus_.depower();
}
if (bus_.crc8(addr, ADDR_LEN - 1) == addr[ADDR_LEN - 1]) {
switch (addr[0]) {
case TYPE_DS18B20:
case TYPE_DS18S20:
case TYPE_DS1822:
case TYPE_DS1825:
int16_t t;
t = get_temperature_c(addr);
if ((t >= -550) && (t <= 1250)) {
sensorreads_++;
// check if we already have this sensor
bool found = false;
for (auto & sensor : sensors_) {
if (sensor.internal_id() == get_id(addr)) {
t += sensor.offset();
if (t != sensor.temperature_c) {
sensor.temperature_c = t;
publish_sensor(sensor);
changed_ |= true;
}
sensor.read = true;
found = true;
break;
}
}
// add new sensor. this will create the id string, empty name and offset
if (!found && (sensors_.size() < (MAX_SENSORS - 1))) {
sensors_.emplace_back(addr);
sensors_.back().read = true;
changed_ = true;
// look in the customization service for an optional alias or offset for that particular sensor
sensors_.back().apply_customization();
sensors_.back().temperature_c = t + sensors_.back().offset();
publish_sensor(sensors_.back()); // call publish single
// sort the sensors based on name
// std::sort(sensors_.begin(), sensors_.end(), [](const Sensor & a, const Sensor & b) { return a.name() < b.name(); });
}
} else {
sensorfails_++;
}
break;
default:
sensorfails_++;
LOG_ERROR("Unknown sensor %s", Sensor(addr).id().c_str());
break;
}
} else {
sensorfails_++;
LOG_ERROR("Invalid sensor %s", Sensor(addr).id().c_str());
}
} else {
if (!parasite_) {
bus_.depower();
}
// check for missing sensors after some samples
// but don't do this if running in test mode where we simulate sensors
if (++scancnt_ > SCAN_MAX) {
for (auto & sensor : sensors_) {
if (!sensor.read) {
sensor.temperature_c = EMS_VALUE_INT16_NOTSET;
changed_ = true;
}
sensor.read = false;
}
scancnt_ = 0;
} else if (scancnt_ == SCAN_START + 1) { // startup
firstscan_ = sensors_.size();
// LOG_DEBUG("Adding %d sensor(s) from first scan", firstscan_);
} else if ((scancnt_ <= 0) && (firstscan_ != sensors_.size())) { // check 2 times for no change of sensor #
scancnt_ = SCAN_START;
sensors_.clear(); // restart scanning and clear to get correct numbering
}
state_ = State::IDLE;
}
}
}
#endif
}
bool TemperatureSensor::temperature_convert_complete() {
#ifndef EMSESP_STANDALONE
if (parasite_) {
return true; // don't care, use the minimum time in loop
}
return bus_.read_bit() == 1;
#else
return true;
#endif
}
int16_t TemperatureSensor::get_temperature_c(const uint8_t addr[]) {
#ifndef EMSESP_STANDALONE
if (!bus_.reset()) {
LOG_ERROR("Bus reset failed before reading scratchpad from %s", Sensor(addr).id().c_str());
return EMS_VALUE_INT16_NOTSET;
}
YIELD;
uint8_t scratchpad[SCRATCHPAD_LEN] = {0};
bus_.select(addr);
bus_.write(CMD_READ_SCRATCHPAD);
bus_.read_bytes(scratchpad, SCRATCHPAD_LEN);
YIELD;
if (!bus_.reset()) {
LOG_ERROR("Bus reset failed after reading scratchpad from %s", Sensor(addr).id().c_str());
return EMS_VALUE_INT16_NOTSET;
}
YIELD;
if (bus_.crc8(scratchpad, SCRATCHPAD_LEN - 1) != scratchpad[SCRATCHPAD_LEN - 1]) {
LOG_WARNING("Invalid scratchpad CRC: %02X%02X%02X%02X%02X%02X%02X%02X%02X from sensor %s",
scratchpad[0],
scratchpad[1],
scratchpad[2],
scratchpad[3],
scratchpad[4],
scratchpad[5],
scratchpad[6],
scratchpad[7],
scratchpad[8],
Sensor(addr).id().c_str());
return EMS_VALUE_INT16_NOTSET;
}
int16_t raw_value = ((int16_t)scratchpad[SCRATCHPAD_TEMP_MSB] << 8) | scratchpad[SCRATCHPAD_TEMP_LSB];
if (addr[0] == TYPE_DS18S20) {
raw_value = (raw_value << 3) + 12 - scratchpad[SCRATCHPAD_CNT_REM];
} else {
// Adjust based on sensor resolution
int resolution = 9 + ((scratchpad[SCRATCHPAD_CONFIG] >> 5) & 0x3);
switch (resolution) {
case 9:
raw_value &= ~0x7;
break;
case 10:
raw_value &= ~0x3;
break;
case 11:
raw_value &= ~0x1;
break;
default: // 12
break;
}
}
raw_value = ((int32_t)raw_value * 625 + 500) / 1000; // round to 0.1
return raw_value;
#else
return EMS_VALUE_INT16_NOTSET;
#endif
}
// update temperature sensor information name and offset
bool TemperatureSensor::update(const std::string & id, const std::string & name, int16_t offset) {
// find the sensor
for (auto & sensor : sensors_) {
if (sensor.id() == id) {
// found a match, update the sensor object
// if HA is enabled then delete the old record
if (Mqtt::ha_enabled()) {
remove_ha_topic(id);
sensor.ha_registered = false; // force HA configs to be re-created
}
sensor.set_name(name);
sensor.set_offset(offset);
// store the new name and offset in our configuration
EMSESP::webCustomizationService.update([&id, &name, &offset, &sensor](WebCustomization & settings) {
// look it up to see if it exists
bool found = false;
for (auto & SensorCustomization : settings.sensorCustomizations) {
if (SensorCustomization.id == id) {
SensorCustomization.name = name;
SensorCustomization.offset = offset;
found = true;
LOG_DEBUG("Customizing existing sensor ID %s", id.c_str());
break;
}
}
if (!found) {
auto newSensor = SensorCustomization();
newSensor.id = id;
newSensor.name = name;
newSensor.offset = offset;
settings.sensorCustomizations.push_back(newSensor);
LOG_DEBUG("Adding new customization for sensor ID %s", id.c_str());
}
sensor.ha_registered = false; // it's changed so we may need to recreate the HA config
return StateUpdateResult::CHANGED;
});
return true;
}
}
return true; // not found, nothing updated
}
// check to see if values have been updated
bool TemperatureSensor::updated_values() {
if (changed_) {
changed_ = false;
return true;
}
return false;
}
// called from emsesp.cpp for commands
bool TemperatureSensor::get_value_info(JsonObject output, const char * cmd, const int8_t id) {
// return empty json if there are no sensors
if (sensors_.empty()) {
return true;
}
if (!strcmp(cmd, F_(info)) || !strcmp(cmd, F_(values))) {
for (const auto & sensor : sensors_) {
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;
}
if (!strcmp(cmd, F_(entities))) {
for (const auto & sensor : sensors_) {
get_value_json(output[sensor.name()].to<JsonObject>(), sensor);
}
return true;
}
// this is for a specific sensor
const char * attribute_s = Command::get_attribute(cmd);
for (const auto & sensor : sensors_) {
// match custom name or sensor ID
if (cmd == Helpers::toLower(sensor.name()) || cmd == Helpers::toLower(sensor.id())) {
get_value_json(output, sensor);
return Command::set_attribute(output, cmd, attribute_s);
}
}
return false; // not found
}
void TemperatureSensor::get_value_json(JsonObject output, const Sensor & sensor) {
output["id"] = sensor.id();
output["name"] = sensor.name();
output["fullname"] = 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["readable"] = true;
output["writeable"] = false;
output["visible"] = true;
}
// publish a single sensor to MQTT
void TemperatureSensor::publish_sensor(const Sensor & sensor) {
if (Mqtt::enabled() && Mqtt::publish_single()) {
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
if (Mqtt::publish_single2cmd()) {
snprintf(topic, sizeof(topic), "%s/%s", F_(temperaturesensor), sensor.name().c_str());
} else {
snprintf(topic, sizeof(topic), "%s%s/%s", F_(temperaturesensor), "_data", sensor.name().c_str());
}
char payload[10];
Mqtt::queue_publish(topic, Helpers::render_value(payload, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0));
}
char cmd[COMMAND_MAX_LENGTH];
snprintf(cmd, sizeof(cmd), "%s/%s", F_(temperaturesensor), sensor.name().c_str());
EMSESP::webSchedulerService.onChange(cmd);
}
// send empty config topic to remove the entry from HA
void TemperatureSensor::remove_ha_topic(const std::string & id) {
if (!Mqtt::ha_enabled()) {
return;
}
LOG_DEBUG("Removing HA config for temperature sensor ID %s", id.c_str());
// use '_' as HA doesn't like '-' in the topic name
std::string sensorid = id;
std::replace(sensorid.begin(), sensorid.end(), '-', '_');
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf(topic, sizeof(topic), "sensor/%s/temperaturesensor_%s/config", Mqtt::basename().c_str(), sensorid.c_str());
Mqtt::queue_remove_topic(topic);
}
// send all temperature sensor values as a JSON package to MQTT
void TemperatureSensor::publish_values(const bool force) {
if (!Mqtt::enabled()) {
return;
}
uint8_t num_sensors = sensors_.size();
if (num_sensors == 0) {
return;
}
if (force && Mqtt::publish_single()) {
for (const auto & sensor : sensors_) {
publish_sensor(sensor);
}
}
JsonDocument doc;
for (auto & sensor : sensors_) {
bool has_value = Helpers::hasValue(sensor.temperature_c);
if (has_value) {
char val[10];
if (Mqtt::is_nested()) {
JsonObject dataSensor = doc[sensor.id()].to<JsonObject>();
dataSensor["name"] = sensor.name();
dataSensor["temp"] = serialized(Helpers::render_value(val, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0));
} else {
doc[sensor.name()] = serialized(Helpers::render_value(val, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0));
}
}
// create the HA MQTT config
// to e.g. homeassistant/sensor/ems-esp/temperaturesensor_28-233D-9497-0C03/config
if (Mqtt::ha_enabled()) {
if (!has_value && sensor.ha_registered) {
remove_ha_topic(sensor.id());
sensor.ha_registered = false;
} else if (!sensor.ha_registered || force) {
LOG_DEBUG("Recreating HA config for sensor ID %s", sensor.id().c_str());
JsonDocument config;
config["dev_cla"] = "temperature";
config["stat_cla"] = "measurement";
char stat_t[50];
snprintf(stat_t, sizeof(stat_t), "%s/%s_data", Mqtt::base().c_str(), F_(temperaturesensor)); // use base path
config["stat_t"] = stat_t;
config["unit_of_meas"] = EMSdevice::uom_to_string(DeviceValueUOM::DEGREES);
char val_obj[70];
char val_cond[170];
if (Mqtt::is_nested()) {
snprintf(val_obj, sizeof(val_obj), "value_json['%s']['temp']", sensor.id().c_str());
snprintf(val_cond, sizeof(val_cond), "value_json['%s'] is defined and %s is defined", sensor.id().c_str(), val_obj);
} else {
snprintf(val_obj, sizeof(val_obj), "value_json['%s']", sensor.name().c_str());
snprintf(val_cond, sizeof(val_cond), "%s is defined", val_obj);
}
// for the value template, there's a problem still with Domoticz probably due to the special characters.
// See https://github.com/emsesp/EMS-ESP32/issues/1360
if (Mqtt::discovery_type() == Mqtt::discoveryType::HOMEASSISTANT) {
config["val_tpl"] = (std::string) "{{" + val_obj + " if " + val_cond + " else -55}}";
} else {
config["val_tpl"] = (std::string) "{{" + val_obj + "}}"; // omit value conditional Jinja2 template code
}
char uniq_s[70];
if (Mqtt::entity_format() == Mqtt::entityFormat::MULTI_SHORT) {
snprintf(uniq_s, sizeof(uniq_s), "%s_%s_%s", Mqtt::basename().c_str(), F_(temperaturesensor), sensor.id().c_str());
} else {
snprintf(uniq_s, sizeof(uniq_s), "%s_%s", F_(temperaturesensor), sensor.id().c_str());
}
config["obj_id"] = uniq_s;
config["uniq_id"] = uniq_s; // same as object_id/obj_id
char name[50];
snprintf(name, sizeof(name), "%s", sensor.name().c_str());
config["name"] = name;
// see if we need to create the [devs] discovery section, as this needs only to be done once for all sensors
bool is_ha_device_created = false;
for (const auto & sensor : sensors_) {
if (sensor.ha_registered) {
is_ha_device_created = true;
break;
}
}
Mqtt::add_ha_sections_to_doc("temperature", stat_t, config, !is_ha_device_created, val_cond);
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf(topic, sizeof(topic), "sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(temperaturesensor), sensor.id().c_str());
sensor.ha_registered = Mqtt::queue_ha(topic, config.as<JsonObject>());
}
}
}
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
snprintf(topic, sizeof(topic), "%s_data", F_(temperaturesensor));
Mqtt::queue_publish(topic, doc.as<JsonObject>());
}
// skip crc from id
TemperatureSensor::Sensor::Sensor(const uint8_t addr[])
: internal_id_(((uint64_t)addr[0] << 48) | ((uint64_t)addr[1] << 40) | ((uint64_t)addr[2] << 32) | ((uint64_t)addr[3] << 24) | ((uint64_t)addr[4] << 16)
| ((uint64_t)addr[5] << 8) | ((uint64_t)addr[6])) {
// create ID string
char id_s[20];
snprintf(id_s,
sizeof(id_s),
"%02X_%04X_%04X_%04X",
(unsigned int)(internal_id_ >> 48) & 0xFF,
(unsigned int)(internal_id_ >> 32) & 0xFFFF,
(unsigned int)(internal_id_ >> 16) & 0xFFFF,
(unsigned int)(internal_id_) & 0xFFFF);
id_ = std::string(id_s);
name_ = std::string{}; // name (alias) is empty
offset_ = 0; // 0 degrees offset
}
uint64_t TemperatureSensor::get_id(const uint8_t addr[]) {
return (((uint64_t)addr[0] << 48) | ((uint64_t)addr[1] << 40) | ((uint64_t)addr[2] << 32) | ((uint64_t)addr[3] << 24) | ((uint64_t)addr[4] << 16)
| ((uint64_t)addr[5] << 8) | ((uint64_t)addr[6]));
}
// find the name from the customization service
// if empty, return the ID as a string
std::string TemperatureSensor::Sensor::name() const {
if (name_.empty()) {
return id_;
}
return name_;
}
// look up in customization service for a specific sensor
// and set the name and offset from that entry if it exists
bool TemperatureSensor::Sensor::apply_customization() {
EMSESP::webCustomizationService.read([&](const WebCustomization & settings) {
auto const & sensors = settings.sensorCustomizations;
if (!sensors.empty()) {
for (const auto & sensor : sensors) {
if (id_ == sensor.id) {
LOG_DEBUG("Loading customization for temperature sensor %s", sensor.id.c_str());
set_name(sensor.name);
set_offset(sensor.offset);
return true;
}
}
}
return false;
});
return false; // not found, will use default ID as name and 0 for offset
}
// hard coded tests
#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().temperature_c = 123; // 12.3
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().temperature_c = 456; // 45.6
sensors_.back().read = true;
publish_sensor(sensors_.back()); // call publish single
}
#endif
} // namespace emsesp

View File

@@ -0,0 +1,177 @@
/*
* EMS-ESP - https://github.com/emsesp/EMS-ESP
* Copyright 2020-2024 emsesp.org - proddy, MichaelDvP
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// code originally written by nomis - https://github.com/nomis
#ifndef EMSESP_TEMPERATURESENSOR_H
#define EMSESP_TEMPERATURESENSOR_H
#include "helpers.h"
#include "mqtt.h"
#include "console.h"
#include <uuid/log.h>
#ifndef EMSESP_STANDALONE
#include <OneWire.h>
#endif
namespace emsesp {
class TemperatureSensor {
public:
class Sensor {
public:
Sensor(const uint8_t addr[]);
~Sensor() = default;
uint64_t internal_id() const {
return internal_id_;
}
std::string id() const {
return id_;
}
int16_t offset() const {
return offset_;
}
void set_offset(const int16_t offset) {
offset_ = offset;
}
std::string name() const;
void set_name(const std::string & name) {
name_ = name;
}
bool apply_customization();
// initial values
int16_t temperature_c = EMS_VALUE_INT16_NOTSET; // value is *10
bool read = false;
bool ha_registered = false;
private:
uint64_t internal_id_;
std::string id_;
std::string name_;
int16_t offset_;
};
TemperatureSensor() = default;
~TemperatureSensor() = default;
void start();
void loop();
void publish_sensor(const Sensor & sensor);
void publish_values(const bool force);
void reload();
bool updated_values();
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 {
return sensors_;
}
uint32_t reads() const {
return sensorreads_;
}
uint32_t fails() const {
return sensorfails_;
}
bool sensor_enabled() const {
return (dallas_gpio_ != 0);
}
bool have_sensors() const {
return (!sensors_.empty());
}
size_t count_entities() const {
return sensors_.size();
}
bool update(const std::string & id, const std::string & name, int16_t offset);
#if defined(EMSESP_TEST)
void test();
#endif
private:
static constexpr uint8_t MAX_SENSORS = 20;
enum class State { IDLE, READING, SCANNING };
static constexpr size_t ADDR_LEN = 8;
static constexpr size_t SCRATCHPAD_LEN = 9;
static constexpr size_t SCRATCHPAD_TEMP_MSB = 1;
static constexpr size_t SCRATCHPAD_TEMP_LSB = 0;
static constexpr size_t SCRATCHPAD_CONFIG = 4;
static constexpr size_t SCRATCHPAD_CNT_REM = 6;
// dallas chip types
static constexpr uint8_t TYPE_DS18B20 = 0x28;
static constexpr uint8_t TYPE_DS18S20 = 0x10;
static constexpr uint8_t TYPE_DS1822 = 0x22;
static constexpr uint8_t TYPE_DS1825 = 0x3B; // also DS1826
static constexpr uint32_t READ_INTERVAL_MS = 5000; // 5 seconds
static constexpr uint32_t CONVERSION_MS = 1000; // 1 seconds
static constexpr uint32_t READ_TIMEOUT_MS = 2000; // 2 seconds
static constexpr uint32_t SCAN_TIMEOUT_MS = 3000; // 3 seconds
static constexpr uint8_t CMD_CONVERT_TEMP = 0x44;
static constexpr uint8_t CMD_READ_SCRATCHPAD = 0xBE;
static constexpr int8_t SCAN_START = -3;
static constexpr int8_t SCAN_MAX = 5;
static uuid::log::Logger logger_;
bool temperature_convert_complete();
int16_t get_temperature_c(const uint8_t addr[]);
uint64_t get_id(const uint8_t addr[]);
void get_value_json(JsonObject output, const Sensor & sensor);
void remove_ha_topic(const std::string & id);
std::vector<Sensor> sensors_; // our list of active sensors
#ifndef EMSESP_STANDALONE
OneWire bus_;
uint32_t last_activity_ = uuid::get_uptime();
State state_ = State::IDLE;
int8_t scancnt_ = SCAN_START;
uint8_t firstscan_ = 0;
int8_t scanretry_ = 0;
#endif
uint8_t dallas_gpio_ = 0;
bool parasite_ = false;
bool changed_ = false;
uint32_t sensorfails_ = 0;
uint32_t sensorreads_ = 0;
};
} // namespace emsesp
#endif