diff --git a/gulpfile.js b/gulpfile.js index d635dbe..586b62f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -6,6 +6,7 @@ const cssnano = require('cssnano'); const terser = require('gulp-terser'); const jsonminify = require('gulp-jsonminify'); const htmlmin = require('gulp-html-minifier-terser'); +const replace = require('gulp-replace'); // Paths for tasks let paths = { @@ -59,6 +60,10 @@ const styles = (cb) => { const items = paths.styles.bundles[name]; src(items) + .pipe(replace( + "{BUILD_TIME}", + Math.floor(Date.now() / 1000) + )) .pipe(postcss([ cssnano({ preset: 'advanced' }) ])) @@ -77,6 +82,10 @@ const scripts = (cb) => { const items = paths.scripts.bundles[name]; src(items) + .pipe(replace( + "{BUILD_TIME}", + Math.floor(Date.now() / 1000) + )) .pipe(terser().on('error', console.error)) .pipe(concat(name)) .pipe(gzip({ @@ -93,6 +102,10 @@ const jsonFiles = (cb) => { const item = paths.json[i]; src(item.src) + .pipe(replace( + "{BUILD_TIME}", + Math.floor(Date.now() / 1000) + )) .pipe(jsonminify()) .pipe(gzip({ append: true @@ -119,6 +132,10 @@ const staticFiles = (cb) => { const pages = () => { return src(paths.pages.src) + .pipe(replace( + "{BUILD_TIME}", + Math.floor(Date.now() / 1000) + )) .pipe(htmlmin({ html5: true, caseSensitive: true, diff --git a/lib/BufferedWebServer/BufferedWebServer.h b/lib/BufferedWebServer/BufferedWebServer.h index 8c04c14..66edaed 100644 --- a/lib/BufferedWebServer/BufferedWebServer.h +++ b/lib/BufferedWebServer/BufferedWebServer.h @@ -10,7 +10,8 @@ class BufferedWebServer { free(this->buffer); } - void send(int code, const char* contentType, JsonDocument& content, bool pretty = false) { + template + void send(int code, T contentType, const JsonVariantConst content, bool pretty = false) { #ifdef ARDUINO_ARCH_ESP8266 if (!this->webServer->chunkedResponseModeStart(code, contentType)) { this->webServer->send(505, F("text/html"), F("HTTP1.1 required")); @@ -76,10 +77,19 @@ class BufferedWebServer { return; } - this->webServer->sendContent((const char*)this->buffer, this->bufferPos); + #ifdef ARDUINO_ARCH_ESP8266 + ::optimistic_yield(1000); + #endif + + auto& client = this->webServer->client(); + if (client.connected()) { + this->webServer->sendContent((const char*)this->buffer, this->bufferPos); + } + this->bufferPos = 0; + #ifdef ARDUINO_ARCH_ESP8266 - ::delay(0); + ::optimistic_yield(1000); #endif } diff --git a/lib/CustomOpenTherm/CustomOpenTherm.h b/lib/CustomOpenTherm/CustomOpenTherm.h index 600f59a..4ae34d8 100644 --- a/lib/CustomOpenTherm/CustomOpenTherm.h +++ b/lib/CustomOpenTherm/CustomOpenTherm.h @@ -98,66 +98,6 @@ class CustomOpenTherm : public OpenTherm { )); } - bool setHeatingCh1Temp(float temperature) { - unsigned long response = this->sendRequest(buildRequest( - OpenThermMessageType::WRITE_DATA, - OpenThermMessageID::TSet, - temperatureToData(temperature) - )); - - return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::TSet); - } - - bool setHeatingCh2Temp(float temperature) { - unsigned long response = this->sendRequest(buildRequest( - OpenThermMessageType::WRITE_DATA, - OpenThermMessageID::TsetCH2, - temperatureToData(temperature) - )); - - return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::TsetCH2); - } - - bool setDhwTemp(float temperature) { - unsigned long response = this->sendRequest(buildRequest( - OpenThermMessageType::WRITE_DATA, - OpenThermMessageID::TdhwSet, - temperatureToData(temperature) - )); - - return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::TdhwSet); - } - - bool setRoomSetpoint(float temperature) { - unsigned long response = this->sendRequest(buildRequest( - OpenThermMessageType::WRITE_DATA, - OpenThermMessageID::TrSet, - temperatureToData(temperature) - )); - - return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::TrSet); - } - - bool setRoomSetpointCh2(float temperature) { - unsigned long response = this->sendRequest(buildRequest( - OpenThermMessageType::WRITE_DATA, - OpenThermMessageID::TrSetCH2, - temperatureToData(temperature) - )); - - return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::TrSetCH2); - } - - bool setRoomTemp(float temperature) { - unsigned long response = this->sendRequest(buildRequest( - OpenThermMessageType::WRITE_DATA, - OpenThermMessageID::Tr, - temperatureToData(temperature) - )); - - return isValidResponse(response) && isValidResponseId(response, OpenThermMessageID::Tr); - } - bool sendBoilerReset() { unsigned int data = 1; data <<= 8; diff --git a/lib/HomeAssistantHelper/HomeAssistantHelper.h b/lib/HomeAssistantHelper/HomeAssistantHelper.h index c27f028..c7417c8 100644 --- a/lib/HomeAssistantHelper/HomeAssistantHelper.h +++ b/lib/HomeAssistantHelper/HomeAssistantHelper.h @@ -100,8 +100,8 @@ class HomeAssistantHelper { return result; } - template - String getTopic(T category, T name, char nameSeparator = '/') { + template + String makeConfigTopic(CT category, NT name, char nameSeparator = '/') { String topic = ""; topic.concat(this->prefix); topic.concat('/'); @@ -110,21 +110,45 @@ class HomeAssistantHelper { topic.concat(this->devicePrefix); topic.concat(nameSeparator); topic.concat(name); - topic.concat("/config"); + topic.concat(F("/config")); return topic; } template - String getDeviceTopic(T value, char separator = '/') { + String getDeviceTopic(T value, char dpvSeparator = '/') { String topic = ""; topic.concat(this->devicePrefix); - topic.concat(separator); + topic.concat(dpvSeparator); topic.concat(value); return topic; } + template + String getDeviceTopic(CT category, NT name, char dpcSeparator = '/', char cnSeparator = '/') { + String topic = ""; + topic.concat(this->devicePrefix); + topic.concat(dpcSeparator); + topic.concat(category); + topic.concat(cnSeparator); + topic.concat(name); + return topic; + } + + template + String getDeviceTopic(CT category, NT name, ST suffix, char dpcSeparator = '/', char cnSeparator = '/', char nsSeparator = '/') { + String topic = ""; + topic.concat(this->devicePrefix); + topic.concat(dpcSeparator); + topic.concat(category); + topic.concat(cnSeparator); + topic.concat(name); + topic.concat(nsSeparator); + topic.concat(suffix); + return topic; + } + template - String getObjectId(T value, char separator = '_') { + String getObjectIdWithPrefix(T value, char separator = '_') { String topic = ""; topic.concat(this->devicePrefix); topic.concat(separator); diff --git a/lib/HomeAssistantHelper/strings.h b/lib/HomeAssistantHelper/strings.h index b9ed038..da99954 100644 --- a/lib/HomeAssistantHelper/strings.h +++ b/lib/HomeAssistantHelper/strings.h @@ -25,6 +25,8 @@ const char HA_ENABLED_BY_DEFAULT[] PROGMEM = "enabled_by_default"; const char HA_UNIQUE_ID[] PROGMEM = "unique_id"; const char HA_OBJECT_ID[] PROGMEM = "object_id"; const char HA_ENTITY_CATEGORY[] PROGMEM = "entity_category"; +const char HA_ENTITY_CATEGORY_DIAGNOSTIC[] PROGMEM = "diagnostic"; +const char HA_ENTITY_CATEGORY_CONFIG[] PROGMEM = "config"; const char HA_STATE_TOPIC[] PROGMEM = "state_topic"; const char HA_VALUE_TEMPLATE[] PROGMEM = "value_template"; const char HA_OPTIONS[] PROGMEM = "options"; @@ -35,16 +37,21 @@ const char HA_DEVICE_CLASS[] PROGMEM = "device_class"; const char HA_UNIT_OF_MEASUREMENT[] PROGMEM = "unit_of_measurement"; const char HA_UNIT_OF_MEASUREMENT_C[] PROGMEM = "°C"; const char HA_UNIT_OF_MEASUREMENT_F[] PROGMEM = "°F"; +const char HA_UNIT_OF_MEASUREMENT_PERCENT[] PROGMEM = "%"; +const char HA_UNIT_OF_MEASUREMENT_L_MIN[] PROGMEM = "L/min"; +const char HA_UNIT_OF_MEASUREMENT_GAL_MIN[] PROGMEM = "gal/min"; const char HA_ICON[] PROGMEM = "icon"; const char HA_MIN[] PROGMEM = "min"; const char HA_MAX[] PROGMEM = "max"; const char HA_STEP[] PROGMEM = "step"; const char HA_MODE[] PROGMEM = "mode"; +const char HA_MODE_BOX[] PROGMEM = "box"; const char HA_STATE_ON[] PROGMEM = "state_on"; const char HA_STATE_OFF[] PROGMEM = "state_off"; const char HA_PAYLOAD_ON[] PROGMEM = "payload_on"; const char HA_PAYLOAD_OFF[] PROGMEM = "payload_off"; const char HA_STATE_CLASS[] PROGMEM = "state_class"; +const char HA_STATE_CLASS_MEASUREMENT[] PROGMEM = "measurement"; const char HA_EXPIRE_AFTER[] PROGMEM = "expire_after"; const char HA_CURRENT_TEMPERATURE_TOPIC[] PROGMEM = "current_temperature_topic"; const char HA_CURRENT_TEMPERATURE_TEMPLATE[] PROGMEM = "current_temperature_template"; diff --git a/lib/MqttWriter/MqttWriter.h b/lib/MqttWriter/MqttWriter.h index 03c54df..9c0fa45 100644 --- a/lib/MqttWriter/MqttWriter.h +++ b/lib/MqttWriter/MqttWriter.h @@ -77,7 +77,7 @@ class MqttWriter { #endif } - bool publish(const char* topic, JsonDocument& doc, bool retained = false) { + bool publish(const char* topic, const JsonVariantConst doc, bool retained = false) { if (!this->client->connected()) { this->bufferPos = 0; return false; diff --git a/lib/WebServerHandlers/DynamicPage.h b/lib/WebServerHandlers/DynamicPage.h index 4ad7182..021f0f3 100644 --- a/lib/WebServerHandlers/DynamicPage.h +++ b/lib/WebServerHandlers/DynamicPage.h @@ -61,7 +61,7 @@ class DynamicPage : public RequestHandler { } if (this->cacheHeader != nullptr) { - server.sendHeader("Cache-Control", this->cacheHeader); + server.sendHeader(F("Cache-Control"), this->cacheHeader); } #ifdef ARDUINO_ARCH_ESP8266 @@ -218,7 +218,6 @@ class DynamicPage : public RequestHandler { CanHandleCallback canHandleCallback; BeforeSendCallback beforeSendCallback; TemplateCallback templateCallback; - String eTag; const char* uri = nullptr; const char* path = nullptr; const char* cacheHeader = nullptr; diff --git a/lib/WebServerHandlers/StaticPage.h b/lib/WebServerHandlers/StaticPage.h index 095635c..14b643e 100644 --- a/lib/WebServerHandlers/StaticPage.h +++ b/lib/WebServerHandlers/StaticPage.h @@ -1,5 +1,8 @@ #include #include +#if defined(ARDUINO_ARCH_ESP32) + #include +#endif using namespace mime; @@ -8,7 +11,8 @@ class StaticPage : public RequestHandler { typedef std::function CanHandleCallback; typedef std::function BeforeSendCallback; - StaticPage(const char* uri, FS* fs, const char* path, const char* cacheHeader = nullptr) { + template + StaticPage(const char* uri, FS* fs, T path, const char* cacheHeader = nullptr) { this->uri = uri; this->fs = fs; this->path = path; @@ -46,21 +50,25 @@ class StaticPage : public RequestHandler { return true; } - #if defined(ARDUINO_ARCH_ESP8266) if (server._eTagEnabled) { - if (server._eTagFunction) { - this->eTag = (server._eTagFunction)(*this->fs, this->path); - - } else if (this->eTag.isEmpty()) { - this->eTag = esp8266webserver::calcETag(*this->fs, this->path); + if (this->eTag.isEmpty()) { + if (server._eTagFunction) { + this->eTag = (server._eTagFunction)(*this->fs, this->path); + + } else { + #if defined(ARDUINO_ARCH_ESP8266) + this->eTag = esp8266webserver::calcETag(*this->fs, this->path); + #elif defined(ARDUINO_ARCH_ESP32) + this->eTag = StaticRequestHandler::calcETag(*this->fs, this->path); + #endif + } } - if (server.header("If-None-Match").equals(this->eTag.c_str())) { + if (!this->eTag.isEmpty() && server.header(F("If-None-Match")).equals(this->eTag.c_str())) { server.send(304); return true; } } - #endif if (!this->path.endsWith(FPSTR(mimeTable[gz].endsWith)) && !this->fs->exists(path)) { String pathWithGz = this->path + FPSTR(mimeTable[gz].endsWith); @@ -80,14 +88,14 @@ class StaticPage : public RequestHandler { } if (this->cacheHeader != nullptr) { - server.sendHeader("Cache-Control", this->cacheHeader); + server.sendHeader(F("Cache-Control"), this->cacheHeader); } - #if defined(ARDUINO_ARCH_ESP8266) - if (server._eTagEnabled && this->eTag.length() > 0) { - server.sendHeader("ETag", this->eTag); + if (server._eTagEnabled && !this->eTag.isEmpty()) { + server.sendHeader(F("ETag"), this->eTag); } + #if defined(ARDUINO_ARCH_ESP8266) server.streamFile(file, F("text/html"), method); #else server.streamFile(file, F("text/html"), 200); diff --git a/lib/WebServerHandlers/UpgradeHandler.h b/lib/WebServerHandlers/UpgradeHandler.h index 13012cf..14c7f93 100644 --- a/lib/WebServerHandlers/UpgradeHandler.h +++ b/lib/WebServerHandlers/UpgradeHandler.h @@ -93,10 +93,12 @@ class UpgradeHandler : public RequestHandler { void upload(WebServer& server, const String& uri, HTTPUpload& upload) override { UpgradeResult* result; - if (upload.name.equals("firmware")) { + if (upload.name.equals(F("firmware"))) { result = &this->firmwareResult; - } else if (upload.name.equals("filesystem")) { + + } else if (upload.name.equals(F("filesystem"))) { result = &this->filesystemResult; + } else { return; } diff --git a/otgateway.ino b/otgateway.ino deleted file mode 100644 index 6068fe7..0000000 --- a/otgateway.ino +++ /dev/null @@ -1,4 +0,0 @@ -/* - This file is needed by the Arduino IDE because the ino file needs to be named as the directory name. - Don't worry, the Arduino compiler will "merge" all files, including src/main.cpp -*/ diff --git a/package.json b/package.json index 62380e7..57f4113 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "gulp-html-minifier-terser": "^7.1.0", "gulp-jsonminify": "^1.1.0", "gulp-postcss": "^10.0.0", - "gulp-terser": "^2.1.0" + "gulp-terser": "^2.1.0", + "gulp-replace": "^1.1.4" } } diff --git a/platformio.ini b/platformio.ini index 2530bbc..70e6e73 100644 --- a/platformio.ini +++ b/platformio.ini @@ -14,7 +14,7 @@ extra_configs = secrets.default.ini core_dir = .pio [env] -version = 1.4.6 +version = 1.5.0 framework = arduino lib_deps = bblanchon/ArduinoJson@^7.1.0 @@ -30,17 +30,14 @@ lib_deps = laxilef/TinyLogger@^1.1.1 build_type = ${secrets.build_type} build_flags = - -D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY - ;-D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH - -D PIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK305 -mtext-section-literals -D MQTT_CLIENT_STD_FUNCTION_CALLBACK=1 ;-D DEBUG_ESP_CORE -D DEBUG_ESP_WIFI -D DEBUG_ESP_HTTP_SERVER -D DEBUG_ESP_PORT=Serial -D BUILD_VERSION='"${this.version}"' -D BUILD_ENV='"$PIOENV"' - -D DEFAULT_SERIAL_ENABLE=${secrets.serial_enable} + -D DEFAULT_SERIAL_ENABLED=${secrets.serial_enabled} -D DEFAULT_SERIAL_BAUD=${secrets.serial_baud} - -D DEFAULT_TELNET_ENABLE=${secrets.telnet_enable} + -D DEFAULT_TELNET_ENABLED=${secrets.telnet_enabled} -D DEFAULT_TELNET_PORT=${secrets.telnet_port} -D DEFAULT_LOG_LEVEL=${secrets.log_level} -D DEFAULT_HOSTNAME='"${secrets.hostname}"' @@ -50,6 +47,7 @@ build_flags = -D DEFAULT_STA_PASSWORD='"${secrets.sta_password}"' -D DEFAULT_PORTAL_LOGIN='"${secrets.portal_login}"' -D DEFAULT_PORTAL_PASSWORD='"${secrets.portal_password}"' + -D DEFAULT_MQTT_ENABLED=${secrets.mqtt_enabled} -D DEFAULT_MQTT_SERVER='"${secrets.mqtt_server}"' -D DEFAULT_MQTT_PORT=${secrets.mqtt_port} -D DEFAULT_MQTT_USER='"${secrets.mqtt_user}"' @@ -57,7 +55,10 @@ build_flags = -D DEFAULT_MQTT_PREFIX='"${secrets.mqtt_prefix}"' upload_speed = 921600 monitor_speed = 115200 -monitor_filters = direct +;monitor_filters = direct +monitor_filters = + esp32_exception_decoder + esp8266_exception_decoder board_build.flash_mode = dio board_build.filesystem = littlefs @@ -71,7 +72,11 @@ lib_ignore = extra_scripts = post:tools/build.py build_type = ${env.build_type} -build_flags = ${env.build_flags} +build_flags = + ${env.build_flags} + -D PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY + ;-D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH + -D PIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK305 board_build.ldscript = eagle.flash.4m1m.ld [esp32_defaults] @@ -80,7 +85,7 @@ board_build.ldscript = eagle.flash.4m1m.ld ;platform_packages = ; framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.5 ; framework-arduinoespressif32-libs @ https://github.com/espressif/esp32-arduino-lib-builder/releases/download/idf-release_v5.1/esp32-arduino-libs-idf-release_v5.1-33fbade6.zip -platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc2/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc3/platform-espressif32.zip platform_packages = board_build.partitions = esp32_partitions.csv lib_deps = diff --git a/secrets.default.ini b/secrets.default.ini index e19d0a4..f478977 100644 --- a/secrets.default.ini +++ b/secrets.default.ini @@ -1,9 +1,9 @@ [secrets] build_type = release -serial_enable = true +serial_enabled = true serial_baud = 115200 -telnet_enable = true +telnet_enabled = true telnet_port = 23 log_level = 5 hostname = opentherm @@ -17,6 +17,7 @@ sta_password = portal_login = admin portal_password = admin +mqtt_enabled = false mqtt_server = mqtt_port = 1883 mqtt_user = diff --git a/src/HaHelper.h b/src/HaHelper.h index 1a71f3e..8d9325e 100644 --- a/src/HaHelper.h +++ b/src/HaHelper.h @@ -5,91 +5,448 @@ class HaHelper : public HomeAssistantHelper { public: static const byte TEMP_SOURCE_HEATING = 0; static const byte TEMP_SOURCE_INDOOR = 1; + static const char AVAILABILITY_OT_CONN[]; + static const char AVAILABILITY_SENSOR_CONN[]; + + void updateCachedTopics() { + this->statusTopic = this->getDeviceTopic(F("status")); + this->stateTopic = this->getDeviceTopic(F("state")); + this->setStateTopic = this->getDeviceTopic(F("state/set")); + this->settingsTopic = this->getDeviceTopic(F("settings")); + this->setSettingsTopic = this->getDeviceTopic(F("settings/set")); + } + + void setExpireAfter(unsigned short value) { + this->expireAfter = value; + } + + auto getExpireAfter() { + return this->expireAfter; + } + + bool publishDynamicSensor(Sensors::Settings& sSensor, Sensors::ValueType vType = Sensors::ValueType::PRIMARY, UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { + JsonDocument doc; + + // set device class & unit of measurement + switch (sSensor.purpose) { + case Sensors::Purpose::OUTDOOR_TEMP: + case Sensors::Purpose::INDOOR_TEMP: + case Sensors::Purpose::HEATING_TEMP: + case Sensors::Purpose::HEATING_RETURN_TEMP: + case Sensors::Purpose::DHW_TEMP: + case Sensors::Purpose::DHW_RETURN_TEMP: + case Sensors::Purpose::EXHAUST_TEMP: + case Sensors::Purpose::TEMPERATURE: + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_TEMPERATURE); + if (unit == UnitSystem::METRIC) { + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); + + if (sSensor.type == Sensors::Type::MANUAL) { + doc[FPSTR(HA_MIN)] = -99; + doc[FPSTR(HA_MAX)] = 99; + } + + } else if (unit == UnitSystem::IMPERIAL) { + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); + + if (sSensor.type == Sensors::Type::MANUAL) { + doc[FPSTR(HA_MIN)] = -147; + doc[FPSTR(HA_MAX)] = 211; + } + } + break; + + case Sensors::Purpose::DHW_FLOW_RATE: + doc[FPSTR(HA_DEVICE_CLASS)] = F("volume_flow_rate"); + if (unit == UnitSystem::METRIC) { + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_L_MIN); + + } else if (unit == UnitSystem::IMPERIAL) { + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_GAL_MIN); + } + break; + + case Sensors::Purpose::MODULATION_LEVEL: + case Sensors::Purpose::POWER_FACTOR: + doc[FPSTR(HA_DEVICE_CLASS)] = F("power_factor"); + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_PERCENT); + break; + + case Sensors::Purpose::POWER: + doc[FPSTR(HA_DEVICE_CLASS)] = F("power"); + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("kW"); + break; + + case Sensors::Purpose::FAN_SPEED: + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("RPM"); + break; + + case Sensors::Purpose::CO2: + doc[FPSTR(HA_DEVICE_CLASS)] = F("carbon_dioxide"); + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("ppm"); + break; + + case Sensors::Purpose::PRESSURE: + doc[FPSTR(HA_DEVICE_CLASS)] = F("pressure"); + if (unit == UnitSystem::METRIC) { + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("bar"); + + } else if (unit == UnitSystem::IMPERIAL) { + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("psi"); + } + break; + + case Sensors::Purpose::HUMIDITY: + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_HUMIDITY); + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_PERCENT); + break; + + default: + break; + } + + // set icon + switch (sSensor.purpose) { + case Sensors::Purpose::OUTDOOR_TEMP: + doc[FPSTR(HA_ICON)] = F("mdi:home-thermometer-outline"); + break; + + case Sensors::Purpose::INDOOR_TEMP: + doc[FPSTR(HA_ICON)] = F("mdi:home-thermometer"); + break; + + case Sensors::Purpose::HEATING_TEMP: + doc[FPSTR(HA_ICON)] = F("mdi:radiator"); + break; + + case Sensors::Purpose::HEATING_RETURN_TEMP: + doc[FPSTR(HA_ICON)] = F("mdi:heating-coil"); + break; + + case Sensors::Purpose::DHW_TEMP: + doc[FPSTR(HA_ICON)] = F("mdi:faucet"); + break; + + case Sensors::Purpose::DHW_RETURN_TEMP: + doc[FPSTR(HA_ICON)] = F("mdi:heating-coil"); + break; + + case Sensors::Purpose::EXHAUST_TEMP: + doc[FPSTR(HA_ICON)] = F("mdi:smoke"); + break; + + case Sensors::Purpose::TEMPERATURE: + doc[FPSTR(HA_ICON)] = F("mdi:thermometer-lines"); + break; + + case Sensors::Purpose::DHW_FLOW_RATE: + doc[FPSTR(HA_ICON)] = F("mdi:faucet"); + break; + + case Sensors::Purpose::MODULATION_LEVEL: + doc[FPSTR(HA_ICON)] = F("mdi:fire-circle"); + break; + + case Sensors::Purpose::POWER_FACTOR: + case Sensors::Purpose::POWER: + doc[FPSTR(HA_ICON)] = F("mdi:chart-bar"); + break; + + case Sensors::Purpose::FAN_SPEED: + doc[FPSTR(HA_ICON)] = F("mdi:fan"); + break; + + case Sensors::Purpose::PRESSURE: + doc[FPSTR(HA_ICON)] = F("mdi:gauge"); + break; + + case Sensors::Purpose::HUMIDITY: + doc[FPSTR(HA_ICON)] = F("mdi:water-percent"); + break; + + default: + break; + } + + String objId = Sensors::makeObjectId(sSensor.name); + + // state topic + doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic( + F("sensors"), + objId.c_str() + ); + + // set device class, name, value template for bluetooth sensors + // or name & value template for another sensors + if (sSensor.type == Sensors::Type::BLUETOOTH) { + // available state topic + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = doc[FPSTR(HA_STATE_TOPIC)]; + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = AVAILABILITY_SENSOR_CONN; + + String sName = sSensor.name; + switch (vType) { + case Sensors::ValueType::TEMPERATURE: + Sensors::makeObjectIdWithSuffix(objId, sSensor.name, F("temp")); + sName += F(" temperature"); + + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_TEMPERATURE); + if (unit == UnitSystem::METRIC) { + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); + + } else if (unit == UnitSystem::IMPERIAL) { + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); + } + doc[FPSTR(HA_NAME)] = sName; + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.temperature|float(0)|round(2) }}"); + break; + + case Sensors::ValueType::HUMIDITY: + Sensors::makeObjectIdWithSuffix(objId, sSensor.name, FPSTR(S_HUMIDITY)); + sName += F(" humidity"); + + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_HUMIDITY); + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_PERCENT); + doc[FPSTR(HA_NAME)] = sName; + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.humidity|float(0)|round(2) }}"); + break; + + case Sensors::ValueType::BATTERY: + Sensors::makeObjectIdWithSuffix(objId, sSensor.name, FPSTR(S_BATTERY)); + sName += F(" battery"); + + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_BATTERY); + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_PERCENT); + doc[FPSTR(HA_NAME)] = sName; + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.battery|float(0)|round(2) }}"); + break; + + case Sensors::ValueType::RSSI: + Sensors::makeObjectIdWithSuffix(objId, sSensor.name, FPSTR(S_RSSI)); + sName += F(" RSSI"); + + doc[FPSTR(HA_DEVICE_CLASS)] = F("signal_strength"); + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("dBm"); + doc[FPSTR(HA_NAME)] = sName; + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.rssi|float(0)|round(2) }}"); + break; + + default: + return false; + } + + } else if (sSensor.type == Sensors::Type::MANUAL) { + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); + doc[FPSTR(HA_STEP)] = 0.01f; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + + doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic( + F("sensors"), + objId.c_str(), + F("set") + ); + doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"value\": {{ value }}}"); + + doc[FPSTR(HA_NAME)] = sSensor.name; + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.value|float(0)|round(2) }}"); + + } else { + // available state topic + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = doc[FPSTR(HA_STATE_TOPIC)]; + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = AVAILABILITY_SENSOR_CONN; + + doc[FPSTR(HA_NAME)] = sSensor.name; + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.value|float(0)|round(2) }}"); + } + + // object id's + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(objId.c_str()); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + + const String& configTopic = this->makeConfigTopic( + sSensor.type == Sensors::Type::MANUAL ? FPSTR(HA_ENTITY_NUMBER) : FPSTR(HA_ENTITY_SENSOR), + objId.c_str() + ); + objId.clear(); + + doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); + doc[FPSTR(HA_STATE_CLASS)] = FPSTR(HA_STATE_CLASS_MEASUREMENT); + doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; + doc.shrinkToFit(); + + return this->publish(configTopic.c_str(), doc); + } + + bool deleteDynamicSensor(Sensors::Settings& sSensor, Sensors::ValueType vType = Sensors::ValueType::PRIMARY) { + String objId; - bool publishSwitchHeating(bool enabledByDefault = true) { + if (sSensor.type == Sensors::Type::BLUETOOTH) { + switch (vType) { + case Sensors::ValueType::TEMPERATURE: + Sensors::makeObjectIdWithSuffix(objId, sSensor.name, F("temp")); + break; + + case Sensors::ValueType::HUMIDITY: + Sensors::makeObjectIdWithSuffix(objId, sSensor.name, FPSTR(S_HUMIDITY)); + break; + + case Sensors::ValueType::BATTERY: + Sensors::makeObjectIdWithSuffix(objId, sSensor.name, FPSTR(S_BATTERY)); + break; + + case Sensors::ValueType::RSSI: + Sensors::makeObjectIdWithSuffix(objId, sSensor.name, FPSTR(S_RSSI)); + break; + + default: + return false; + } + + } else { + Sensors::makeObjectId(objId, sSensor.name); + } + + const String& configTopic = this->makeConfigTopic( + sSensor.type == Sensors::Type::MANUAL ? FPSTR(HA_ENTITY_NUMBER) : FPSTR(HA_ENTITY_SENSOR), + objId.c_str() + ); + objId.clear(); + + return this->publish(configTopic.c_str()); + } + + bool publishConnectionDynamicSensor(Sensors::Settings& sSensor, bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + String objId = Sensors::makeObjectIdWithSuffix(sSensor.name, F("connected")); + + // object id's + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(objId.c_str()); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + + // state topic + doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic( + F("sensors"), + Sensors::makeObjectId(sSensor.name).c_str() + ); + + // sensor name + { + String sName = sSensor.name; + sName.trim(); + sName += F(" connected"); + + doc[FPSTR(HA_NAME)] = sName; + } + + const String& configTopic = this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), objId.c_str()); + objId.clear(); + + + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("heating")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("heating")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_NAME)] = F("Heating"); - doc[FPSTR(HA_ICON)] = F("mdi:radiator"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); - doc[FPSTR(HA_STATE_ON)] = true; - doc[FPSTR(HA_STATE_OFF)] = false; - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.heating.enable }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); - doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"heating\": {\"enable\" : true}}"); - doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"heating\": {\"enable\" : false}}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); + doc[FPSTR(HA_DEVICE_CLASS)] = F("connectivity"); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.connected, 'ON', 'OFF') }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; + doc.shrinkToFit(); + + return this->publish(configTopic.c_str(), doc); + } + + bool deleteConnectionDynamicSensor(Sensors::Settings& sSensor) { + const String& configTopic = this->makeConfigTopic( + FPSTR(HA_ENTITY_BINARY_SENSOR), + Sensors::makeObjectIdWithSuffix(sSensor.name, F("connected")).c_str() + ); + + return this->publish(configTopic.c_str()); + } + + bool publishSignalQualityDynamicSensor(Sensors::Settings& sSensor, bool enabledByDefault = true) { + JsonDocument doc; + String objId = Sensors::makeObjectIdWithSuffix(sSensor.name, F("signal_quality")); + + // object id's + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(objId.c_str()); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + + // state topic + doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic( + F("sensors"), + Sensors::makeObjectId(sSensor.name).c_str() + ); + + // sensor name + { + String sName = sSensor.name; + sName.trim(); + sName += F(" signal quality"); + + doc[FPSTR(HA_NAME)] = sName; + } + + const String& configTopic = this->makeConfigTopic(FPSTR(HA_ENTITY_SENSOR), objId.c_str()); + objId.clear(); + + + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); + doc[FPSTR(HA_DEVICE_CLASS)] = F("signal_strength"); + doc[FPSTR(HA_STATE_CLASS)] = FPSTR(HA_STATE_CLASS_MEASUREMENT); + doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_PERCENT); + doc[FPSTR(HA_ICON)] = F("mdi:signal"); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.signalQuality|float(0)|round(0) }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SWITCH), F("heating")).c_str(), doc); + return this->publish(configTopic.c_str(), doc); } + bool deleteSignalQualityDynamicSensor(Sensors::Settings& sSensor) { + JsonDocument doc; + const String& configTopic = this->makeConfigTopic( + FPSTR(HA_ENTITY_SENSOR), + Sensors::makeObjectIdWithSuffix(sSensor.name, F("signal_quality")).c_str() + ); + + return this->publish(configTopic.c_str()); + } + + bool publishSwitchHeatingTurbo(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("heating_turbo")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("heating_turbo")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("heating_turbo")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_NAME)] = F("Turbo heating"); doc[FPSTR(HA_ICON)] = F("mdi:rocket-launch-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_STATE_ON)] = true; doc[FPSTR(HA_STATE_OFF)] = false; doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.heating.turbo }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"heating\": {\"turbo\" : true}}"); doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"heating\": {\"turbo\" : false}}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SWITCH), F("heating_turbo")).c_str(), doc); - } - - bool publishInputHeatingTarget(UnitSystem unit = UnitSystem::METRIC, byte minTemp = 20, byte maxTemp = 90, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("heating_target")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("heating_target")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - } - - doc[FPSTR(HA_NAME)] = F("Heating target"); - doc[FPSTR(HA_ICON)] = F("mdi:radiator"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.heating.target|float(0)|round(1) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); - doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"heating\": {\"target\" : {{ value }}}}"); - doc[FPSTR(HA_MIN)] = minTemp; - doc[FPSTR(HA_MAX)] = maxTemp; - doc[FPSTR(HA_STEP)] = 0.5f; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("heating_target")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), F("heating_turbo")).c_str(), doc); } bool publishInputHeatingHysteresis(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("heating_hysteresis")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("heating_hysteresis")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("heating_hysteresis")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_TEMPERATURE); if (unit == UnitSystem::METRIC) { doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); @@ -100,51 +457,52 @@ class HaHelper : public HomeAssistantHelper { doc[FPSTR(HA_NAME)] = F("Heating hysteresis"); doc[FPSTR(HA_ICON)] = F("mdi:altimeter"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.heating.hysteresis|float(0)|round(2) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"heating\": {\"hysteresis\" : {{ value }}}}"); doc[FPSTR(HA_MIN)] = 0; doc[FPSTR(HA_MAX)] = 15; doc[FPSTR(HA_STEP)] = 0.01f; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("heating_hysteresis")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("heating_hysteresis")).c_str(), doc); } bool publishInputHeatingTurboFactor(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("heating_turbo_factor")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("heating_turbo_factor")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("heating_turbo_factor")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_DEVICE_CLASS)] = F("power_factor"); doc[FPSTR(HA_NAME)] = F("Heating turbo factor"); doc[FPSTR(HA_ICON)] = F("mdi:multiplication-box"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.heating.turboFactor|float(0)|round(2) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"heating\": {\"turboFactor\" : {{ value }}}}"); doc[FPSTR(HA_MIN)] = 1.5; doc[FPSTR(HA_MAX)] = 10; doc[FPSTR(HA_STEP)] = 0.01f; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("heating_turbo_factor")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("heating_turbo_factor")).c_str(), doc); } + bool publishInputHeatingMinTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("heating_min_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("heating_min_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("heating_min_temp")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_TEMPERATURE); if (unit == UnitSystem::METRIC) { doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); @@ -159,26 +517,26 @@ class HaHelper : public HomeAssistantHelper { doc[FPSTR(HA_NAME)] = F("Heating min temp"); doc[FPSTR(HA_ICON)] = F("mdi:thermometer-chevron-down"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.heating.minTemp|float(0)|round(1) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"heating\": {\"minTemp\" : {{ value }}}}"); doc[FPSTR(HA_STEP)] = 1; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("heating_min_temp")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("heating_min_temp")).c_str(), doc); } bool publishInputHeatingMaxTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("heating_max_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("heating_max_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("heating_max_temp")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_TEMPERATURE); if (unit == UnitSystem::METRIC) { doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); @@ -193,229 +551,27 @@ class HaHelper : public HomeAssistantHelper { doc[FPSTR(HA_NAME)] = F("Heating max temp"); doc[FPSTR(HA_ICON)] = F("mdi:thermometer-chevron-up"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.heating.maxTemp|float(0)|round(1) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"heating\": {\"maxTemp\" : {{ value }}}}"); doc[FPSTR(HA_STEP)] = 1; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("heating_max_temp")).c_str(), doc); - } - - - bool publishSensorHeatingSetpoint(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("heating_setpoint")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("heating_setpoint")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - } - - doc[FPSTR(HA_NAME)] = F("Heating setpoint"); - doc[FPSTR(HA_ICON)] = F("mdi:coolant-temperature"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.parameters.heatingSetpoint|float(0)|round(1) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("heating_setpoint")).c_str(), doc); - } - - bool publishSensorBoilerHeatingMinTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("boiler_heating_min_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("boiler_heating_min_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - } - - doc[FPSTR(HA_NAME)] = F("Boiler heating min temp"); - doc[FPSTR(HA_ICON)] = F("mdi:thermometer-chevron-down"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.parameters.heatingMinTemp|int(0) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("boiler_heating_min_temp")).c_str(), doc); - } - - bool publishSensorBoilerHeatingMaxTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("boiler_heating_max_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("boiler_heating_max_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - } - - doc[FPSTR(HA_NAME)] = F("Boiler heating max temp"); - doc[FPSTR(HA_ICON)] = F("mdi:thermometer-chevron-up"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.parameters.heatingMaxTemp|int(0) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("boiler_heating_max_temp")).c_str(), doc); - } - - - bool publishSwitchDhw(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("dhw")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("dhw")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_NAME)] = F("DHW"); - doc[FPSTR(HA_ICON)] = F("mdi:water-pump"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); - doc[FPSTR(HA_STATE_ON)] = true; - doc[FPSTR(HA_STATE_OFF)] = false; - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.dhw.enable }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); - doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"dhw\": {\"enable\" : true}}"); - doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"dhw\": {\"enable\" : false}}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SWITCH), F("dhw")).c_str(), doc); - } - - bool publishInputDhwTarget(UnitSystem unit = UnitSystem::METRIC, byte minTemp = 40, byte maxTemp = 60, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("dhw_target")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("dhw_target")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - } - - doc[FPSTR(HA_NAME)] = F("DHW target"); - doc[FPSTR(HA_ICON)] = F("mdi:water-pump"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.dhw.target|float(0)|round(1) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); - doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"dhw\": {\"target\" : {{ value|int(0) }}}}"); - doc[FPSTR(HA_MIN)] = minTemp; - doc[FPSTR(HA_MAX)] = maxTemp > minTemp ? maxTemp : minTemp; - doc[FPSTR(HA_STEP)] = 1; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("dhw_target")).c_str(), doc); - } - - bool publishSensorBoilerDhwMinTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("boiler_dhw_min_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("boiler_dhw_min_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - } - - doc[FPSTR(HA_NAME)] = F("Boiler DHW min temp"); - doc[FPSTR(HA_ICON)] = F("mdi:thermometer-chevron-down"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.parameters.dhwMinTemp|int(0) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("boiler_dhw_min_temp")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("heating_max_temp")).c_str(), doc); } - bool publishSensorBoilerDhwMaxTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("boiler_dhw_max_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("boiler_dhw_max_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - } - - doc[FPSTR(HA_NAME)] = F("Boiler DHW max temp"); - doc[FPSTR(HA_ICON)] = F("mdi:thermometer-chevron-up"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.parameters.dhwMaxTemp|int(0) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("boiler_dhw_max_temp")).c_str(), doc); - } bool publishInputDhwMinTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("dhw_min_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("dhw_min_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("dhw_min_temp")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_TEMPERATURE); if (unit == UnitSystem::METRIC) { doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); @@ -430,26 +586,26 @@ class HaHelper : public HomeAssistantHelper { doc[FPSTR(HA_NAME)] = F("DHW min temp"); doc[FPSTR(HA_ICON)] = F("mdi:thermometer-chevron-down"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.dhw.minTemp|float(0)|round(1) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"dhw\": {\"minTemp\" : {{ value }}}}"); doc[FPSTR(HA_STEP)] = 1; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("dhw_min_temp")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("dhw_min_temp")).c_str(), doc); } bool publishInputDhwMaxTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("dhw_max_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("dhw_max_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("dhw_max_temp")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_TEMPERATURE); if (unit == UnitSystem::METRIC) { doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); @@ -464,143 +620,143 @@ class HaHelper : public HomeAssistantHelper { doc[FPSTR(HA_NAME)] = F("DHW max temp"); doc[FPSTR(HA_ICON)] = F("mdi:thermometer-chevron-up"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.dhw.maxTemp|float(0)|round(1) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"dhw\": {\"maxTemp\" : {{ value }}}}"); doc[FPSTR(HA_STEP)] = 1; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("dhw_max_temp")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("dhw_max_temp")).c_str(), doc); } bool publishSwitchPid(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("pid")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("pid")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("pid")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_NAME)] = F("PID"); doc[FPSTR(HA_ICON)] = F("mdi:chart-bar-stacked"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_STATE_ON)] = true; doc[FPSTR(HA_STATE_OFF)] = false; - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.pid.enable }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); - doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"pid\": {\"enable\" : true}}"); - doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"pid\": {\"enable\" : false}}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.pid.enabled }}"); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); + doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"pid\": {\"enabled\" : true}}"); + doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"pid\": {\"enabled\" : false}}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SWITCH), F("pid")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), F("pid")).c_str(), doc); } bool publishInputPidFactorP(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("pid_p")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("pid_p")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("pid_p")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_NAME)] = F("PID factor P"); doc[FPSTR(HA_ICON)] = F("mdi:alpha-p-circle-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.pid.p_factor|float(0)|round(3) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"pid\": {\"p_factor\" : {{ value }}}}"); doc[FPSTR(HA_MIN)] = 0.1f; doc[FPSTR(HA_MAX)] = 1000; doc[FPSTR(HA_STEP)] = 0.1f; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("pid_p_factor")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("pid_p_factor")).c_str(), doc); } bool publishInputPidFactorI(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("pid_i")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("pid_i")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("pid_i")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_NAME)] = F("PID factor I"); doc[FPSTR(HA_ICON)] = F("mdi:alpha-i-circle-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.pid.i_factor|float(0)|round(4) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"pid\": {\"i_factor\" : {{ value }}}}"); doc[FPSTR(HA_MIN)] = 0; doc[FPSTR(HA_MAX)] = 100; doc[FPSTR(HA_STEP)] = 0.001f; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("pid_i_factor")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("pid_i_factor")).c_str(), doc); } bool publishInputPidFactorD(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("pid_d")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("pid_d")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("pid_d")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_NAME)] = F("PID factor D"); doc[FPSTR(HA_ICON)] = F("mdi:alpha-d-circle-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.pid.d_factor|float(0)|round(3) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"pid\": {\"d_factor\" : {{ value }}}}"); doc[FPSTR(HA_MIN)] = 0; doc[FPSTR(HA_MAX)] = 100000; doc[FPSTR(HA_STEP)] = 1; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("pid_d_factor")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("pid_d_factor")).c_str(), doc); } bool publishInputPidDt(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("pid_dt")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("pid_dt")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("pid_dt")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_DEVICE_CLASS)] = F("duration"); doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("s"); doc[FPSTR(HA_NAME)] = F("PID DT"); doc[FPSTR(HA_ICON)] = F("mdi:timer-cog-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.pid.dt|int(0) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"pid\": {\"dt\" : {{ value }}}}"); doc[FPSTR(HA_MIN)] = 30; doc[FPSTR(HA_MAX)] = 1800; doc[FPSTR(HA_STEP)] = 1; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("pid_dt")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("pid_dt")).c_str(), doc); } bool publishInputPidMinTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("pid_min_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("pid_min_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("pid_min_temp")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_TEMPERATURE); if (unit == UnitSystem::METRIC) { doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); @@ -615,26 +771,26 @@ class HaHelper : public HomeAssistantHelper { doc[FPSTR(HA_NAME)] = F("PID min temp"); doc[FPSTR(HA_ICON)] = F("mdi:thermometer-chevron-down"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.pid.minTemp|float(0)|round(1) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"pid\": {\"minTemp\" : {{ value }}}}"); doc[FPSTR(HA_STEP)] = 1; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("pid_min_temp")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("pid_min_temp")).c_str(), doc); } bool publishInputPidMaxTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("pid_max_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("pid_max_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("pid_max_temp")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_TEMPERATURE); if (unit == UnitSystem::METRIC) { doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); @@ -649,893 +805,387 @@ class HaHelper : public HomeAssistantHelper { doc[FPSTR(HA_NAME)] = F("PID max temp"); doc[FPSTR(HA_ICON)] = F("mdi:thermometer-chevron-up"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.pid.maxTemp|float(0)|round(1) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"pid\": {\"maxTemp\" : {{ value }}}}"); doc[FPSTR(HA_STEP)] = 1; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("pid_max_temp")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("pid_max_temp")).c_str(), doc); } bool publishSwitchEquitherm(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("equitherm")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("equitherm")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_NAME)] = F("Equitherm"); doc[FPSTR(HA_ICON)] = F("mdi:sun-snowflake-variant"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_STATE_ON)] = true; doc[FPSTR(HA_STATE_OFF)] = false; - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.enable }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); - doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"equitherm\": {\"enable\" : true}}"); - doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"equitherm\": {\"enable\" : false}}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.enabled }}"); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); + doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"equitherm\": {\"enabled\" : true}}"); + doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"equitherm\": {\"enabled\" : false}}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SWITCH), F("equitherm")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), F("equitherm")).c_str(), doc); } bool publishInputEquithermFactorN(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("equitherm_n")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("equitherm_n")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_n")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_NAME)] = F("Equitherm factor N"); doc[FPSTR(HA_ICON)] = F("mdi:alpha-n-circle-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.n_factor|float(0)|round(3) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"n_factor\" : {{ value }}}}"); doc[FPSTR(HA_MIN)] = 0.001f; doc[FPSTR(HA_MAX)] = 10; doc[FPSTR(HA_STEP)] = 0.001f; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_n_factor")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_n_factor")).c_str(), doc); } bool publishInputEquithermFactorK(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("equitherm_k")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("equitherm_k")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_k")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_NAME)] = F("Equitherm factor K"); doc[FPSTR(HA_ICON)] = F("mdi:alpha-k-circle-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.k_factor|float(0)|round(2) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"k_factor\" : {{ value }}}}"); doc[FPSTR(HA_MIN)] = 0; doc[FPSTR(HA_MAX)] = 10; doc[FPSTR(HA_STEP)] = 0.01f; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_k_factor")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_k_factor")).c_str(), doc); } bool publishInputEquithermFactorT(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("settings")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.pid.enable, 'offline', 'online') }}"); + doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->settingsTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.pid.enabled, 'offline', 'online') }}"); doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("equitherm_t")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("equitherm_t")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_t")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_NAME)] = F("Equitherm factor T"); doc[FPSTR(HA_ICON)] = F("mdi:alpha-t-circle-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.t_factor|float(0)|round(2) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"t_factor\" : {{ value }}}}"); doc[FPSTR(HA_MIN)] = 0; doc[FPSTR(HA_MAX)] = 10; doc[FPSTR(HA_STEP)] = 0.01f; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_t_factor")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_t_factor")).c_str(), doc); } - bool publishStateStatus(bool enabledByDefault = true) { + bool publishStatusState(bool enabledByDefault = true) { JsonDocument doc; doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("status")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("status")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("status")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); doc[FPSTR(HA_DEVICE_CLASS)] = F("problem"); doc[FPSTR(HA_NAME)] = F("Status"); doc[FPSTR(HA_ICON)] = F("mdi:list-status"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_STATE_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value == 'online', 'OFF', 'ON') }}"); doc[FPSTR(HA_EXPIRE_AFTER)] = 60; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("status")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("status")).c_str(), doc); } - bool publishStateEmergency(bool enabledByDefault = true) { + bool publishEmergencyState(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("emergency")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("emergency")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("emergency")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); doc[FPSTR(HA_DEVICE_CLASS)] = F("problem"); - doc[FPSTR(HA_NAME)] = F("Emergency mode"); + doc[FPSTR(HA_NAME)] = F("Emergency"); doc[FPSTR(HA_ICON)] = F("mdi:alert-rhombus-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.emergency, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.master.emergency.state, 'ON', 'OFF') }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("emergency")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("emergency")).c_str(), doc); } - bool publishStateOtStatus(bool enabledByDefault = true) { + bool publishOpenthermConnectedState(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("ot_status")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("ot_status")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("ot_status")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); doc[FPSTR(HA_DEVICE_CLASS)] = F("connectivity"); doc[FPSTR(HA_NAME)] = F("Opentherm status"); doc[FPSTR(HA_ICON)] = F("mdi:list-status"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.connected, 'ON', 'OFF') }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("ot_status")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("ot_status")).c_str(), doc); } - bool publishStateHeating(bool enabledByDefault = true) { + bool publishHeatingState(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); + doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = AVAILABILITY_OT_CONN; doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("heating")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("heating")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("heating")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + //doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); doc[FPSTR(HA_DEVICE_CLASS)] = F("running"); doc[FPSTR(HA_NAME)] = F("Heating"); doc[FPSTR(HA_ICON)] = F("mdi:radiator"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.heating, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.heating.active, 'ON', 'OFF') }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("heating")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("heating")).c_str(), doc); } - bool publishStateDhw(bool enabledByDefault = true) { + bool publishDhwState(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); + doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = AVAILABILITY_OT_CONN; doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("dhw")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("dhw")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("dhw")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + //doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); doc[FPSTR(HA_DEVICE_CLASS)] = F("running"); doc[FPSTR(HA_NAME)] = F("DHW"); - doc[FPSTR(HA_ICON)] = F("mdi:water-pump"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.dhw, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_ICON)] = F("mdi:faucet"); + doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.dhw.active, 'ON', 'OFF') }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("dhw")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("dhw")).c_str(), doc); } - bool publishStateFlame(bool enabledByDefault = true) { + bool publishFlameState(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); + doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = AVAILABILITY_OT_CONN; doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("flame")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("flame")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("flame")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + //doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); doc[FPSTR(HA_DEVICE_CLASS)] = F("running"); doc[FPSTR(HA_NAME)] = F("Flame"); - doc[FPSTR(HA_ICON)] = F("mdi:fire"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.flame, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_ICON)] = F("mdi:gas-burner"); + doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.flame, 'ON', 'OFF') }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("flame")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("flame")).c_str(), doc); } - bool publishStateFault(bool enabledByDefault = true) { + bool publishFaultState(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); + doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = AVAILABILITY_OT_CONN; doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("fault")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("fault")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("fault")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); doc[FPSTR(HA_DEVICE_CLASS)] = F("problem"); doc[FPSTR(HA_NAME)] = F("Fault"); - doc[FPSTR(HA_ICON)] = F("mdi:water-boiler-alert"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.fault, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_ICON)] = F("mdi:alert-remove-outline"); + doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.fault.active, 'ON', 'OFF') }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("fault")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("fault")).c_str(), doc); } - bool publishStateDiagnostic(bool enabledByDefault = true) { + bool publishDiagState(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); + doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = AVAILABILITY_OT_CONN; doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("diagnostic")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("diagnostic")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC)); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); doc[FPSTR(HA_DEVICE_CLASS)] = F("problem"); doc[FPSTR(HA_NAME)] = F("Diagnostic"); doc[FPSTR(HA_ICON)] = F("mdi:account-wrench"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.diagnostic, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.diag.active, 'ON', 'OFF') }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("diagnostic")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC)).c_str(), doc); } - bool publishStateExtPump(bool enabledByDefault = true) { + bool publishExternalPumpState(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("ext_pump")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("ext_pump")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("ext_pump")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); doc[FPSTR(HA_DEVICE_CLASS)] = F("running"); doc[FPSTR(HA_NAME)] = F("External pump"); doc[FPSTR(HA_ICON)] = F("mdi:pump"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.externalPump, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("ext_pump")).c_str(), doc); - } - - - bool publishSensorModulation(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("modulation_level")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("modulation_level")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("power_factor"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("%"); - doc[FPSTR(HA_NAME)] = F("Modulation level"); - doc[FPSTR(HA_ICON)] = F("mdi:fire-circle"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.sensors.modulation|float(0)|round(0) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("modulation")).c_str(), doc); - } - - bool publishSensorPressure(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("pressure")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("pressure")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("pressure"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("bar"); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("psi"); - } - - doc[FPSTR(HA_NAME)] = F("Pressure"); - doc[FPSTR(HA_ICON)] = F("mdi:gauge"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.sensors.pressure|float(0)|round(2) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("pressure")).c_str(), doc); - } - - bool publishSensorDhwFlowRate(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("dhw_flow_rate")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("dhw_flow_rate")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("volume_flow_rate"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("L/min"); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("gal/min"); - } - - doc[FPSTR(HA_NAME)] = F("DHW flow rate"); - doc[FPSTR(HA_ICON)] = F("mdi:water-pump"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.sensors.dhwFlowRate|float(0)|round(2) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("dhw_flow_rate")).c_str(), doc); - } - - bool publishSensorPower(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("power")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("power")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("power"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("kW"); - doc[FPSTR(HA_NAME)] = F("Current power"); - doc[FPSTR(HA_ICON)] = F("mdi:chart-bar"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.sensors.power|float(0)|round(2) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.master.externalPump.state, 'ON', 'OFF') }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("power")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("ext_pump")).c_str(), doc); } - bool publishSensorFaultCode(bool enabledByDefault = true) { + bool publishFaultCode(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus and value_json.states.fault, 'online', 'offline') }}"); + doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.connected and value_json.slave.fault.active, 'online', 'offline') }}"); doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("fault_code")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("fault_code")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("fault_code")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); doc[FPSTR(HA_NAME)] = F("Fault code"); - doc[FPSTR(HA_ICON)] = F("mdi:chat-alert-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ \"%02d (0x%02X)\"|format(value_json.sensors.faultCode, value_json.sensors.faultCode) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_ICON)] = F("mdi:cog-box"); + doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ \"%02d (0x%02X)\"|format(value_json.slave.fault.code, value_json.slave.fault.code) }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("fault_code")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SENSOR), F("fault_code")).c_str(), doc); } - bool publishSensorDiagnosticCode(bool enabledByDefault = true) { + bool publishDiagCode(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus and value_json.states.fault or value_json.states.diagnostic, 'online', 'offline') }}"); + doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.connected and value_json.slave.fault.active or value_json.slave.diag.active, 'online', 'offline') }}"); doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("diagnostic_code")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("diagnostic_code")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("diagnostic_code")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); doc[FPSTR(HA_NAME)] = F("Diagnostic code"); - doc[FPSTR(HA_ICON)] = F("mdi:chat-alert-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ \"%02d (0x%02X)\"|format(value_json.sensors.diagnosticCode, value_json.sensors.diagnosticCode) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_ICON)] = F("mdi:information-box"); + doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ \"%02d (0x%02X)\"|format(value_json.slave.diag.code, value_json.slave.diag.code) }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("diagnostic_code")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SENSOR), F("diagnostic_code")).c_str(), doc); } - bool publishSensorRssi(bool enabledByDefault = true) { + bool publishNetworkRssi(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("rssi")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("rssi")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(FPSTR(S_RSSI)); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); doc[FPSTR(HA_DEVICE_CLASS)] = F("signal_strength"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); + doc[FPSTR(HA_STATE_CLASS)] = FPSTR(HA_STATE_CLASS_MEASUREMENT); doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("dBm"); doc[FPSTR(HA_NAME)] = F("RSSI"); doc[FPSTR(HA_ICON)] = F("mdi:signal"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.sensors.rssi|float(0)|round(1) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.master.network.rssi|float(0)|round(1) }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("rssi")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SENSOR), FPSTR(S_RSSI)).c_str(), doc); } - bool publishSensorUptime(bool enabledByDefault = true) { + bool publishUptime(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("uptime")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("uptime")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("uptime")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_DIAGNOSTIC); doc[FPSTR(HA_DEVICE_CLASS)] = F("duration"); doc[FPSTR(HA_STATE_CLASS)] = F("total_increasing"); doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("s"); doc[FPSTR(HA_NAME)] = F("Uptime"); doc[FPSTR(HA_ICON)] = F("mdi:clock-start"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.sensors.uptime|int(0) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("uptime")).c_str(), doc); - } - - bool publishOutdoorSensorConnected(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("outdoor_sensor_connected")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("outdoor_sensor_connected")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("connectivity"); - doc[FPSTR(HA_NAME)] = F("Outdoor sensor connected"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.sensors.outdoor.connected, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("outdoor_sensor_connected")).c_str(), doc); - } - - bool publishOutdoorSensorRssi(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("outdoor_sensor_rssi")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("outdoor_sensor_rssi")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("signal_strength"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("dBm"); - doc[FPSTR(HA_NAME)] = F("Outdoor sensor RSSI"); - doc[FPSTR(HA_ICON)] = F("mdi:signal"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.sensors.outdoor.rssi|float(0)|round(1) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("outdoor_sensor_rssi")).c_str(), doc); - } - - bool publishOutdoorSensorBattery(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("outdoor_sensor_battery")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("outdoor_sensor_battery")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("battery"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR("%"); - doc[FPSTR(HA_NAME)] = F("Outdoor sensor battery"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.sensors.outdoor.battery|float(0)|round(1) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("outdoor_sensor_battery")).c_str(), doc); - } - - bool publishOutdoorSensorHumidity(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("outdoor_sensor_humidity")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("outdoor_sensor_humidity")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("humidity"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR("%"); - doc[FPSTR(HA_NAME)] = F("Outdoor sensor humidity"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.sensors.outdoor.humidity|float(0)|round(1) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("outdoor_sensor_humidity")).c_str(), doc); - } - - bool publishIndoorSensorConnected(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("indoor_sensor_connected")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("indoor_sensor_connected")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("connectivity"); - doc[FPSTR(HA_NAME)] = F("Indoor sensor connected"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.sensors.indoor.connected, 'ON', 'OFF') }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("indoor_sensor_connected")).c_str(), doc); - } - - bool publishIndoorSensorRssi(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("indoor_sensor_rssi")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("indoor_sensor_rssi")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("signal_strength"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = F("dBm"); - doc[FPSTR(HA_NAME)] = F("Indoor sensor RSSI"); - doc[FPSTR(HA_ICON)] = F("mdi:signal"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.sensors.indoor.rssi|float(0)|round(1) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("indoor_sensor_rssi")).c_str(), doc); - } - - bool publishIndoorSensorBattery(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("indoor_sensor_battery")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("indoor_sensor_battery")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("battery"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR("%"); - doc[FPSTR(HA_NAME)] = F("Indoor sensor battery"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.sensors.indoor.battery|float(0)|round(1) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("indoor_sensor_battery")).c_str(), doc); - } - - bool publishIndoorSensorHumidity(bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("indoor_sensor_humidity")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("indoor_sensor_humidity")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("humidity"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR("%"); - doc[FPSTR(HA_NAME)] = F("Indoor sensor humidity"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.sensors.indoor.humidity|float(0)|round(1) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("indoor_sensor_humidity")).c_str(), doc); - } - - - bool publishInputIndoorTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("indoor_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("indoor_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - doc[FPSTR(HA_MIN)] = -99; - doc[FPSTR(HA_MAX)] = 99; - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - doc[FPSTR(HA_MIN)] = -147; - doc[FPSTR(HA_MAX)] = 211; - } - - doc[FPSTR(HA_NAME)] = F("Indoor temperature"); - doc[FPSTR(HA_ICON)] = F("mdi:home-thermometer"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.temperatures.indoor|float(0)|round(1) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("state/set")); - doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"temperatures\": {\"indoor\":{{ value }}}}"); - doc[FPSTR(HA_STEP)] = 0.01f; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("indoor_temp")).c_str(), doc); - } - - bool publishSensorIndoorTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("indoor_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("indoor_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - } - - doc[FPSTR(HA_NAME)] = F("Indoor temperature"); - doc[FPSTR(HA_ICON)] = F("mdi:home-thermometer"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.temperatures.indoor|float(0)|round(1) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("indoor_temp")).c_str(), doc); - } - - bool publishInputOutdoorTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("outdoor_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("outdoor_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - doc[FPSTR(HA_MIN)] = -99; - doc[FPSTR(HA_MAX)] = 99; - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - doc[FPSTR(HA_MIN)] = -147; - doc[FPSTR(HA_MAX)] = 211; - } - - doc[FPSTR(HA_NAME)] = F("Outdoor temperature"); - doc[FPSTR(HA_ICON)] = F("mdi:home-thermometer-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.temperatures.outdoor|float(0)|round(1) }}"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("state/set")); - doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"temperatures\": {\"outdoor\":{{ value }}}}"); - doc[FPSTR(HA_STEP)] = 0.01f; - doc[FPSTR(HA_MODE)] = "box"; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("outdoor_temp")).c_str(), doc); - } - - bool publishSensorOutdoorTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("outdoor_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("outdoor_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - } - - doc[FPSTR(HA_NAME)] = F("Outdoor temperature"); - doc[FPSTR(HA_ICON)] = F("mdi:home-thermometer-outline"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.temperatures.outdoor|float(0)|round(1) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("outdoor_temp")).c_str(), doc); - } - - bool publishSensorHeatingTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("heating_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("heating_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - } - - doc[FPSTR(HA_NAME)] = F("Heating temperature"); - doc[FPSTR(HA_ICON)] = F("mdi:radiator"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.temperatures.heating|float(0)|round(2) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("heating_temp")).c_str(), doc); - } - - bool publishSensorHeatingReturnTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("heating_return_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("heating_return_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - } - - doc[FPSTR(HA_NAME)] = F("Heating return temperature"); - doc[FPSTR(HA_ICON)] = F("mdi:radiator"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.temperatures.heatingReturn|float(0)|round(2) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_STATE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.master.uptime|int(0) }}"); + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("heating_return_temp")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SENSOR), F("uptime")).c_str(), doc); } - bool publishSensorDhwTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("dhw_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("dhw_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - } - - doc[FPSTR(HA_NAME)] = F("DHW temperature"); - doc[FPSTR(HA_ICON)] = F("mdi:water-pump"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.temperatures.dhw|float(0)|round(2) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("dhw_temp")).c_str(), doc); - } - - bool publishSensorExhaustTemp(UnitSystem unit = UnitSystem::METRIC, bool enabledByDefault = true) { + bool publishClimateHeating(UnitSystem unit = UnitSystem::METRIC, byte minTemp = 20, byte maxTemp = 90, bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.otStatus, 'online', 'offline') }}"); - doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("exhaust_temp")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("exhaust_temp")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("diagnostic"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("temperature"); - doc[FPSTR(HA_STATE_CLASS)] = F("measurement"); - - if (unit == UnitSystem::METRIC) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_C); - - } else if (unit == UnitSystem::IMPERIAL) { - doc[FPSTR(HA_UNIT_OF_MEASUREMENT)] = FPSTR(HA_UNIT_OF_MEASUREMENT_F); - } - - doc[FPSTR(HA_NAME)] = F("Exhaust temperature"); - doc[FPSTR(HA_ICON)] = F("mdi:smoke"); - doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.temperatures.exhaust|float(0)|round(2) }}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; - doc.shrinkToFit(); - - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("exhaust_temp")).c_str(), doc); - } - - - bool publishClimateHeating(UnitSystem unit = UnitSystem::METRIC, byte minTemp = 20, byte maxTemp = 90, byte currentTempSource = HaHelper::TEMP_SOURCE_HEATING, bool enabledByDefault = true) { - JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("heating")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("heating")); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("heating")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; doc[FPSTR(HA_NAME)] = F("Heating"); doc[FPSTR(HA_ICON)] = F("mdi:radiator"); - if (currentTempSource == HaHelper::TEMP_SOURCE_HEATING || currentTempSource == HaHelper::TEMP_SOURCE_INDOOR) { - doc[FPSTR(HA_CURRENT_TEMPERATURE_TOPIC)] = this->getDeviceTopic(F("state")); - } - - if (currentTempSource == HaHelper::TEMP_SOURCE_HEATING) { - doc[FPSTR(HA_CURRENT_TEMPERATURE_TEMPLATE)] = F("{{ value_json.temperatures.heating|float(0)|round(2) }}"); + doc[FPSTR(HA_CURRENT_TEMPERATURE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_CURRENT_TEMPERATURE_TEMPLATE)] = F("{{ iif(value_json.master.heating.indoorTempControl, value_json.master.heating.indoorTemp, value_json.master.heating.currentTemp, 0)|float(0)|round(2) }}"); - } else if (currentTempSource == HaHelper::TEMP_SOURCE_INDOOR) { - doc[FPSTR(HA_CURRENT_TEMPERATURE_TEMPLATE)] = F("{{ value_json.temperatures.indoor|float(0)|round(2) }}"); - } - - doc[FPSTR(HA_TEMPERATURE_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_TEMPERATURE_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_TEMPERATURE_COMMAND_TEMPLATE)] = F("{\"heating\": {\"target\" : {{ value }}}}"); - doc[FPSTR(HA_TEMPERATURE_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_TEMPERATURE_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_TEMPERATURE_STATE_TEMPLATE)] = F("{{ value_json.heating.target|float(0)|round(1) }}"); if (unit == UnitSystem::METRIC) { @@ -1545,49 +1195,49 @@ class HaHelper : public HomeAssistantHelper { doc[FPSTR(HA_TEMPERATURE_UNIT)] = "F"; } - doc[FPSTR(HA_MODE_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); - doc[FPSTR(HA_MODE_COMMAND_TEMPLATE)] = F("{% if value == 'heat' %}{\"heating\": {\"enable\" : true}}" - "{% elif value == 'off' %}{\"heating\": {\"enable\" : false}}{% endif %}"); - doc[FPSTR(HA_MODE_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); - doc[FPSTR(HA_MODE_STATE_TEMPLATE)] = F("{{ iif(value_json.heating.enable, 'heat', 'off') }}"); + doc[FPSTR(HA_MODE_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); + doc[FPSTR(HA_MODE_COMMAND_TEMPLATE)] = F("{% if value == 'heat' %}{\"heating\": {\"enabled\" : true}}" + "{% elif value == 'off' %}{\"heating\": {\"enabled\" : false}}{% endif %}"); + doc[FPSTR(HA_MODE_STATE_TOPIC)] = this->settingsTopic.c_str(); + doc[FPSTR(HA_MODE_STATE_TEMPLATE)] = F("{{ iif(value_json.heating.enabled, 'heat', 'off') }}"); doc[FPSTR(HA_MODES)][0] = F("off"); doc[FPSTR(HA_MODES)][1] = F("heat"); - doc[FPSTR(HA_ACTION_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_ACTION_TEMPLATE)] = F("{{ iif(value_json.states.heating, 'heating', 'idle') }}"); + doc[FPSTR(HA_ACTION_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_ACTION_TEMPLATE)] = F("{{ iif(value_json.slave.heating.active, 'heating', 'idle') }}"); - doc[FPSTR(HA_PRESET_MODE_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_PRESET_MODE_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_PRESET_MODE_COMMAND_TEMPLATE)] = F("{% if value == 'boost' %}{\"heating\": {\"turbo\" : true}}" "{% elif value == 'none' %}{\"heating\": {\"turbo\" : false}}{% endif %}"); - doc[FPSTR(HA_PRESET_MODE_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_PRESET_MODE_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_PRESET_MODE_VALUE_TEMPLATE)] = F("{{ iif(value_json.heating.turbo, 'boost', 'none') }}"); doc[FPSTR(HA_PRESET_MODES)][0] = F("boost"); doc[FPSTR(HA_MIN_TEMP)] = minTemp; doc[FPSTR(HA_MAX_TEMP)] = maxTemp; doc[FPSTR(HA_TEMP_STEP)] = 0.5f; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_CLIMATE), F("heating"), '_').c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_CLIMATE), F("heating"), '_').c_str(), doc); } bool publishClimateDhw(UnitSystem unit = UnitSystem::METRIC, byte minTemp = 40, byte maxTemp = 60, bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("dhw")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("dhw")); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("dhw")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; doc[FPSTR(HA_NAME)] = F("DHW"); - doc[FPSTR(HA_ICON)] = F("mdi:water-pump"); + doc[FPSTR(HA_ICON)] = F("mdi:faucet"); - doc[FPSTR(HA_CURRENT_TEMPERATURE_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_CURRENT_TEMPERATURE_TEMPLATE)] = F("{{ value_json.temperatures.dhw|float(0)|round(1) }}"); + doc[FPSTR(HA_CURRENT_TEMPERATURE_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_CURRENT_TEMPERATURE_TEMPLATE)] = F("{{ value_json.master.dhw.currentTemp|float(0)|round(1) }}"); - doc[FPSTR(HA_TEMPERATURE_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); + doc[FPSTR(HA_TEMPERATURE_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_TEMPERATURE_COMMAND_TEMPLATE)] = F("{\"dhw\": {\"target\" : {{ value|int(0) }}}}"); - doc[FPSTR(HA_TEMPERATURE_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); + doc[FPSTR(HA_TEMPERATURE_STATE_TOPIC)] = this->settingsTopic.c_str(); doc[FPSTR(HA_TEMPERATURE_STATE_TEMPLATE)] = F("{{ value_json.dhw.target|float(0)|round(1) }}"); if (unit == UnitSystem::METRIC) { @@ -1597,137 +1247,117 @@ class HaHelper : public HomeAssistantHelper { doc[FPSTR(HA_TEMPERATURE_UNIT)] = "F"; } - doc[FPSTR(HA_MODE_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set")); - doc[FPSTR(HA_MODE_COMMAND_TEMPLATE)] = F("{% if value == 'heat' %}{\"dhw\": {\"enable\" : true}}" - "{% elif value == 'off' %}{\"dhw\": {\"enable\" : false}}{% endif %}"); - doc[FPSTR(HA_MODE_STATE_TOPIC)] = this->getDeviceTopic(F("settings")); - doc[FPSTR(HA_MODE_STATE_TEMPLATE)] = F("{{ iif(value_json.dhw.enable, 'heat', 'off') }}"); + doc[FPSTR(HA_MODE_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); + doc[FPSTR(HA_MODE_COMMAND_TEMPLATE)] = F("{% if value == 'heat' %}{\"dhw\": {\"enabled\" : true}}" + "{% elif value == 'off' %}{\"dhw\": {\"enabled\" : false}}{% endif %}"); + doc[FPSTR(HA_MODE_STATE_TOPIC)] = this->settingsTopic.c_str(); + doc[FPSTR(HA_MODE_STATE_TEMPLATE)] = F("{{ iif(value_json.dhw.enabled, 'heat', 'off') }}"); doc[FPSTR(HA_MODES)][0] = F("off"); doc[FPSTR(HA_MODES)][1] = F("heat"); - doc[FPSTR(HA_ACTION_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_ACTION_TEMPLATE)] = F("{{ iif(value_json.states.dhw, 'heating', 'idle') }}"); + doc[FPSTR(HA_ACTION_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_ACTION_TEMPLATE)] = F("{{ iif(value_json.slave.dhw.active, 'heating', 'idle') }}"); doc[FPSTR(HA_MIN_TEMP)] = minTemp; doc[FPSTR(HA_MAX_TEMP)] = maxTemp; - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_CLIMATE), F("dhw"), '_').c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_CLIMATE), F("dhw"), '_').c_str(), doc); } - bool publishButtonRestart(bool enabledByDefault = true) { + bool publishRestartButton(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); + doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("restart")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("restart")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("restart"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(FPSTR(S_RESTART)); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_RESTART); doc[FPSTR(HA_NAME)] = F("Restart"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("state/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setStateTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"actions\": {\"restart\": true}}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BUTTON), F("restart")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BUTTON), FPSTR(S_RESTART)).c_str(), doc); } - bool publishButtonResetFault(bool enabledByDefault = true) { + bool publishResetFaultButton(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.fault, 'online', 'offline') }}"); + doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.fault.active, 'online', 'offline') }}"); doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("reset_fault")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("reset_fault")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("restart"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("reset_fault")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_RESTART); doc[FPSTR(HA_NAME)] = F("Reset fault"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("state/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setStateTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"actions\": {\"resetFault\": true}}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BUTTON), F("reset_fault")).c_str(), doc); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BUTTON), F("reset_fault")).c_str(), doc); } - bool publishButtonResetDiagnostic(bool enabledByDefault = true) { + bool publishResetDiagButton(bool enabledByDefault = true) { JsonDocument doc; - doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("status")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->getDeviceTopic(F("state")); - doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.states.diagnostic, 'online', 'offline') }}"); + doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_TOPIC)] = this->stateTopic.c_str(); + doc[FPSTR(HA_AVAILABILITY)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.slave.diag.active, 'online', 'offline') }}"); doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; - doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("reset_diagnostic")); - doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("reset_diagnostic")); - doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config"); - doc[FPSTR(HA_DEVICE_CLASS)] = F("restart"); + doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("reset_diagnostic")); + doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; + doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); + doc[FPSTR(HA_DEVICE_CLASS)] = FPSTR(S_RESTART); doc[FPSTR(HA_NAME)] = F("Reset diagnostic"); - doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("state/set")); + doc[FPSTR(HA_COMMAND_TOPIC)] = this->setStateTopic.c_str(); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"actions\": {\"resetDiagnostic\": true}}"); - doc[FPSTR(HA_EXPIRE_AFTER)] = 120; + doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc.shrinkToFit(); - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BUTTON), F("reset_diagnostic")).c_str(), doc); - } - - - bool deleteInputOutdoorTemp() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("outdoor_temp")).c_str()); - } - - bool deleteSensorOutdoorTemp() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("outdoor_temp")).c_str()); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BUTTON), F("reset_diagnostic")).c_str(), doc); } - bool deleteInputIndoorTemp() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("indoor_temp")).c_str()); - } - bool deleteSensorIndoorTemp() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("indoor_temp")).c_str()); + template + bool deleteEntities(CT category) { + return this->publish(this->makeConfigTopic(category).c_str()); } bool deleteSwitchDhw() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SWITCH), F("dhw")).c_str()); - } - - bool deleteSensorBoilerDhwMinTemp() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("boiler_dhw_min_temp")).c_str()); - } - - bool deleteSensorBoilerDhwMaxTemp() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("boiler_dhw_max_temp")).c_str()); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_SWITCH), F("dhw")).c_str()); } bool deleteInputDhwMinTemp() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("dhw_min_temp")).c_str()); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("dhw_min_temp")).c_str()); } bool deleteInputDhwMaxTemp() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("dhw_max_temp")).c_str()); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("dhw_max_temp")).c_str()); } - bool deleteStateDhw() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("dhw")).c_str()); + bool deleteDhwState() { + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_BINARY_SENSOR), F("dhw")).c_str()); } - - bool deleteSensorDhwTemp() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("dhw_temp")).c_str()); - } - + bool deleteInputDhwTarget() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_NUMBER), F("dhw_target")).c_str()); - } - - bool deleteSensorDhwFlowRate() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_SENSOR), F("dhw_flow_rate")).c_str()); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("dhw_target")).c_str()); } bool deleteClimateDhw() { - return this->publish(this->getTopic(FPSTR(HA_ENTITY_CLIMATE), F("dhw"), '_').c_str()); + return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_CLIMATE), F("dhw"), '_').c_str()); } + + protected: + unsigned short expireAfter = 300u; + String statusTopic, stateTopic, setStateTopic, settingsTopic, setSettingsTopic; }; + +const char HaHelper::AVAILABILITY_OT_CONN[] = "{{ iif(value_json.slave.connected, 'online', 'offline') }}"; +const char HaHelper::AVAILABILITY_SENSOR_CONN[] = "{{ iif(value_json.connected, 'online', 'offline') }}"; \ No newline at end of file diff --git a/src/MainTask.h b/src/MainTask.h index f592927..e80bc0f 100644 --- a/src/MainTask.h +++ b/src/MainTask.h @@ -5,7 +5,7 @@ using namespace NetworkUtils; extern NetworkMgr* network; extern MqttTask* tMqtt; extern OpenThermTask* tOt; -extern FileData fsSettings, fsNetworkSettings; +extern FileData fsNetworkSettings, fsSettings, fsSensorsSettings; extern ESPTelnetStream* telnetStream; @@ -32,7 +32,8 @@ class MainTask : public Task { unsigned long lastHeapInfo = 0; unsigned int minFreeHeap = 0; unsigned int minMaxFreeBlockHeap = 0; - unsigned long restartSignalTime = 0; + bool restartSignalReceived = false; + unsigned long restartSignalReceivedTime = 0; bool heatingEnabled = false; unsigned long heatingDisabledTime = 0; PumpStartReason extPumpStartReason = PumpStartReason::NONE; @@ -60,31 +61,46 @@ class MainTask : public Task { void loop() { network->loop(); + if (fsNetworkSettings.tick() == FD_WRITE) { + Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Updated")); + } + if (fsSettings.tick() == FD_WRITE) { Log.sinfoln(FPSTR(L_SETTINGS), F("Updated")); } - if (fsNetworkSettings.tick() == FD_WRITE) { - Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Updated")); + if (fsSensorsSettings.tick() == FD_WRITE) { + Log.sinfoln(FPSTR(L_SENSORS_SETTINGS), F("Updated")); } if (vars.actions.restart) { + this->restartSignalReceivedTime = millis(); + this->restartSignalReceived = true; vars.actions.restart = false; - this->restartSignalTime = millis(); + + Log.sinfoln(FPSTR(L_MAIN), F("Received restart signal")); + } + + if (!vars.states.restarting && this->restartSignalReceived && millis() - this->restartSignalReceivedTime > 5000) { + vars.states.restarting = true; // save settings fsSettings.updateNow(); + // save sensors settings + fsSensorsSettings.updateNow(); + // force save network settings if (fsNetworkSettings.updateNow() == FD_FILE_ERR && LittleFS.begin()) { fsNetworkSettings.write(); } - Log.sinfoln(FPSTR(L_MAIN), F("Restart signal received. Restart after 10 sec.")); + Log.sinfoln(FPSTR(L_MAIN), F("Restart scheduled in 10 sec.")); } - vars.states.mqtt = tMqtt->isConnected(); - vars.sensors.rssi = network->isConnected() ? WiFi.RSSI() : 0; + vars.mqtt.connected = tMqtt->isConnected(); + vars.network.connected = network->isConnected(); + vars.network.rssi = network->isConnected() ? WiFi.RSSI() : 0; if (settings.system.logLevel >= TinyLogger::Level::SILENT && settings.system.logLevel <= TinyLogger::Level::VERBOSE) { if (Log.getLevel() != settings.system.logLevel) { @@ -98,20 +114,14 @@ class MainTask : public Task { this->telnetStarted = true; } - if (settings.mqtt.enable && !tMqtt->isEnabled()) { + if (settings.mqtt.enabled && !tMqtt->isEnabled()) { tMqtt->enable(); - } else if (!settings.mqtt.enable && tMqtt->isEnabled()) { + } else if (!settings.mqtt.enabled && tMqtt->isEnabled()) { tMqtt->disable(); } - if (settings.sensors.indoor.type == SensorType::MANUAL) { - vars.sensors.indoor.connected = !settings.mqtt.enable || vars.states.mqtt; - } - - if (settings.sensors.outdoor.type == SensorType::MANUAL) { - vars.sensors.outdoor.connected = !settings.mqtt.enable || vars.states.mqtt; - } + Sensors::setConnectionStatusByType(Sensors::Type::MANUAL, !settings.mqtt.enabled || vars.mqtt.connected, false); } else { if (this->telnetStarted) { @@ -123,13 +133,7 @@ class MainTask : public Task { tMqtt->disable(); } - if (settings.sensors.indoor.type == SensorType::MANUAL) { - vars.sensors.indoor.connected = false; - } - - if (settings.sensors.outdoor.type == SensorType::MANUAL) { - vars.sensors.outdoor.connected = false; - } + Sensors::setConnectionStatusByType(Sensors::Type::MANUAL, false, false); } this->yield(); @@ -151,8 +155,9 @@ class MainTask : public Task { for (Stream* stream : Log.getStreams()) { while (stream->available() > 0) { stream->read(); + #ifdef ARDUINO_ARCH_ESP8266 - ::delay(0); + ::optimistic_yield(1000); #endif } } @@ -162,8 +167,9 @@ class MainTask : public Task { // restart - if (this->restartSignalTime > 0 && millis() - this->restartSignalTime > 10000) { - this->restartSignalTime = 0; + if (this->restartSignalReceived && millis() - this->restartSignalReceivedTime > 15000) { + this->restartSignalReceived = false; + ESP.restart(); } } @@ -172,8 +178,10 @@ class MainTask : public Task { unsigned int freeHeap = getFreeHeap(); unsigned int maxFreeBlockHeap = getMaxFreeBlockHeap(); - if (!this->restartSignalTime && (freeHeap < 2048 || maxFreeBlockHeap < 2048)) { - this->restartSignalTime = millis(); + // critical heap + if (!vars.states.restarting && (freeHeap < 2048 || maxFreeBlockHeap < 2048)) { + this->restartSignalReceivedTime = millis(); + vars.states.restarting = true; } if (settings.system.logLevel < TinyLogger::Level::VERBOSE) { @@ -209,18 +217,21 @@ class MainTask : public Task { uint8_t emergencyFlags = 0b00000000; // set outdoor sensor flag - if (settings.equitherm.enable && !vars.sensors.outdoor.connected) { - emergencyFlags |= 0b00000001; - } - - // set indoor sensor flag - if (!settings.equitherm.enable && settings.pid.enable && !vars.sensors.indoor.connected) { - emergencyFlags |= 0b00000010; + if (settings.equitherm.enabled) { + if (!Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::OUTDOOR_TEMP)) { + emergencyFlags |= 0b00000001; + } } - // set indoor sensor flag for OT native heating control - if (settings.opentherm.nativeHeatingControl && !vars.sensors.indoor.connected) { - emergencyFlags |= 0b00000100; + // set indoor sensor flags + if (!Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::INDOOR_TEMP)) { + if (!settings.equitherm.enabled && settings.pid.enabled) { + emergencyFlags |= 0b00000010; + } + + if (settings.opentherm.nativeHeatingControl) { + emergencyFlags |= 0b00000100; + } } // if any flags is true @@ -230,10 +241,10 @@ class MainTask : public Task { this->emergencyDetected = true; this->emergencyFlipTime = millis(); - } else if (this->emergencyDetected && !vars.states.emergency) { + } else if (this->emergencyDetected && !vars.emergency.state) { // enable emergency if (millis() - this->emergencyFlipTime > (settings.emergency.tresholdTime * 1000)) { - vars.states.emergency = true; + vars.emergency.state = true; Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode enabled (%hhu)"), emergencyFlags); } } @@ -244,10 +255,10 @@ class MainTask : public Task { this->emergencyDetected = false; this->emergencyFlipTime = millis(); - } else if (!this->emergencyDetected && vars.states.emergency) { + } else if (!this->emergencyDetected && vars.emergency.state) { // disable emergency if (millis() - this->emergencyFlipTime > (settings.emergency.tresholdTime * 1000)) { - vars.states.emergency = false; + vars.emergency.state = false; Log.sinfoln(FPSTR(L_MAIN), F("Emergency mode disabled")); } } @@ -286,15 +297,15 @@ class MainTask : public Task { errors[errCount++] = 2; } - if (!vars.states.otStatus) { + if (!vars.slave.connected) { errors[errCount++] = 3; } - if (vars.states.fault) { + if (vars.slave.fault.active) { errors[errCount++] = 4; } - if (vars.states.emergency) { + if (vars.emergency.state) { errors[errCount++] = 5; } @@ -342,7 +353,7 @@ class MainTask : public Task { static unsigned long outputChangedTs = 0; // input - if (settings.cascadeControl.input.enable) { + if (settings.cascadeControl.input.enabled) { if (settings.cascadeControl.input.gpio != configuredInputGpio) { if (configuredInputGpio != GPIO_IS_NOT_CONFIGURED) { pinMode(configuredInputGpio, OUTPUT); @@ -393,7 +404,7 @@ class MainTask : public Task { } } - if (!settings.cascadeControl.input.enable || configuredInputGpio == GPIO_IS_NOT_CONFIGURED) { + if (!settings.cascadeControl.input.enabled || configuredInputGpio == GPIO_IS_NOT_CONFIGURED) { if (!vars.cascadeControl.input) { vars.cascadeControl.input = true; @@ -407,7 +418,7 @@ class MainTask : public Task { // output - if (settings.cascadeControl.output.enable) { + if (settings.cascadeControl.output.enabled) { if (settings.cascadeControl.output.gpio != configuredOutputGpio) { if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) { pinMode(configuredOutputGpio, OUTPUT); @@ -437,13 +448,13 @@ class MainTask : public Task { if (configuredOutputGpio != GPIO_IS_NOT_CONFIGURED) { bool value = false; - if (settings.cascadeControl.output.onFault && vars.states.fault) { + if (settings.cascadeControl.output.onFault && vars.slave.fault.active) { value = true; - } else if (settings.cascadeControl.output.onLossConnection && !vars.states.otStatus) { + } else if (settings.cascadeControl.output.onLossConnection && !vars.slave.connected) { value = true; - } else if (settings.cascadeControl.output.onEnabledHeating && settings.heating.enable && vars.cascadeControl.input) { + } else if (settings.cascadeControl.output.onEnabledHeating && settings.heating.enabled && vars.cascadeControl.input) { value = true; } @@ -475,7 +486,7 @@ class MainTask : public Task { } } - if (!settings.cascadeControl.output.enable || configuredOutputGpio == GPIO_IS_NOT_CONFIGURED) { + if (!settings.cascadeControl.output.enabled || configuredOutputGpio == GPIO_IS_NOT_CONFIGURED) { if (vars.cascadeControl.output) { vars.cascadeControl.output = false; @@ -516,75 +527,75 @@ class MainTask : public Task { } if (configuredGpio == GPIO_IS_NOT_CONFIGURED) { - if (vars.states.externalPump) { - vars.states.externalPump = false; - vars.parameters.extPumpLastEnableTime = millis(); + if (vars.externalPump.state) { + vars.externalPump.state = false; + vars.externalPump.lastEnabledTime = millis(); - Log.sinfoln("EXTPUMP", F("Disabled: use = off")); + Log.sinfoln(FPSTR(L_EXTPUMP), F("Disabled: use = off")); } return; } - if (!vars.states.heating && this->heatingEnabled) { + if (!vars.master.heating.enabled && this->heatingEnabled) { this->heatingEnabled = false; this->heatingDisabledTime = millis(); - } else if (vars.states.heating && !this->heatingEnabled) { + } else if (vars.master.heating.enabled && !this->heatingEnabled) { this->heatingEnabled = true; } if (!settings.externalPump.use) { - if (vars.states.externalPump) { + if (vars.externalPump.state) { digitalWrite(configuredGpio, LOW); - vars.states.externalPump = false; - vars.parameters.extPumpLastEnableTime = millis(); + vars.externalPump.state = false; + vars.externalPump.lastEnabledTime = millis(); - Log.sinfoln("EXTPUMP", F("Disabled: use = off")); + Log.sinfoln(FPSTR(L_EXTPUMP), F("Disabled: use = off")); } return; } - if (vars.states.externalPump && !this->heatingEnabled) { + if (vars.externalPump.state && !this->heatingEnabled) { if (this->extPumpStartReason == MainTask::PumpStartReason::HEATING && millis() - this->heatingDisabledTime > (settings.externalPump.postCirculationTime * 1000u)) { digitalWrite(configuredGpio, LOW); - vars.states.externalPump = false; - vars.parameters.extPumpLastEnableTime = millis(); + vars.externalPump.state = false; + vars.externalPump.lastEnabledTime = millis(); - Log.sinfoln("EXTPUMP", F("Disabled: expired post circulation time")); + Log.sinfoln(FPSTR(L_EXTPUMP), F("Disabled: expired post circulation time")); } else if (this->extPumpStartReason == MainTask::PumpStartReason::ANTISTUCK && millis() - this->externalPumpStartTime >= (settings.externalPump.antiStuckTime * 1000u)) { digitalWrite(configuredGpio, LOW); - vars.states.externalPump = false; - vars.parameters.extPumpLastEnableTime = millis(); + vars.externalPump.state = false; + vars.externalPump.lastEnabledTime = millis(); - Log.sinfoln("EXTPUMP", F("Disabled: expired anti stuck time")); + Log.sinfoln(FPSTR(L_EXTPUMP), F("Disabled: expired anti stuck time")); } - } else if (vars.states.externalPump && this->heatingEnabled && this->extPumpStartReason == MainTask::PumpStartReason::ANTISTUCK) { + } else if (vars.externalPump.state && this->heatingEnabled && this->extPumpStartReason == MainTask::PumpStartReason::ANTISTUCK) { this->extPumpStartReason = MainTask::PumpStartReason::HEATING; - } else if (!vars.states.externalPump && this->heatingEnabled) { - vars.states.externalPump = true; + } else if (!vars.externalPump.state && this->heatingEnabled) { + vars.externalPump.state = true; this->externalPumpStartTime = millis(); this->extPumpStartReason = MainTask::PumpStartReason::HEATING; digitalWrite(configuredGpio, HIGH); - Log.sinfoln("EXTPUMP", F("Enabled: heating on")); + Log.sinfoln(FPSTR(L_EXTPUMP), F("Enabled: heating on")); - } else if (!vars.states.externalPump && (vars.parameters.extPumpLastEnableTime == 0 || millis() - vars.parameters.extPumpLastEnableTime >= (settings.externalPump.antiStuckInterval * 1000ul))) { - vars.states.externalPump = true; + } else if (!vars.externalPump.state && (vars.externalPump.lastEnabledTime == 0 || millis() - vars.externalPump.lastEnabledTime >= (settings.externalPump.antiStuckInterval * 1000lu))) { + vars.externalPump.state = true; this->externalPumpStartTime = millis(); this->extPumpStartReason = MainTask::PumpStartReason::ANTISTUCK; digitalWrite(configuredGpio, HIGH); - Log.sinfoln("EXTPUMP", F("Enabled: anti stuck")); + Log.sinfoln(FPSTR(L_EXTPUMP), F("Enabled: anti stuck")); } } }; \ No newline at end of file diff --git a/src/MqttTask.h b/src/MqttTask.h index cd81fcf..fe29a9c 100644 --- a/src/MqttTask.h +++ b/src/MqttTask.h @@ -1,3 +1,4 @@ +#include #include #include #include @@ -61,10 +62,18 @@ class MqttTask : public Task { this->prevPubSettingsTime = 0; } + inline void resetPublishedSensorTime(uint8_t sensorId) { + this->prevPubSensorTime[sensorId] = 0; + } + inline void resetPublishedVarsTime() { this->prevPubVarsTime = 0; } + inline void rebuildHaEntity(uint8_t sensorId, Sensors::Settings& prevSettings) { + this->queueRebuildingHaEntities[sensorId] = prevSettings; + } + protected: MqttWiFiClient* wifiClient = nullptr; MqttClient* client = nullptr; @@ -72,12 +81,14 @@ class MqttTask : public Task { MqttWriter* writer = nullptr; UnitSystem currentUnitSystem = UnitSystem::METRIC; bool currentHomeAssistantDiscovery = false; + std::unordered_map queueRebuildingHaEntities; unsigned short readyForSendTime = 30000; unsigned long lastReconnectTime = 0; unsigned long connectedTime = 0; unsigned long disconnectedTime = 0; unsigned long prevPubVarsTime = 0; unsigned long prevPubSettingsTime = 0; + std::unordered_map prevPubSensorTime; bool connected = false; bool newConnection = false; @@ -118,7 +129,7 @@ class MqttTask : public Task { #endif this->client->onMessage([this] (void*, size_t length) { - String topic = this->client->messageTopic(); + const String& topic = this->client->messageTopic(); if (!length || length > 2048 || !topic.length()) { return; } @@ -128,7 +139,7 @@ class MqttTask : public Task { payload[i] = this->client->read(); } - this->onMessage(topic.c_str(), payload, length); + this->onMessage(topic, payload, length); }); // writer settings @@ -142,7 +153,7 @@ class MqttTask : public Task { Log.straceln(FPSTR(L_MQTT), F("%s publish %u of %u bytes to topic: %s"), result ? F("Successfully") : F("Failed"), written, length, topic); #ifdef ARDUINO_ARCH_ESP8266 - ::delay(0); + ::optimistic_yield(1000); #endif //this->client->poll(); @@ -151,13 +162,13 @@ class MqttTask : public Task { #ifdef ARDUINO_ARCH_ESP8266 this->writer->setFlushEventCallback([this] (size_t, size_t) { - ::delay(0); + ::optimistic_yield(1000); if (this->wifiClient->connected()) { this->wifiClient->flush(); } - ::delay(0); + ::optimistic_yield(1000); }); #endif @@ -173,9 +184,8 @@ class MqttTask : public Task { } void loop() { - if (settings.mqtt.interval > 120) { - settings.mqtt.interval = 5; - fsSettings.update(); + if (vars.states.restarting || vars.states.upgrading) { + return; } if (this->connected && !this->client->connected()) { @@ -186,9 +196,15 @@ class MqttTask : public Task { Log.sinfoln(FPSTR(L_MQTT), F("Connecting to %s:%u..."), settings.mqtt.server, settings.mqtt.port); this->haHelper->setDevicePrefix(settings.mqtt.prefix); + this->haHelper->updateCachedTopics(); this->client->stop(); this->client->setId(networkSettings.hostname); this->client->setUsernamePassword(settings.mqtt.user, settings.mqtt.password); + + this->client->beginWill(this->haHelper->getDeviceTopic(F("status")).c_str(), 7, true, 1); + this->client->print(F("offline")); + this->client->endWill(); + this->client->connect(settings.mqtt.server, settings.mqtt.port); this->lastReconnectTime = millis(); this->yield(); @@ -210,22 +226,44 @@ class MqttTask : public Task { } #ifdef ARDUINO_ARCH_ESP8266 - ::delay(0); + ::optimistic_yield(1000); #endif // publish variables and status if (this->newConnection || millis() - this->prevPubVarsTime > (settings.mqtt.interval * 1000u)) { - this->writer->publish(this->haHelper->getDeviceTopic("status").c_str(), "online", false); - this->publishVariables(this->haHelper->getDeviceTopic("state").c_str()); + this->writer->publish(this->haHelper->getDeviceTopic(F("status")).c_str(), "online", false); + this->publishVariables(this->haHelper->getDeviceTopic(F("state")).c_str()); this->prevPubVarsTime = millis(); } // publish settings if (this->newConnection || millis() - this->prevPubSettingsTime > (settings.mqtt.interval * 10000u)) { - this->publishSettings(this->haHelper->getDeviceTopic("settings").c_str()); + this->publishSettings(this->haHelper->getDeviceTopic(F("settings")).c_str()); this->prevPubSettingsTime = millis(); } + // publish sensors + for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { + if (!Sensors::hasEnabledAndValid(sensorId)) { + continue; + } + + auto& rSensor = Sensors::results[sensorId]; + bool needUpdate = false; + if (millis() - this->prevPubSensorTime[sensorId] > ((this->haHelper->getExpireAfter() - 10) * 1000u)) { + needUpdate = true; + + } else if (rSensor.activityTime >= this->prevPubSensorTime[sensorId]) { + auto estimated = rSensor.activityTime - this->prevPubSensorTime[sensorId]; + needUpdate = estimated > 1000u; + } + + if (this->newConnection || needUpdate) { + this->publishSensor(sensorId); + this->prevPubSensorTime[sensorId] = millis(); + } + } + // publish ha entities if not published if (settings.mqtt.homeAssistantDiscovery) { if (this->newConnection || !this->currentHomeAssistantDiscovery || this->currentUnitSystem != settings.system.unitSystem) { @@ -239,6 +277,79 @@ class MqttTask : public Task { this->publishNonStaticHaEntities(); } + + for (auto& [sensorId, prevSettings] : this->queueRebuildingHaEntities) { + Log.sinfoln(FPSTR(L_MQTT_HA), F("Rebuilding config for sensor #%hhu '%s'"), sensorId, prevSettings.name); + + // delete old config + if (strlen(prevSettings.name) && prevSettings.enabled) { + switch (prevSettings.type) { + case Sensors::Type::BLUETOOTH: + this->haHelper->deleteConnectionDynamicSensor(prevSettings); + this->haHelper->deleteSignalQualityDynamicSensor(prevSettings); + this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::TEMPERATURE); + this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::HUMIDITY); + this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::BATTERY); + this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::RSSI); + break; + + case Sensors::Type::DALLAS_TEMP: + this->haHelper->deleteConnectionDynamicSensor(prevSettings); + this->haHelper->deleteSignalQualityDynamicSensor(prevSettings); + this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::TEMPERATURE); + break; + + case Sensors::Type::MANUAL: + this->client->unsubscribe( + this->haHelper->getDeviceTopic( + F("sensors"), + Sensors::makeObjectId(prevSettings.name).c_str(), + F("set") + ).c_str() + ); + + default: + this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::PRIMARY); + } + } + + if (!Sensors::hasEnabledAndValid(sensorId)) { + continue; + } + + // make new config + auto& sSettings = Sensors::settings[sensorId]; + switch (sSettings.type) { + case Sensors::Type::BLUETOOTH: + this->haHelper->publishConnectionDynamicSensor(sSettings); + this->haHelper->publishSignalQualityDynamicSensor(sSettings, false); + this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem); + this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::HUMIDITY, settings.system.unitSystem); + this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::BATTERY, settings.system.unitSystem); + this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::RSSI, settings.system.unitSystem, false); + break; + + case Sensors::Type::DALLAS_TEMP: + this->haHelper->publishConnectionDynamicSensor(sSettings); + this->haHelper->publishSignalQualityDynamicSensor(sSettings, false); + this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem); + break; + + case Sensors::Type::MANUAL: + this->client->subscribe( + this->haHelper->getDeviceTopic( + F("sensors"), + Sensors::makeObjectId(prevSettings.name).c_str(), + F("set") + ).c_str() + ); + + default: + this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::PRIMARY, settings.system.unitSystem); + } + } + this->queueRebuildingHaEntities.clear(); + } else if (this->currentHomeAssistantDiscovery) { this->currentHomeAssistantDiscovery = false; } @@ -254,24 +365,24 @@ class MqttTask : public Task { unsigned long downtime = (millis() - this->disconnectedTime) / 1000; Log.sinfoln(FPSTR(L_MQTT), F("Connected (downtime: %u s.)"), downtime); - this->client->subscribe(this->haHelper->getDeviceTopic("settings/set").c_str()); - this->client->subscribe(this->haHelper->getDeviceTopic("state/set").c_str()); + this->client->subscribe(this->haHelper->getDeviceTopic(F("settings/set")).c_str()); + this->client->subscribe(this->haHelper->getDeviceTopic(F("state/set")).c_str()); } void onDisconnect() { this->disconnectedTime = millis(); unsigned long uptime = (millis() - this->connectedTime) / 1000; - Log.swarningln(FPSTR(L_MQTT), F("Disconnected (reason: %d uptime: %u s.)"), this->client->connectError(), uptime); + Log.swarningln(FPSTR(L_MQTT), F("Disconnected (reason: %d uptime: %lu s.)"), this->client->connectError(), uptime); } - void onMessage(const char* topic, uint8_t* payload, size_t length) { + void onMessage(const String& topic, uint8_t* payload, size_t length) { if (!length) { return; } if (settings.system.logLevel >= TinyLogger::Level::TRACE) { - Log.strace(FPSTR(L_MQTT_MSG), F("Topic: %s\r\n> "), topic); + Log.strace(FPSTR(L_MQTT_MSG), F("Topic: %s\r\n> "), topic.c_str()); if (Log.lock()) { for (size_t i = 0; i < length; i++) { if (payload[i] == 0) { @@ -279,12 +390,12 @@ class MqttTask : public Task { } else if (payload[i] == 13) { continue; } else if (payload[i] == 10) { - Log.print("\r\n> "); + Log.print(F("\r\n> ")); } else { Log.print((char) payload[i]); } } - Log.print("\r\n\n"); + Log.print(F("\r\n\n")); Log.flush(); Log.unlock(); } @@ -302,34 +413,48 @@ class MqttTask : public Task { } doc.shrinkToFit(); - if (this->haHelper->getDeviceTopic("state/set").equals(topic)) { - this->writer->publish(this->haHelper->getDeviceTopic("state/set").c_str(), nullptr, 0, true); - + // delete topic + this->writer->publish(topic.c_str(), nullptr, 0, true); + + if (this->haHelper->getDeviceTopic(F("state/set")).equals(topic)) { if (jsonToVars(doc, vars)) { this->resetPublishedVarsTime(); } - } else if (this->haHelper->getDeviceTopic("settings/set").equals(topic)) { - this->writer->publish(this->haHelper->getDeviceTopic("settings/set").c_str(), nullptr, 0, true); - + } else if (this->haHelper->getDeviceTopic(F("settings/set")).equals(topic)) { if (safeJsonToSettings(doc, settings)) { this->resetPublishedSettingsTime(); fsSettings.update(); } + + } else { + const String& sensorsTopic = this->haHelper->getDeviceTopic(F("sensors/")); + auto stLength = sensorsTopic.length(); + + if (topic.startsWith(sensorsTopic) && topic.endsWith(F("/set"))) { + if (topic.length() > stLength + 4) { + const String& name = topic.substring(stLength, topic.indexOf('/', stLength)); + int16_t id = Sensors::getIdByObjectId(name.c_str()); + + if (id == -1) { + return; + } + + if (jsonToSensorResult(id, doc)) { + this->resetPublishedSensorTime(id); + } + } + } } } void publishHaEntities() { // heating - this->haHelper->publishSwitchHeating(false); this->haHelper->publishSwitchHeatingTurbo(false); this->haHelper->publishInputHeatingHysteresis(settings.system.unitSystem); this->haHelper->publishInputHeatingTurboFactor(false); this->haHelper->publishInputHeatingMinTemp(settings.system.unitSystem); this->haHelper->publishInputHeatingMaxTemp(settings.system.unitSystem); - this->haHelper->publishSensorHeatingSetpoint(settings.system.unitSystem, false); - this->haHelper->publishSensorBoilerHeatingMinTemp(settings.system.unitSystem, false); - this->haHelper->publishSensorBoilerHeatingMaxTemp(settings.system.unitSystem, false); // pid this->haHelper->publishSwitchPid(); @@ -347,103 +472,98 @@ class MqttTask : public Task { this->haHelper->publishInputEquithermFactorT(false); // states - this->haHelper->publishStateStatus(); - this->haHelper->publishStateEmergency(); - this->haHelper->publishStateOtStatus(); - this->haHelper->publishStateHeating(); - this->haHelper->publishStateFlame(); - this->haHelper->publishStateFault(); - this->haHelper->publishStateDiagnostic(); - this->haHelper->publishStateExtPump(false); + this->haHelper->publishStatusState(); + this->haHelper->publishEmergencyState(); + this->haHelper->publishOpenthermConnectedState(); + this->haHelper->publishHeatingState(); + this->haHelper->publishFlameState(); + this->haHelper->publishFaultState(); + this->haHelper->publishDiagState(); + this->haHelper->publishExternalPumpState(false); // sensors - this->haHelper->publishSensorModulation(); - this->haHelper->publishSensorPressure(settings.system.unitSystem, false); - this->haHelper->publishSensorPower(); - this->haHelper->publishSensorFaultCode(); - this->haHelper->publishSensorDiagnosticCode(); - this->haHelper->publishSensorRssi(false); - this->haHelper->publishSensorUptime(false); - this->haHelper->publishOutdoorSensorConnected(); - this->haHelper->publishOutdoorSensorRssi(false); - this->haHelper->publishOutdoorSensorBattery(false); - this->haHelper->publishOutdoorSensorHumidity(false); - this->haHelper->publishIndoorSensorConnected(); - this->haHelper->publishIndoorSensorRssi(false); - this->haHelper->publishIndoorSensorBattery(false); - this->haHelper->publishIndoorSensorHumidity(false); - - // temperatures - this->haHelper->publishSensorHeatingTemp(settings.system.unitSystem); - this->haHelper->publishSensorHeatingReturnTemp(settings.system.unitSystem, false); - this->haHelper->publishSensorExhaustTemp(settings.system.unitSystem, false); + this->haHelper->publishFaultCode(); + this->haHelper->publishDiagCode(); + this->haHelper->publishNetworkRssi(false); + this->haHelper->publishUptime(false); // buttons - this->haHelper->publishButtonRestart(false); - this->haHelper->publishButtonResetFault(); - this->haHelper->publishButtonResetDiagnostic(); + this->haHelper->publishRestartButton(false); + this->haHelper->publishResetFaultButton(); + this->haHelper->publishResetDiagButton(); + + // dynamic sensors + for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { + if (!Sensors::hasEnabledAndValid(sensorId)) { + continue; + } + + auto& sSettings = Sensors::settings[sensorId]; + switch (sSettings.type) { + case Sensors::Type::BLUETOOTH: + this->haHelper->publishConnectionDynamicSensor(sSettings); + this->haHelper->publishSignalQualityDynamicSensor(sSettings, false); + this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem); + this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::HUMIDITY, settings.system.unitSystem); + this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::BATTERY, settings.system.unitSystem); + this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::RSSI, settings.system.unitSystem, false); + break; + + case Sensors::Type::DALLAS_TEMP: + this->haHelper->publishConnectionDynamicSensor(sSettings); + this->haHelper->publishSignalQualityDynamicSensor(sSettings, false); + this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem); + break; + + case Sensors::Type::MANUAL: + this->client->subscribe( + this->haHelper->getDeviceTopic( + F("sensors"), + Sensors::makeObjectId(sSettings.name).c_str(), + F("set") + ).c_str() + ); + + default: + this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::PRIMARY, settings.system.unitSystem); + } + } } bool publishNonStaticHaEntities(bool force = false) { static byte _heatingMinTemp, _heatingMaxTemp, _dhwMinTemp, _dhwMaxTemp = 0; - static bool _noRegulators, _editableOutdoorTemp, _editableIndoorTemp, _dhwPresent = false; + static bool _indoorTempControl, _dhwPresent = false; bool published = false; - bool noRegulators = !settings.opentherm.nativeHeatingControl && !settings.pid.enable && !settings.equitherm.enable; - byte heatingMinTemp = 0; - byte heatingMaxTemp = 0; - bool editableOutdoorTemp = settings.sensors.outdoor.type == SensorType::MANUAL; - bool editableIndoorTemp = settings.sensors.indoor.type == SensorType::MANUAL; - - if (noRegulators) { - heatingMinTemp = settings.heating.minTemp; - heatingMaxTemp = settings.heating.maxTemp; - - } else { - heatingMinTemp = convertTemp(THERMOSTAT_INDOOR_MIN_TEMP, UnitSystem::METRIC, settings.system.unitSystem); - heatingMaxTemp = convertTemp(THERMOSTAT_INDOOR_MAX_TEMP, UnitSystem::METRIC, settings.system.unitSystem); - } - if (force || _dhwPresent != settings.opentherm.dhwPresent) { _dhwPresent = settings.opentherm.dhwPresent; if (_dhwPresent) { - this->haHelper->publishSwitchDhw(false); - this->haHelper->publishSensorBoilerDhwMinTemp(settings.system.unitSystem, false); - this->haHelper->publishSensorBoilerDhwMaxTemp(settings.system.unitSystem, false); this->haHelper->publishInputDhwMinTemp(settings.system.unitSystem); this->haHelper->publishInputDhwMaxTemp(settings.system.unitSystem); - this->haHelper->publishStateDhw(); - this->haHelper->publishSensorDhwTemp(settings.system.unitSystem); - this->haHelper->publishSensorDhwFlowRate(settings.system.unitSystem); + this->haHelper->publishDhwState(); } else { this->haHelper->deleteSwitchDhw(); - this->haHelper->deleteSensorBoilerDhwMinTemp(); - this->haHelper->deleteSensorBoilerDhwMaxTemp(); this->haHelper->deleteInputDhwMinTemp(); this->haHelper->deleteInputDhwMaxTemp(); - this->haHelper->deleteStateDhw(); - this->haHelper->deleteSensorDhwTemp(); + this->haHelper->deleteDhwState(); this->haHelper->deleteInputDhwTarget(); this->haHelper->deleteClimateDhw(); - this->haHelper->deleteSensorDhwFlowRate(); } published = true; } - if (force || _noRegulators != noRegulators || _heatingMinTemp != heatingMinTemp || _heatingMaxTemp != heatingMaxTemp) { - _heatingMinTemp = heatingMinTemp; - _heatingMaxTemp = heatingMaxTemp; - _noRegulators = noRegulators; + if (force || _indoorTempControl != vars.master.heating.indoorTempControl || _heatingMinTemp != vars.master.heating.minTemp || _heatingMaxTemp != vars.master.heating.maxTemp) { + _heatingMinTemp = vars.master.heating.minTemp; + _heatingMaxTemp = vars.master.heating.maxTemp; + _indoorTempControl = vars.master.heating.indoorTempControl; - this->haHelper->publishInputHeatingTarget(settings.system.unitSystem, heatingMinTemp, heatingMaxTemp, false); this->haHelper->publishClimateHeating( settings.system.unitSystem, - heatingMinTemp, - heatingMaxTemp, - noRegulators ? HaHelper::TEMP_SOURCE_HEATING : HaHelper::TEMP_SOURCE_INDOOR + vars.master.heating.minTemp, + vars.master.heating.maxTemp ); published = true; @@ -453,49 +573,44 @@ class MqttTask : public Task { _dhwMinTemp = settings.dhw.minTemp; _dhwMaxTemp = settings.dhw.maxTemp; - this->haHelper->publishInputDhwTarget(settings.system.unitSystem, settings.dhw.minTemp, settings.dhw.maxTemp, false); this->haHelper->publishClimateDhw(settings.system.unitSystem, settings.dhw.minTemp, settings.dhw.maxTemp); published = true; } - if (force || _editableOutdoorTemp != editableOutdoorTemp) { - _editableOutdoorTemp = editableOutdoorTemp; + return published; + } - if (editableOutdoorTemp) { - this->haHelper->deleteSensorOutdoorTemp(); - this->haHelper->publishInputOutdoorTemp(settings.system.unitSystem); - } else { - this->haHelper->deleteInputOutdoorTemp(); - this->haHelper->publishSensorOutdoorTemp(settings.system.unitSystem); - } + bool publishSettings(const char* topic) { + JsonDocument doc; + safeSettingsToJson(settings, doc); + doc.shrinkToFit(); - published = true; - } + return this->writer->publish(topic, doc, true); + } - if (force || _editableIndoorTemp != editableIndoorTemp) { - _editableIndoorTemp = editableIndoorTemp; + bool publishSensor(uint8_t sensorId) { + auto& sSettings = Sensors::settings[sensorId]; - if (editableIndoorTemp) { - this->haHelper->deleteSensorIndoorTemp(); - this->haHelper->publishInputIndoorTemp(settings.system.unitSystem); - } else { - this->haHelper->deleteInputIndoorTemp(); - this->haHelper->publishSensorIndoorTemp(settings.system.unitSystem); - } + if (!Sensors::isValidSensorId(sensorId)) { + return false; - published = true; + } else if (!strlen(sSettings.name)) { + return false; } - return published; - } - - bool publishSettings(const char* topic) { JsonDocument doc; - safeSettingsToJson(settings, doc); + sensorResultToJson(sensorId, doc); doc.shrinkToFit(); - return this->writer->publish(topic, doc, true); + return this->writer->publish( + this->haHelper->getDeviceTopic( + F("sensors"), + Sensors::makeObjectId(sSettings.name).c_str() + ).c_str(), + doc, + true + ); } bool publishVariables(const char* topic) { diff --git a/src/OpenThermTask.h b/src/OpenThermTask.h index 542e80b..7d3d583 100644 --- a/src/OpenThermTask.h +++ b/src/OpenThermTask.h @@ -11,22 +11,22 @@ class OpenThermTask : public Task { protected: const unsigned short readyTime = 60000; - const unsigned short dhwSetTempInterval = 60000; const unsigned short heatingSetTempInterval = 60000; + const unsigned short dhwSetTempInterval = 60000; + const unsigned short ch2SetTempInterval = 60000; const unsigned int initializingInterval = 3600000; CustomOpenTherm* instance = nullptr; unsigned long instanceCreatedTime = 0; byte instanceInGpio = 0; byte instanceOutGpio = 0; - bool isInitialized = false; + bool initialized = false; unsigned long initializedTime = 0; - unsigned int initializedMemberIdCode = 0; unsigned long lastSuccessResponse = 0; unsigned long prevUpdateNonEssentialVars = 0; - unsigned long dhwSetTempTime = 0; unsigned long heatingSetTempTime = 0; - bool heatingBlocking = false; + unsigned long dhwSetTempTime = 0; + unsigned long ch2SetTempTime = 0; byte configuredRxLedGpio = GPIO_IS_NOT_CONFIGURED; #if defined(ARDUINO_ARCH_ESP32) @@ -44,11 +44,12 @@ class OpenThermTask : public Task { #endif void setup() { + // Convert defaults at start if (settings.system.unitSystem != UnitSystem::METRIC) { - vars.parameters.heatingMinTemp = convertTemp(vars.parameters.heatingMinTemp, UnitSystem::METRIC, settings.system.unitSystem); - vars.parameters.heatingMaxTemp = convertTemp(vars.parameters.heatingMaxTemp, UnitSystem::METRIC, settings.system.unitSystem); - vars.parameters.dhwMinTemp = convertTemp(vars.parameters.dhwMinTemp, UnitSystem::METRIC, settings.system.unitSystem); - vars.parameters.dhwMaxTemp = convertTemp(vars.parameters.dhwMaxTemp, UnitSystem::METRIC, settings.system.unitSystem); + vars.slave.heating.minTemp = convertTemp(vars.slave.heating.minTemp, UnitSystem::METRIC, settings.system.unitSystem); + vars.slave.heating.maxTemp = convertTemp(vars.slave.heating.maxTemp, UnitSystem::METRIC, settings.system.unitSystem); + vars.slave.dhw.minTemp = convertTemp(vars.slave.dhw.minTemp, UnitSystem::METRIC, settings.system.unitSystem); + vars.slave.dhw.maxTemp = convertTemp(vars.slave.dhw.maxTemp, UnitSystem::METRIC, settings.system.unitSystem); } // delete instance @@ -75,12 +76,12 @@ class OpenThermTask : public Task { this->instanceCreatedTime = millis(); this->instanceInGpio = settings.opentherm.inGpio; this->instanceOutGpio = settings.opentherm.outGpio; - this->isInitialized = false; + this->initialized = false; Log.sinfoln(FPSTR(L_OT), F("Started. GPIO IN: %hhu, GPIO OUT: %hhu"), settings.opentherm.inGpio, settings.opentherm.outGpio); this->instance->setAfterSendRequestCallback([this](unsigned long request, unsigned long response, OpenThermResponseStatus status, byte attempt) { - Log.straceln( + Log.sverboseln( FPSTR(L_OT), F("ID: %4d Request: %8lx Response: %8lx Attempt: %2d Status: %s"), CustomOpenTherm::getDataID(request), request, response, attempt, CustomOpenTherm::statusToString(status) @@ -105,14 +106,23 @@ class OpenThermTask : public Task { } void loop() { - static float currentHeatingTemp = 0.0f; - static float currentDhwTemp = 0.0f; + if (vars.states.restarting || vars.states.upgrading) { + return; + } if (this->instanceInGpio != settings.opentherm.inGpio || this->instanceOutGpio != settings.opentherm.outGpio) { this->setup(); - } else if (this->initializedMemberIdCode != settings.opentherm.memberIdCode || millis() - this->initializedTime > this->initializingInterval) { - this->isInitialized = false; + } else if (vars.master.memberId != settings.opentherm.memberId || vars.master.flags != settings.opentherm.flags) { + this->initialized = false; + vars.master.memberId = settings.opentherm.memberId; + vars.master.flags = settings.opentherm.flags; + vars.master.protocolVersion = 2.2f; + vars.master.appVersion = 0x3F; + vars.master.type = 0x01; + + } else if (millis() - this->initializedTime > this->initializingInterval) { + this->initialized = false; } if (this->instance == nullptr) { @@ -136,16 +146,26 @@ class OpenThermTask : public Task { } } - bool heatingEnabled = (vars.states.emergency || settings.heating.enable) + // Heating settings + vars.master.heating.enabled = this->isReady() + && (settings.heating.enabled || vars.emergency.state) && vars.cascadeControl.input - && this->isReady() - && !this->heatingBlocking; - bool heatingCh2Enabled = settings.opentherm.heatingCh2Enabled; + && !vars.master.heating.blocking; + + // DHW settings + vars.master.dhw.enabled = settings.opentherm.dhwPresent && settings.dhw.enabled; + vars.master.dhw.targetTemp = settings.dhw.target; + + // CH2 settings + vars.master.ch2.enabled = settings.opentherm.heatingCh2Enabled + || (settings.opentherm.heatingCh1ToCh2 && vars.master.heating.enabled) + || (settings.opentherm.dhwToCh2 && settings.opentherm.dhwPresent && settings.dhw.enabled); + if (settings.opentherm.heatingCh1ToCh2) { - heatingCh2Enabled = heatingEnabled; + vars.master.ch2.targetTemp = vars.master.heating.setpointTemp; } else if (settings.opentherm.dhwToCh2) { - heatingCh2Enabled = settings.opentherm.dhwPresent && settings.dhw.enable; + vars.master.ch2.targetTemp = vars.master.dhw.targetTemp; } // Set boiler status LB @@ -159,11 +179,11 @@ class OpenThermTask : public Task { } unsigned long response = this->instance->setBoilerStatus( - heatingEnabled, - settings.opentherm.dhwPresent && settings.dhw.enable, + vars.master.heating.enabled, + vars.master.dhw.enabled, false, settings.opentherm.nativeHeatingControl, - heatingCh2Enabled, + vars.master.ch2.enabled, settings.opentherm.summerWinterMode, settings.opentherm.dhwBlocking, statusLb @@ -177,108 +197,138 @@ class OpenThermTask : public Task { ); } - if (!vars.states.otStatus && millis() - this->lastSuccessResponse < 1150) { + if (!vars.slave.connected && millis() - this->lastSuccessResponse < 1150) { Log.sinfoln(FPSTR(L_OT), F("Connected")); - vars.states.otStatus = true; + vars.slave.connected = true; - } else if (vars.states.otStatus && millis() - this->lastSuccessResponse > 1150) { + } else if (vars.slave.connected && millis() - this->lastSuccessResponse > 1150) { Log.swarningln(FPSTR(L_OT), F("Disconnected")); - if (settings.sensors.outdoor.type == SensorType::BOILER_OUTDOOR) { - vars.sensors.outdoor.connected = false; - } - - if (settings.sensors.indoor.type == SensorType::BOILER_RETURN) { - vars.sensors.indoor.connected = false; - } - - vars.states.otStatus = false; - this->isInitialized = false; + // Mark sensors as disconnected + Sensors::setConnectionStatusByType(Sensors::Type::OT_OUTDOOR_TEMP, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_HEATING_TEMP, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_HEATING_RETURN_TEMP, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_DHW_TEMP, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_DHW_TEMP2, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_DHW_FLOW_RATE, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_CH2_TEMP, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_EXHAUST_TEMP, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_HEAT_EXCHANGER_TEMP, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_PRESSURE, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_MODULATION_LEVEL, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_CURRENT_POWER, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_EXHAUST_CO2, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_EXHAUST_FAN_SPEED, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_SUPPLY_FAN_SPEED, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_SOLAR_STORAGE_TEMP, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_SOLAR_COLLECTOR_TEMP, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_FAN_SPEED_SETPOINT, false); + Sensors::setConnectionStatusByType(Sensors::Type::OT_FAN_SPEED_CURRENT, false); + + this->initialized = false; + vars.slave.connected = false; } // If boiler is disconnected, no need try setting other OT stuff - if (!vars.states.otStatus) { - vars.states.heating = false; - vars.states.dhw = false; - vars.states.flame = false; - vars.states.fault = false; - vars.states.diagnostic = false; + if (!vars.slave.connected) { + vars.slave.heating.enabled = false; + vars.slave.heating.active = false; + vars.slave.dhw.enabled = false; + vars.slave.dhw.active = false; + vars.slave.flame = false; + vars.slave.fault.active = false; + vars.slave.fault.code = 0; + vars.slave.diag.active = false; + vars.slave.diag.code = 0; return; } - if (!this->isInitialized) { + if (!this->initialized) { Log.sinfoln(FPSTR(L_OT), F("Initializing...")); - this->isInitialized = true; + this->initialized = true; this->initializedTime = millis(); - this->initializedMemberIdCode = settings.opentherm.memberIdCode; this->initialize(); } - if (vars.parameters.heatingEnabled != heatingEnabled) { + if (vars.master.heating.enabled != vars.slave.heating.enabled) { + this->prevUpdateNonEssentialVars = 0; + vars.slave.heating.enabled = vars.master.heating.enabled; + Log.sinfoln(FPSTR(L_OT_HEATING), vars.master.heating.enabled ? F("Enabled") : F("Disabled")); + } + + if (vars.master.dhw.enabled != vars.slave.dhw.enabled) { this->prevUpdateNonEssentialVars = 0; - vars.parameters.heatingEnabled = heatingEnabled; - Log.sinfoln(FPSTR(L_OT_HEATING), "%s", heatingEnabled ? F("Enabled") : F("Disabled")); + vars.slave.dhw.enabled = vars.master.dhw.enabled; + Log.sinfoln(FPSTR(L_OT_DHW), vars.master.dhw.enabled ? F("Enabled") : F("Disabled")); } - vars.states.heating = CustomOpenTherm::isCentralHeatingActive(response); - vars.states.dhw = settings.opentherm.dhwPresent ? CustomOpenTherm::isHotWaterActive(response) : false; - vars.states.flame = CustomOpenTherm::isFlameOn(response); - vars.states.fault = CustomOpenTherm::isFault(response); - vars.states.diagnostic = CustomOpenTherm::isDiagnostic(response); + vars.slave.heating.active = CustomOpenTherm::isCentralHeatingActive(response); + vars.slave.dhw.active = settings.opentherm.dhwPresent ? CustomOpenTherm::isHotWaterActive(response) : false; + vars.slave.flame = CustomOpenTherm::isFlameOn(response); + vars.slave.fault.active = CustomOpenTherm::isFault(response); + vars.slave.diag.active = CustomOpenTherm::isDiagnostic(response); Log.snoticeln( - FPSTR(L_OT), - F("Received boiler status. Heating: %hhu; DHW: %hhu; flame: %hhu; fault: %hhu; diag: %hhu"), - vars.states.heating, vars.states.dhw, vars.states.flame, vars.states.fault, vars.states.diagnostic + FPSTR(L_OT), F("Received boiler status. Heating: %hhu; DHW: %hhu; flame: %hhu; fault: %hhu; diag: %hhu"), + vars.slave.heating.active, vars.slave.dhw.active, + vars.slave.flame, vars.slave.fault.active, vars.slave.diag.active ); // These parameters will be updated every minute if (millis() - this->prevUpdateNonEssentialVars > 60000) { if (this->updateMinModulationLevel()) { Log.snoticeln( - FPSTR(L_OT), - F("Received min modulation: %hhu%%, max power: %hhu kW"), - vars.parameters.minModulation, - vars.parameters.maxPower + FPSTR(L_OT), F("Received min modulation: %hhu%%, max power: %.2f kW"), + vars.slave.modulation.min, vars.slave.power.max ); - if (settings.opentherm.maxModulation < vars.parameters.minModulation) { - settings.opentherm.maxModulation = vars.parameters.minModulation; + if (settings.opentherm.maxModulation < vars.slave.modulation.min) { + settings.opentherm.maxModulation = vars.slave.modulation.min; fsSettings.update(); - Log.swarningln(FPSTR(L_SETTINGS_OT), F("Updated min modulation: %hhu%%"), settings.opentherm.maxModulation); - } - if (fabsf(settings.opentherm.maxPower) < 0.1f && vars.parameters.maxPower > 0) { - settings.opentherm.maxPower = vars.parameters.maxPower; + Log.swarningln( + FPSTR(L_SETTINGS_OT), F("Updated min modulation: %hhu%%"), + settings.opentherm.maxModulation + ); + } - if (vars.parameters.minModulation > 0) { - settings.opentherm.minPower = (vars.parameters.minModulation / 100.0f) * vars.parameters.maxPower; - } + if (fabsf(settings.opentherm.maxPower) < 0.1f && vars.slave.power.max > 0.1f) { + settings.opentherm.maxPower = vars.slave.power.max; + settings.opentherm.minPower = vars.slave.power.min; fsSettings.update(); - Log.swarningln(FPSTR(L_SETTINGS_OT), F("Updated max power: %.2f kW"), settings.opentherm.maxPower); + Log.swarningln( + FPSTR(L_SETTINGS_OT), F("Updated power, min: %.2f kW, max: %.2f kW"), + settings.opentherm.minPower, settings.opentherm.maxPower + ); } } else { Log.swarningln(FPSTR(L_OT), F("Failed receive min modulation and max power")); } - if (!heatingEnabled && settings.opentherm.modulationSyncWithHeating) { + if (!vars.master.heating.enabled && settings.opentherm.modulationSyncWithHeating) { if (this->setMaxModulationLevel(0)) { - Log.snoticeln(FPSTR(L_OT), F("Set max modulation: 0% (off)")); + Log.snoticeln(FPSTR(L_OT), F("Set max modulation: 0% (response: %hhu%%)"), vars.slave.modulation.max); } else { - Log.swarningln(FPSTR(L_OT), F("Failed set max modulation: 0% (off)")); + Log.swarningln(FPSTR(L_OT), F("Failed set max modulation: 0% (response: %hhu%%)"), vars.slave.modulation.max); } } else { if (this->setMaxModulationLevel(settings.opentherm.maxModulation)) { - Log.snoticeln(FPSTR(L_OT), F("Set max modulation: %hhu%%"), settings.opentherm.maxModulation); + Log.snoticeln( + FPSTR(L_OT), F("Set max modulation: %hhu%% (response: %hhu%%)"), + settings.opentherm.maxModulation, vars.slave.modulation.max + ); } else { - Log.swarningln(FPSTR(L_OT), F("Failed set max modulation: %hhu%%"), settings.opentherm.maxModulation); + Log.swarningln( + FPSTR(L_OT), F("Failed set max modulation: %hhu%% (response: %hhu%%)"), + settings.opentherm.maxModulation, vars.slave.modulation.max + ); } } @@ -286,270 +336,560 @@ class OpenThermTask : public Task { // Get DHW min/max temp (if necessary) if (settings.opentherm.dhwPresent && settings.opentherm.getMinMaxTemp) { if (this->updateMinMaxDhwTemp()) { + uint8_t convertedMinTemp = convertTemp( + vars.slave.dhw.minTemp, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + + uint8_t convertedMaxTemp = convertTemp( + vars.slave.dhw.maxTemp, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + Log.snoticeln( - FPSTR(L_OT_DHW), - F("Received min temp: %hhu, max temp: %hhu"), - vars.parameters.dhwMinTemp, - vars.parameters.dhwMaxTemp + FPSTR(L_OT_DHW), F("Received min temp: %hhu (converted: %hhu), max temp: %hhu (converted: %hhu)"), + vars.slave.dhw.minTemp, convertedMinTemp, vars.slave.dhw.maxTemp, convertedMaxTemp ); - if (settings.dhw.minTemp < vars.parameters.dhwMinTemp) { - settings.dhw.minTemp = vars.parameters.dhwMinTemp; + if (settings.dhw.minTemp < convertedMinTemp) { + settings.dhw.minTemp = convertedMinTemp; fsSettings.update(); + Log.swarningln(FPSTR(L_SETTINGS_DHW), F("Updated min temp: %hhu"), settings.dhw.minTemp); } - if (settings.dhw.maxTemp > vars.parameters.dhwMaxTemp) { - settings.dhw.maxTemp = vars.parameters.dhwMaxTemp; + if (settings.dhw.maxTemp > convertedMaxTemp) { + settings.dhw.maxTemp = convertedMaxTemp; fsSettings.update(); + Log.swarningln(FPSTR(L_SETTINGS_DHW), F("Updated max temp: %hhu"), settings.dhw.maxTemp); } } else { - vars.parameters.dhwMinTemp = convertTemp(DEFAULT_DHW_MIN_TEMP, UnitSystem::METRIC, settings.system.unitSystem); - vars.parameters.dhwMaxTemp = convertTemp(DEFAULT_DHW_MAX_TEMP, UnitSystem::METRIC, settings.system.unitSystem); - Log.swarningln(FPSTR(L_OT_DHW), F("Failed receive min/max temp")); } + } - if (settings.dhw.minTemp >= settings.dhw.maxTemp) { - settings.dhw.minTemp = vars.parameters.dhwMinTemp; - settings.dhw.maxTemp = vars.parameters.dhwMaxTemp; - fsSettings.update(); - } + if (settings.dhw.minTemp >= settings.dhw.maxTemp) { + settings.dhw.minTemp = convertTemp(DEFAULT_DHW_MIN_TEMP, UnitSystem::METRIC, settings.system.unitSystem); + settings.dhw.maxTemp = convertTemp(DEFAULT_DHW_MAX_TEMP, UnitSystem::METRIC, settings.system.unitSystem); + fsSettings.update(); } // Get heating min/max temp if (settings.opentherm.getMinMaxTemp) { if (this->updateMinMaxHeatingTemp()) { + uint8_t convertedMinTemp = convertTemp( + vars.slave.heating.minTemp, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + + uint8_t convertedMaxTemp = convertTemp( + vars.slave.heating.maxTemp, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + Log.snoticeln( - FPSTR(L_OT_HEATING), - F("Received min temp: %hhu, max temp: %hhu"), - vars.parameters.heatingMinTemp, - vars.parameters.heatingMaxTemp + FPSTR(L_OT_HEATING), F("Received min temp: %hhu (converted: %hhu), max temp: %hhu (converted: %hhu)"), + vars.slave.heating.minTemp, convertedMinTemp, vars.slave.heating.maxTemp, convertedMaxTemp ); - if (settings.heating.minTemp < vars.parameters.heatingMinTemp) { - settings.heating.minTemp = vars.parameters.heatingMinTemp; + if (settings.heating.minTemp < convertedMinTemp) { + settings.heating.minTemp = convertedMinTemp; fsSettings.update(); + Log.swarningln(FPSTR(L_SETTINGS_HEATING), F("Updated min temp: %hhu"), settings.heating.minTemp); } - if (settings.heating.maxTemp > vars.parameters.heatingMaxTemp) { - settings.heating.maxTemp = vars.parameters.heatingMaxTemp; + if (settings.heating.maxTemp > convertedMaxTemp) { + settings.heating.maxTemp = convertedMaxTemp; fsSettings.update(); + Log.swarningln(FPSTR(L_SETTINGS_HEATING), F("Updated max temp: %hhu"), settings.heating.maxTemp); } } else { - vars.parameters.heatingMinTemp = convertTemp(DEFAULT_HEATING_MIN_TEMP, UnitSystem::METRIC, settings.system.unitSystem); - vars.parameters.heatingMaxTemp = convertTemp(DEFAULT_HEATING_MAX_TEMP, UnitSystem::METRIC, settings.system.unitSystem); - Log.swarningln(FPSTR(L_OT_HEATING), F("Failed receive min/max temp")); } } if (settings.heating.minTemp >= settings.heating.maxTemp) { - settings.heating.minTemp = vars.parameters.heatingMinTemp; - settings.heating.maxTemp = vars.parameters.heatingMaxTemp; + settings.heating.minTemp = convertTemp(DEFAULT_HEATING_MIN_TEMP, UnitSystem::METRIC, settings.system.unitSystem);; + settings.heating.maxTemp = convertTemp(DEFAULT_HEATING_MAX_TEMP, UnitSystem::METRIC, settings.system.unitSystem);; fsSettings.update(); } // Get fault code (if necessary) - if (vars.states.fault) { + if (vars.slave.fault.active) { if (this->updateFaultCode()) { Log.snoticeln( - FPSTR(L_OT), - F("Received fault code: %hhu (0x%02X)"), - vars.sensors.faultCode, - vars.sensors.faultCode + FPSTR(L_OT), F("Received fault code: %hhu (0x%02X)"), + vars.slave.fault.code, vars.slave.fault.code ); } else { - vars.sensors.faultCode = 0; - Log.swarningln(FPSTR(L_OT), F("Failed receive fault code")); } - } else if (vars.sensors.faultCode != 0) { - vars.sensors.faultCode = 0; + } else if (vars.slave.fault.code != 0) { + vars.slave.fault.code = 0; } // Get diagnostic code (if necessary) - if (vars.states.fault || vars.states.diagnostic) { + if (vars.slave.fault.active || vars.slave.diag.active) { if (this->updateDiagCode()) { Log.snoticeln( - FPSTR(L_OT), - F("Received diag code: %hu (0x%02X)"), - vars.sensors.diagnosticCode, - vars.sensors.diagnosticCode + FPSTR(L_OT), F("Received diag code: %hu (0x%02X)"), + vars.slave.diag.code, vars.slave.diag.code ); } else { - vars.sensors.diagnosticCode = 0; - Log.swarningln(FPSTR(L_OT), F("Failed receive diag code")); } - } else if (vars.sensors.diagnosticCode != 0) { - vars.sensors.diagnosticCode = 0; + } else if (vars.slave.diag.code != 0) { + vars.slave.diag.code = 0; } - // If filtering is disabled, then it is enough to - // update these parameters once a minute - if (!settings.opentherm.filterNumValues.enable) { - // Get outdoor temp (if necessary) - if (settings.sensors.outdoor.type == SensorType::BOILER_OUTDOOR) { - if (this->updateOutdoorTemp()) { - if (!vars.sensors.outdoor.connected) { - vars.sensors.outdoor.connected = true; - } + this->prevUpdateNonEssentialVars = millis(); + } - Log.snoticeln(FPSTR(L_OT), F("Received outdoor temp: %.2f"), vars.temperatures.outdoor); - } else { - if (vars.sensors.outdoor.connected) { - vars.sensors.outdoor.connected = false; - } + // Update modulation level + if ( + Sensors::getAmountByType(Sensors::Type::OT_MODULATION_LEVEL, true) || + Sensors::getAmountByType(Sensors::Type::OT_CURRENT_POWER, true) + ) { + if (vars.slave.flame) { + if (this->updateModulationLevel()) { + float power = 0.0f; + if (settings.opentherm.maxPower > 0.1f) { + power += settings.opentherm.minPower; - Log.swarningln(FPSTR(L_OT), F("Failed receive outdoor temp")); + if (vars.slave.modulation.current > 0) { + power += ( + settings.opentherm.maxPower - settings.opentherm.minPower + ) / 100.0f * vars.slave.modulation.current; + } } - } - - // Get pressure - if (this->updatePressure()) { - Log.snoticeln(FPSTR(L_OT), F("Received pressure: %.2f"), vars.sensors.pressure); + vars.slave.power.current = power; + + Log.snoticeln( + FPSTR(L_OT), F("Received modulation level: %hhu%%, power: %.2f of %.2f kW (min: %.2f kW)"), + vars.slave.modulation.current, vars.slave.power.current, + settings.opentherm.maxPower, settings.opentherm.minPower + ); + + // Modulation level sensors + Sensors::setValueByType( + Sensors::Type::OT_MODULATION_LEVEL, vars.slave.modulation.current, + Sensors::ValueType::PRIMARY, true, true + ); + + // Power sensors + Sensors::setValueByType( + Sensors::Type::OT_CURRENT_POWER, vars.slave.power.current, + Sensors::ValueType::PRIMARY, true, true + ); } else { - Log.swarningln(FPSTR(L_OT), F("Failed receive pressure")); + Log.swarningln(FPSTR(L_OT), F("Failed receive modulation level")); } - } - this->prevUpdateNonEssentialVars = millis(); - } + } else { + vars.slave.modulation.current = 0; + vars.slave.power.current = 0.0f; + // Modulation level sensors + Sensors::setValueByType( + Sensors::Type::OT_MODULATION_LEVEL, vars.slave.modulation.current, + Sensors::ValueType::PRIMARY, true, true + ); - // Get current modulation level (if necessary) - if (vars.states.flame) { - if (this->updateModulationLevel()) { - if (settings.opentherm.maxPower > 0.1f) { - float modulatedPower = settings.opentherm.maxPower - settings.opentherm.minPower; - vars.sensors.power = settings.opentherm.minPower + (modulatedPower / 100.0f * vars.sensors.modulation); + // Power sensors + Sensors::setValueByType( + Sensors::Type::OT_CURRENT_POWER, vars.slave.power.current, + Sensors::ValueType::PRIMARY, true, true + ); + } + } + + // Update DHW temp + if (settings.opentherm.dhwPresent && Sensors::getAmountByType(Sensors::Type::OT_DHW_TEMP, true)) { + bool result = this->updateDhwTemp(); + + if (result) { + float convertedDhwTemp = convertTemp( + vars.slave.dhw.currentTemp, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); - } else { - vars.sensors.power = 0.0f; - } - Log.snoticeln( - FPSTR(L_OT), - F("Received modulation level: %.2f%%, power: %.2f of %.2f kW (min: %.2f kW)"), - vars.sensors.modulation, - vars.sensors.power, - settings.opentherm.maxPower, - settings.opentherm.minPower + FPSTR(L_OT_DHW), F("Received temp: %.2f (converted: %.2f)"), + vars.slave.dhw.currentTemp, convertedDhwTemp + ); + + Sensors::setValueByType( + Sensors::Type::OT_DHW_TEMP, convertedDhwTemp, + Sensors::ValueType::PRIMARY, true, true ); } else { - Log.swarningln(FPSTR(L_OT), F("Failed receive modulation level")); + Log.swarningln(FPSTR(L_OT_DHW), F("Failed receive temp")); } - - } else { - vars.sensors.modulation = 0; - vars.sensors.power = 0; } - // Update DHW sensors (if necessary) - if (settings.opentherm.dhwPresent) { - if (this->updateDhwTemp()) { - Log.snoticeln(FPSTR(L_OT_DHW), F("Received temp: %.2f"), vars.temperatures.dhw); + // Update DHW temp 2 + if (settings.opentherm.dhwPresent && Sensors::getAmountByType(Sensors::Type::OT_DHW_TEMP2, true)) { + if (this->updateDhwTemp2()) { + float convertedDhwTemp2 = convertTemp( + vars.slave.dhw.currentTemp2, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + + Log.snoticeln( + FPSTR(L_OT_DHW), F("Received temp 2: %.2f (converted: %.2f)"), + vars.slave.dhw.currentTemp2, convertedDhwTemp2 + ); + + Sensors::setValueByType( + Sensors::Type::OT_DHW_TEMP2, convertedDhwTemp2, + Sensors::ValueType::PRIMARY, true, true + ); } else { - Log.swarningln(FPSTR(L_OT_DHW), F("Failed receive temp")); + Log.swarningln(FPSTR(L_OT_DHW), F("Failed receive temp 2")); } + } + // Update DHW flow rate + if (settings.opentherm.dhwPresent && Sensors::getAmountByType(Sensors::Type::OT_DHW_FLOW_RATE, true)) { if (this->updateDhwFlowRate()) { - Log.snoticeln(FPSTR(L_OT_DHW), F("Received flow rate: %.2f"), vars.sensors.dhwFlowRate); + float convertedDhwFlowRate = convertVolume( + vars.slave.dhw.flowRate, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + + Log.snoticeln( + FPSTR(L_OT_DHW), F("Received flow rate: %.2f (converted: %.2f)"), + vars.slave.dhw.flowRate, convertedDhwFlowRate + ); + + Sensors::setValueByType( + Sensors::Type::OT_DHW_FLOW_RATE, convertedDhwFlowRate, + Sensors::ValueType::PRIMARY, true, true + ); } else { Log.swarningln(FPSTR(L_OT_DHW), F("Failed receive flow rate")); } + } - } else { - vars.temperatures.dhw = 0.0f; - vars.sensors.dhwFlowRate = 0.0f; + // Update heating temp + if (Sensors::getAmountByType(Sensors::Type::OT_HEATING_TEMP, true)) { + if (this->updateHeatingTemp()) { + float convertedHeatingTemp = convertTemp( + vars.slave.heating.currentTemp, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + + Log.snoticeln( + FPSTR(L_OT_HEATING), F("Received temp: %.2f"), + vars.slave.heating.currentTemp, convertedHeatingTemp + ); + + Sensors::setValueByType( + Sensors::Type::OT_HEATING_TEMP, convertedHeatingTemp, + Sensors::ValueType::PRIMARY, true, true + ); + + } else { + Log.swarningln(FPSTR(L_OT_HEATING), F("Failed receive temp")); + } } - // Get current heating temp - if (this->updateHeatingTemp()) { - Log.snoticeln(FPSTR(L_OT_HEATING), F("Received temp: %.2f"), vars.temperatures.heating); + // Update heating return temp + if (Sensors::getAmountByType(Sensors::Type::OT_HEATING_RETURN_TEMP, true)) { + if (this->updateHeatingReturnTemp()) { + float convertedHeatingReturnTemp = convertTemp( + vars.slave.heating.returnTemp, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); - } else { - Log.swarningln(FPSTR(L_OT_HEATING), F("Failed receive temp")); + Log.snoticeln( + FPSTR(L_OT_HEATING), F("Received return temp: %.2f (converted: %.2f)"), + vars.slave.heating.returnTemp, convertedHeatingReturnTemp + ); + + Sensors::setValueByType( + Sensors::Type::OT_HEATING_RETURN_TEMP, convertedHeatingReturnTemp, + Sensors::ValueType::PRIMARY, true, true + ); + + } else { + Log.swarningln(FPSTR(L_OT_HEATING), F("Failed receive return temp")); + } } - // Get heating return temp - if (this->updateHeatingReturnTemp()) { - if (settings.sensors.indoor.type == SensorType::BOILER_RETURN) { - vars.temperatures.indoor = settings.sensors.outdoor.offset + vars.temperatures.heatingReturn; + // Update CH2 temp + if (Sensors::getAmountByType(Sensors::Type::OT_CH2_TEMP, true)) { + if (vars.master.ch2.enabled && !settings.opentherm.nativeHeatingControl) { + if (this->updateCh2Temp()) { + float convertedCh2Temp = convertTemp( + vars.slave.ch2.currentTemp, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + + Log.snoticeln( + FPSTR(L_OT_CH2), F("Received temp: %.2f (converted: %.2f)"), + vars.slave.ch2.currentTemp, convertedCh2Temp + ); + + Sensors::setValueByType( + Sensors::Type::OT_CH2_TEMP, convertedCh2Temp, + Sensors::ValueType::PRIMARY, true, true + ); - if (!vars.sensors.outdoor.connected) { - vars.sensors.indoor.connected = true; + } else { + Log.swarningln(FPSTR(L_OT_CH2), F("Failed receive temp")); } } + } - Log.snoticeln(FPSTR(L_OT_HEATING), F("Received return temp: %.2f"), vars.temperatures.heatingReturn); + // Update exhaust temp + if (Sensors::getAmountByType(Sensors::Type::OT_EXHAUST_TEMP, true)) { + if (this->updateExhaustTemp()) { + float convertedExhaustTemp = convertTemp( + vars.slave.exhaust.temp, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); - } else { - if (settings.sensors.indoor.type == SensorType::BOILER_RETURN && vars.sensors.outdoor.connected) { - vars.sensors.indoor.connected = false; + Log.snoticeln( + FPSTR(L_OT), F("Received exhaust temp: %.2f (converted: %.2f)"), + vars.slave.exhaust.temp, convertedExhaustTemp + ); + + Sensors::setValueByType( + Sensors::Type::OT_EXHAUST_TEMP, convertedExhaustTemp, + Sensors::ValueType::PRIMARY, true, true + ); + + } else { + Log.swarningln(FPSTR(L_OT), F("Failed receive exhaust temp")); } + } + + // Update heat exchanger temp + if (Sensors::getAmountByType(Sensors::Type::OT_HEAT_EXCHANGER_TEMP, true)) { + if (this->updateHeatExchangerTemp()) { + float convertedHeatExchTemp = convertTemp( + vars.slave.heatExchangerTemp, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + + Log.snoticeln( + FPSTR(L_OT), F("Received heat exchanger temp: %.2f (converted: %.2f)"), + vars.slave.heatExchangerTemp, convertedHeatExchTemp + ); + + Sensors::setValueByType( + Sensors::Type::OT_HEAT_EXCHANGER_TEMP, convertedHeatExchTemp, + Sensors::ValueType::PRIMARY, true, true + ); - Log.swarningln(FPSTR(L_OT_HEATING), F("Failed receive return temp")); + } else { + Log.swarningln(FPSTR(L_OT), F("Failed receive heat exchanger temp")); + } } - // Get exhaust temp - if (this->updateExhaustTemp()) { - Log.snoticeln(FPSTR(L_OT), F("Received exhaust temp: %.2f"), vars.temperatures.exhaust); + // Update outdoor temp + if (Sensors::getAmountByType(Sensors::Type::OT_OUTDOOR_TEMP, true)) { + if (this->updateOutdoorTemp()) { + float convertedOutdoorTemp = convertTemp( + vars.slave.heating.outdoorTemp, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + + Log.snoticeln( + FPSTR(L_OT), F("Received outdoor temp: %.2f (converted: %.2f)"), + vars.slave.heating.outdoorTemp, convertedOutdoorTemp + ); - } else { - Log.swarningln(FPSTR(L_OT), F("Failed receive exhaust temp")); + Sensors::setValueByType( + Sensors::Type::OT_OUTDOOR_TEMP, convertedOutdoorTemp, + Sensors::ValueType::PRIMARY, true, true + ); + + } else { + Log.swarningln(FPSTR(L_OT), F("Failed receive outdoor temp")); + } } + + // Update solar storage temp + if (Sensors::getAmountByType(Sensors::Type::OT_SOLAR_STORAGE_TEMP, true)) { + if (this->updateSolarStorageTemp()) { + float convertedSolarStorageTemp = convertTemp( + vars.slave.solar.storage, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); - // If filtering is enabled, these parameters - // must be updated every time. - if (settings.opentherm.filterNumValues.enable) { - // Get outdoor temp (if necessary) - if (settings.sensors.outdoor.type == SensorType::BOILER_OUTDOOR) { - if (this->updateOutdoorTemp()) { - if (!vars.sensors.outdoor.connected) { - vars.sensors.outdoor.connected = true; - } + Log.snoticeln( + FPSTR(L_OT), F("Received solar storage temp: %.2f (converted: %.2f)"), + vars.slave.solar.storage, convertedSolarStorageTemp + ); - Log.snoticeln(FPSTR(L_OT), F("Received outdoor temp: %.2f"), vars.temperatures.outdoor); + Sensors::setValueByType( + Sensors::Type::OT_SOLAR_STORAGE_TEMP, convertedSolarStorageTemp, + Sensors::ValueType::PRIMARY, true, true + ); - } else { - if (vars.sensors.outdoor.connected) { - vars.sensors.outdoor.connected = false; - } + } else { + Log.swarningln(FPSTR(L_OT), F("Failed receive solar storage temp")); + } + } - Log.swarningln(FPSTR(L_OT), F("Failed receive outdoor temp")); - } + // Update solar collector temp + if (Sensors::getAmountByType(Sensors::Type::OT_SOLAR_COLLECTOR_TEMP, true)) { + if (this->updateSolarCollectorTemp()) { + float convertedSolarCollectorTemp = convertTemp( + vars.slave.solar.collector, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + + Log.snoticeln( + FPSTR(L_OT), F("Received solar collector temp: %.2f (converted: %.2f)"), + vars.slave.solar.collector, convertedSolarCollectorTemp + ); + + Sensors::setValueByType( + Sensors::Type::OT_SOLAR_COLLECTOR_TEMP, convertedSolarCollectorTemp, + Sensors::ValueType::PRIMARY, true, true + ); + + } else { + Log.swarningln(FPSTR(L_OT), F("Failed receive solar collector temp")); } - - // Get pressure + } + + // Update fan speed + if ( + Sensors::getAmountByType(Sensors::Type::OT_FAN_SPEED_SETPOINT, true) || + Sensors::getAmountByType(Sensors::Type::OT_FAN_SPEED_CURRENT, true) + ) { + if (this->updateFanSpeed()) { + Log.snoticeln( + FPSTR(L_OT), F("Received fan speed, setpoint: %hhu%%, current: %hhu%%"), + vars.slave.fanSpeed.setpoint, vars.slave.fanSpeed.current + ); + + Sensors::setValueByType( + Sensors::Type::OT_FAN_SPEED_SETPOINT, vars.slave.fanSpeed.setpoint, + Sensors::ValueType::PRIMARY, true, true + ); + Sensors::setValueByType( + Sensors::Type::OT_FAN_SPEED_CURRENT, vars.slave.fanSpeed.current, + Sensors::ValueType::PRIMARY, true, true + ); + } + } + + // Update pressure + if (Sensors::getAmountByType(Sensors::Type::OT_PRESSURE, true)) { if (this->updatePressure()) { - Log.snoticeln(FPSTR(L_OT), F("Received pressure: %.2f"), vars.sensors.pressure); + float convertedPressure = convertPressure( + vars.slave.pressure, + settings.opentherm.unitSystem, + settings.system.unitSystem + ); + + Log.snoticeln( + FPSTR(L_OT), F("Received pressure: %.2f (converted: %.2f)"), + vars.slave.pressure, convertedPressure + ); + + Sensors::setValueByType( + Sensors::Type::OT_PRESSURE, convertedPressure, + Sensors::ValueType::PRIMARY, true, true + ); } else { Log.swarningln(FPSTR(L_OT), F("Failed receive pressure")); } } + // Update exhaust CO2 + if (Sensors::getAmountByType(Sensors::Type::OT_EXHAUST_CO2, true)) { + if (this->updateExhaustCo2()) { + Log.snoticeln( + FPSTR(L_OT), F("Received exhaust CO2: %hu ppm"), + vars.slave.exhaust.co2 + ); + + Sensors::setValueByType( + Sensors::Type::OT_EXHAUST_CO2, vars.slave.exhaust.co2, + Sensors::ValueType::PRIMARY, true, true + ); + + } else { + Log.swarningln(FPSTR(L_OT), F("Failed receive exhaust CO2")); + } + } + + // Update exhaust fan speed + if (Sensors::getAmountByType(Sensors::Type::OT_EXHAUST_FAN_SPEED, true)) { + if (this->updateExhaustFanSpeed()) { + Log.snoticeln( + FPSTR(L_OT), F("Received exhaust fan speed: %hu rpm"), + vars.slave.exhaust.fanSpeed + ); + + Sensors::setValueByType( + Sensors::Type::OT_EXHAUST_FAN_SPEED, vars.slave.exhaust.fanSpeed, + Sensors::ValueType::PRIMARY, true, true + ); + + } else { + Log.swarningln(FPSTR(L_OT), F("Failed receive exhaust fan speed")); + } + } + + // Update supply fan speed + if (Sensors::getAmountByType(Sensors::Type::OT_SUPPLY_FAN_SPEED, true)) { + if (this->updateSupplyFanSpeed()) { + Log.snoticeln( + FPSTR(L_OT), F("Received supply fan speed: %hu rpm"), + vars.slave.fanSpeed.supply + ); + + Sensors::setValueByType( + Sensors::Type::OT_SUPPLY_FAN_SPEED, vars.slave.fanSpeed.supply, + Sensors::ValueType::PRIMARY, true, true + ); + + } else { + Log.swarningln(FPSTR(L_OT), F("Failed receive supply fan speed")); + } + } // Fault reset action if (vars.actions.resetFault) { - if (vars.states.fault) { + if (vars.slave.fault.active) { if (this->instance->sendBoilerReset()) { Log.sinfoln(FPSTR(L_OT), F("Boiler fault reset successfully")); @@ -563,7 +903,7 @@ class OpenThermTask : public Task { // Diag reset action if (vars.actions.resetDiagnostic) { - if (vars.states.diagnostic) { + if (vars.slave.diag.active) { if (this->instance->sendServiceReset()) { Log.sinfoln(FPSTR(L_OT), F("Boiler diagnostic reset successfully")); @@ -577,158 +917,204 @@ class OpenThermTask : public Task { // Update DHW temp - if (settings.opentherm.dhwPresent && settings.dhw.enable && (this->needSetDhwTemp() || fabs(settings.dhw.target - currentDhwTemp) > 0.0001f)) { - float convertedTemp = convertTemp(settings.dhw.target, settings.system.unitSystem, settings.opentherm.unitSystem); - Log.sinfoln(FPSTR(L_OT_DHW), F("Set temp: %.2f (converted: %.2f)"), settings.dhw.target, convertedTemp); + if (vars.master.dhw.enabled) { + // Converted target dhw temp + float convertedTemp = convertTemp( + vars.master.dhw.targetTemp, + settings.system.unitSystem, + settings.opentherm.unitSystem + ); // Set DHW temp - if (this->instance->setDhwTemp(convertedTemp)) { - currentDhwTemp = settings.dhw.target; - this->dhwSetTempTime = millis(); + if (this->needSetDhwTemp(convertedTemp)) { + if (this->setDhwTemp(convertedTemp)) { + this->dhwSetTempTime = millis(); - } else { - Log.swarningln(FPSTR(L_OT_DHW), F("Failed set temp")); - } + Log.sinfoln( + FPSTR(L_OT_DHW), F("Set temp: %.2f (converted: %.2f, response: %.2f)"), + vars.master.dhw.targetTemp, convertedTemp, vars.slave.dhw.targetTemp + ); - // Set DHW temp to CH2 - if (settings.opentherm.dhwToCh2) { - if (!this->instance->setHeatingCh2Temp(convertedTemp)) { - Log.swarningln(FPSTR(L_OT_DHW), F("Failed set CH2 temp")); + } else { + Log.swarningln(FPSTR(L_OT_DHW), F("Failed set temp")); } } } - // Native heating control if (settings.opentherm.nativeHeatingControl) { - // Set current indoor temp - float indoorTemp = 0.0f; - float convertedTemp = 0.0f; + // Converted current indoor temp + float convertedTemp = convertTemp(vars.master.heating.indoorTemp, settings.system.unitSystem, settings.opentherm.unitSystem); - if (vars.sensors.indoor.connected) { - indoorTemp = vars.temperatures.indoor; - convertedTemp = convertTemp(indoorTemp, settings.system.unitSystem, settings.opentherm.unitSystem); + // Set current indoor temp + if (this->setRoomTemp(convertedTemp)) { + Log.sinfoln( + FPSTR(L_OT_HEATING), F("Set current indoor temp: %.2f (converted: %.2f, response: %.2f)"), + vars.master.heating.indoorTemp, convertedTemp, vars.slave.heating.indoorTemp + ); + + } else { + Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set current indoor temp")); } - Log.sinfoln(FPSTR(L_OT_HEATING), F("Set current indoor temp: %.2f (converted: %.2f)"), indoorTemp, convertedTemp); - if (!this->instance->setRoomTemp(convertedTemp)) { - Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set current indoor temp")); + // Set current CH2 indoor temp + if (settings.opentherm.heatingCh1ToCh2) { + if (this->setRoomTempCh2(convertedTemp)) { + Log.sinfoln( + FPSTR(L_OT_HEATING), F("Set current CH2 indoor temp: %.2f (converted: %.2f, response: %.2f)"), + vars.master.heating.indoorTemp, convertedTemp, vars.slave.ch2.indoorTemp + ); + + } else { + Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set current CH2 indoor temp")); + } } - // Set target indoor temp - if (this->needSetHeatingTemp() || fabs(vars.parameters.heatingSetpoint - currentHeatingTemp) > 0.0001f) { - convertedTemp = convertTemp(vars.parameters.heatingSetpoint, settings.system.unitSystem, settings.opentherm.unitSystem); - Log.sinfoln(FPSTR(L_OT_HEATING), F("Set target indoor temp: %.2f (converted: %.2f)"), vars.parameters.heatingSetpoint, convertedTemp); - if (this->instance->setRoomSetpoint(convertedTemp)) { - currentHeatingTemp = vars.parameters.heatingSetpoint; + // Converted target indoor temp + convertedTemp = convertTemp(vars.master.heating.targetTemp, settings.system.unitSystem, settings.opentherm.unitSystem); + + // Set target indoor temp + if (this->needSetHeatingTemp(convertedTemp)) { + if (this->setRoomSetpoint(convertedTemp)) { this->heatingSetTempTime = millis(); + Log.sinfoln( + FPSTR(L_OT_HEATING), F("Set target indoor temp: %.2f (converted: %.2f, response: %.2f)"), + vars.master.heating.targetTemp, convertedTemp, vars.slave.heating.targetTemp + ); + } else { Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set target indoor temp")); } + } - // Set target temp to CH2 - if (settings.opentherm.heatingCh1ToCh2) { - if (!this->instance->setRoomSetpointCh2(convertedTemp)) { - Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set target indoor temp to CH2")); - } + // Set target CH2 temp + if (settings.opentherm.heatingCh1ToCh2 && this->needSetCh2Temp(convertedTemp)) { + if (this->setRoomSetpointCh2(convertedTemp)) { + this->ch2SetTempTime = millis(); + + Log.sinfoln( + FPSTR(L_OT_HEATING), F("Set target CH2 indoor temp: %.2f (converted: %.2f, response: %.2f)"), + vars.master.heating.targetTemp, convertedTemp, vars.slave.ch2.targetTemp + ); + + } else { + Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set target CH2 indoor temp")); } } + } - } else { - // Update heating temp - if (heatingEnabled && (this->needSetHeatingTemp() || fabs(vars.parameters.heatingSetpoint - currentHeatingTemp) > 0.0001f)) { - float convertedTemp = convertTemp(vars.parameters.heatingSetpoint, settings.system.unitSystem, settings.opentherm.unitSystem); - Log.sinfoln(FPSTR(L_OT_HEATING), F("Set temp: %.2f (converted: %.2f)"), vars.parameters.heatingSetpoint, convertedTemp); + // Normal heating control + if (!settings.opentherm.nativeHeatingControl && vars.master.heating.enabled) { + // Converted target heating temp + float convertedTemp = convertTemp(vars.master.heating.setpointTemp, settings.system.unitSystem, settings.opentherm.unitSystem); + if (this->needSetHeatingTemp(convertedTemp)) { // Set max heating temp if (this->setMaxHeatingTemp(convertedTemp)) { - currentHeatingTemp = vars.parameters.heatingSetpoint; - this->heatingSetTempTime = millis(); + Log.sinfoln( + FPSTR(L_OT_HEATING), F("Set max heating temp: %.2f (converted: %.2f)"), + vars.master.heating.setpointTemp, convertedTemp + ); } else { Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set max heating temp")); } - // Set heating temp - if (this->instance->setHeatingCh1Temp(convertedTemp)) { - currentHeatingTemp = vars.parameters.heatingSetpoint; + // Set target heating temp + if (this->setHeatingTemp(convertedTemp)) { this->heatingSetTempTime = millis(); - } else { - Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set CH1 temp")); - } + Log.sinfoln( + FPSTR(L_OT_HEATING), F("Set target temp: %.2f (converted: %.2f, response: %.2f)"), + vars.master.heating.setpointTemp, convertedTemp, vars.slave.heating.targetTemp + ); - // Set heating temp to CH2 - if (settings.opentherm.heatingCh1ToCh2) { - if (!this->instance->setHeatingCh2Temp(convertedTemp)) { - Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set CH2 temp")); - } + } else { + Log.swarningln(FPSTR(L_OT_HEATING), F("Failed set target temp")); } } } - // Hysteresis - // Only if enabled PID or/and Equitherm or Native heating control via OT - bool useHyst = false; - if (settings.heating.hysteresis > 0.01f && vars.sensors.indoor.connected) { - useHyst = settings.equitherm.enable || settings.pid.enable || settings.opentherm.nativeHeatingControl; - } + // Set CH2 temp + if (!settings.opentherm.nativeHeatingControl && vars.master.ch2.enabled) { + if (settings.opentherm.heatingCh1ToCh2 || settings.opentherm.dhwToCh2) { + // Converted target CH2 temp + float convertedTemp = convertTemp( + vars.master.ch2.targetTemp, + settings.system.unitSystem, + settings.opentherm.unitSystem + ); - if (useHyst) { - if (!this->heatingBlocking && vars.temperatures.indoor - settings.heating.target + 0.0001f >= settings.heating.hysteresis) { - this->heatingBlocking = true; + if (this->needSetCh2Temp(convertedTemp)) { + if (this->setCh2Temp(convertedTemp)) { + this->ch2SetTempTime = millis(); - } else if (this->heatingBlocking && vars.temperatures.indoor - settings.heating.target - 0.0001f <= -(settings.heating.hysteresis)) { - this->heatingBlocking = false; - } + Log.sinfoln( + FPSTR(L_OT_CH2), F("Set temp: %.2f (converted: %.2f, response: %.2f)"), + vars.master.ch2.targetTemp, convertedTemp, vars.slave.ch2.targetTemp + ); - } else if (this->heatingBlocking) { - this->heatingBlocking = false; + } else { + Log.swarningln(FPSTR(L_OT_CH2), F("Failed set temp")); + } + } + } } } void initialize() { // Not all boilers support these, only try once when the boiler becomes connected if (this->updateSlaveVersion()) { - Log.snoticeln(FPSTR(L_OT), F("Received slave version: %u, type: %u"), vars.parameters.slaveVersion, vars.parameters.slaveType); + Log.snoticeln( + FPSTR(L_OT), F("Received slave app version: %u, type: %u"), + vars.slave.appVersion, vars.slave.type + ); } else { Log.swarningln(FPSTR(L_OT), F("Failed receive slave version")); } - // 0x013F - if (this->setMasterVersion(0x3F, 0x01)) { - Log.snoticeln(FPSTR(L_OT), F("Set master version: %u, type: %u"), vars.parameters.masterVersion, vars.parameters.masterType); + if (this->setMasterVersion(vars.master.appVersion, vars.master.type)) { + Log.snoticeln( + FPSTR(L_OT), F("Set master version: %u, type: %u"), + vars.master.appVersion, vars.master.type + ); } else { Log.swarningln(FPSTR(L_OT), F("Failed set master version")); } if (this->updateSlaveOtVersion()) { - Log.snoticeln(FPSTR(L_OT), F("Received slave OT version: %f"), vars.parameters.slaveOtVersion); + Log.snoticeln(FPSTR(L_OT), F("Received slave OT version: %f"), vars.slave.protocolVersion); } else { Log.swarningln(FPSTR(L_OT), F("Failed receive slave OT version")); } - if (this->setMasterOtVersion(2.2f)) { - Log.snoticeln(FPSTR(L_OT), F("Set master OT version: %f"), vars.parameters.masterOtVersion); + if (this->setMasterOtVersion(vars.master.protocolVersion)) { + Log.snoticeln(FPSTR(L_OT), F("Set master OT version: %f"), vars.master.protocolVersion); } else { Log.swarningln(FPSTR(L_OT), F("Failed set master OT version")); } if (this->updateSlaveConfig()) { - Log.snoticeln(FPSTR(L_OT), F("Received slave member id: %u, flags: %u"), vars.parameters.slaveMemberId, vars.parameters.slaveFlags); + Log.snoticeln( + FPSTR(L_OT), F("Received slave member id: %u, flags: %u"), + vars.slave.memberId, vars.slave.flags + ); } else { Log.swarningln(FPSTR(L_OT), F("Failed receive slave config")); } - if (this->setMasterConfig(settings.opentherm.memberIdCode & 0xFF, (settings.opentherm.memberIdCode & 0xFFFF) >> 8)) { - Log.snoticeln(FPSTR(L_OT), F("Set master member id: %u, flags: %u"), vars.parameters.masterMemberId, vars.parameters.masterFlags); + if (this->setMasterConfig(vars.master.memberId, vars.master.flags)) { + Log.snoticeln( + FPSTR(L_OT), F("Set master member id: %u, flags: %u"), + vars.master.memberId, vars.master.flags + ); } else { Log.swarningln(FPSTR(L_OT), F("Failed set master config")); @@ -739,12 +1125,19 @@ class OpenThermTask : public Task { return millis() - this->instanceCreatedTime > this->readyTime; } - bool needSetDhwTemp() { - return millis() - this->dhwSetTempTime > this->dhwSetTempInterval; + bool needSetDhwTemp(const float target) { + return millis() - this->dhwSetTempTime > this->dhwSetTempInterval + || fabsf(target - vars.slave.dhw.targetTemp) > 0.001f; } - bool needSetHeatingTemp() { - return millis() - this->heatingSetTempTime > this->heatingSetTempInterval; + bool needSetHeatingTemp(const float target) { + return millis() - this->heatingSetTempTime > this->heatingSetTempInterval + || fabsf(target - vars.slave.heating.targetTemp) > 0.001f; + } + + bool needSetCh2Temp(const float target) { + return millis() - this->ch2SetTempTime > this->ch2SetTempInterval + || fabsf(target - vars.slave.ch2.targetTemp) > 0.001f; } bool updateSlaveConfig() { @@ -761,8 +1154,8 @@ class OpenThermTask : public Task { return false; } - vars.parameters.slaveMemberId = response & 0xFF; - vars.parameters.slaveFlags = (response & 0xFFFF) >> 8; + vars.slave.memberId = response & 0xFF; + vars.slave.flags = (response & 0xFFFF) >> 8; /*uint8_t flags = (response & 0xFFFF) >> 8; Log.straceln( @@ -783,6 +1176,224 @@ class OpenThermTask : public Task { return true; } + + bool setMaxModulationLevel(const uint8_t value) { + const unsigned int request = CustomOpenTherm::toFloat(value); + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermRequestType::WRITE_DATA, + OpenThermMessageID::MaxRelModLevelSetting, + request + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::MaxRelModLevelSetting)) { + return false; + } + + vars.slave.modulation.max = CustomOpenTherm::getFloat(response); + + return CustomOpenTherm::getUInt(response) == request; + } + + bool setDhwTemp(const float temperature) { + const unsigned int request = CustomOpenTherm::temperatureToData(temperature); + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermMessageType::WRITE_DATA, + OpenThermMessageID::TdhwSet, + request + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::TdhwSet)) { + return false; + } + + vars.slave.dhw.targetTemp = CustomOpenTherm::getFloat(response); + + return CustomOpenTherm::getUInt(response) == request; + } + + + bool setRoomTemp(float temperature) { + const unsigned int request = CustomOpenTherm::temperatureToData(temperature); + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermMessageType::WRITE_DATA, + OpenThermMessageID::Tr, + request + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Tr)) { + return false; + } + + vars.slave.heating.indoorTemp = CustomOpenTherm::getFloat(response); + + return CustomOpenTherm::getUInt(response) == request; + } + + bool setRoomTempCh2(float temperature) { + const unsigned int request = CustomOpenTherm::temperatureToData(temperature); + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermMessageType::WRITE_DATA, + OpenThermMessageID::TrCH2, + request + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::TrCH2)) { + return false; + } + + vars.slave.ch2.indoorTemp = CustomOpenTherm::getFloat(response); + + return CustomOpenTherm::getUInt(response) == request; + } + + bool setRoomSetpoint(const float temperature) { + const unsigned int request = CustomOpenTherm::temperatureToData(temperature); + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermMessageType::WRITE_DATA, + OpenThermMessageID::TrSet, + request + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::TrSet)) { + return false; + } + + vars.slave.heating.targetTemp = CustomOpenTherm::getFloat(response); + + return CustomOpenTherm::getUInt(response) == request; + } + + bool setRoomSetpointCh2(const float temperature) { + const unsigned int request = CustomOpenTherm::temperatureToData(temperature); + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermMessageType::WRITE_DATA, + OpenThermMessageID::TrSetCH2, + request + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::TrSetCH2)) { + return false; + } + + vars.slave.ch2.targetTemp = CustomOpenTherm::getFloat(response); + + return CustomOpenTherm::getUInt(response) == request; + } + + bool setMaxHeatingTemp(const uint8_t temperature) { + const unsigned int request = CustomOpenTherm::temperatureToData(temperature); + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermMessageType::WRITE_DATA, + OpenThermMessageID::MaxTSet, + request + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::MaxTSet)) { + return false; + } + + return CustomOpenTherm::getUInt(response) == request; + } + + bool setHeatingTemp(const float temperature) { + const unsigned int request = CustomOpenTherm::temperatureToData(temperature); + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermMessageType::WRITE_DATA, + OpenThermMessageID::TSet, + request + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::TSet)) { + return false; + } + + vars.slave.heating.targetTemp = CustomOpenTherm::getFloat(response); + + return CustomOpenTherm::getUInt(response) == request; + } + + bool setCh2Temp(const float temperature) { + const unsigned int request = CustomOpenTherm::temperatureToData(temperature); + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermMessageType::WRITE_DATA, + OpenThermMessageID::TsetCH2, + request + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::TsetCH2)) { + return false; + } + + vars.slave.ch2.targetTemp = CustomOpenTherm::getFloat(response); + + return CustomOpenTherm::getUInt(response) == request; + } + + bool setMasterVersion(const uint8_t version, const uint8_t type) { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermRequestType::WRITE_DATA, + OpenThermMessageID::MasterVersion, + (unsigned int) version | (unsigned int) type << 8 + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::MasterVersion)) { + return false; + } + + uint8_t rVersion = response & 0xFF; + uint8_t rType = (response & 0xFFFF) >> 8; + + return rVersion == version && rType == type; + } + + bool setMasterOtVersion(const float version) { + const unsigned int request = CustomOpenTherm::toFloat(version); + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermRequestType::WRITE_DATA, + OpenThermMessageID::OpenThermVersionMaster, + request + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::OpenThermVersionMaster)) { + return false; + } + + return CustomOpenTherm::getUInt(response) == request; + } + /** * @brief Set the Master Config * From slave member id code: @@ -794,53 +1405,37 @@ class OpenThermTask : public Task { * @return true * @return false */ - bool setMasterConfig(uint8_t id, uint8_t flags, bool force = false) { - //uint8_t configId = settings.opentherm.memberIdCode & 0xFF; - //uint8_t configFlags = (settings.opentherm.memberIdCode & 0xFFFF) >> 8; - - vars.parameters.masterMemberId = (force || id || settings.opentherm.memberIdCode > 65535) - ? id - : vars.parameters.slaveMemberId; + bool setMasterConfig(const uint8_t id, const uint8_t flags, const bool force = false) { + const uint8_t rMemberId = (force || id > 0) ? id : vars.slave.memberId; + const uint8_t rFlags = (force || flags > 0) ? flags : vars.slave.flags; + const unsigned int request = (unsigned int) rMemberId | (unsigned int) rFlags << 8; - vars.parameters.masterFlags = (force || flags || settings.opentherm.memberIdCode > 65535) - ? flags - : vars.parameters.slaveFlags; - - unsigned int request = (unsigned int) vars.parameters.masterMemberId | (unsigned int) vars.parameters.masterFlags << 8; // if empty request if (!request) { return true; } - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::WRITE_DATA, OpenThermMessageID::MConfigMMemberIDcode, request )); - return CustomOpenTherm::isValidResponse(response) && CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::MConfigMMemberIDcode); - } - - bool setMaxModulationLevel(byte value) { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( - OpenThermRequestType::WRITE_DATA, - OpenThermMessageID::MaxRelModLevelSetting, - CustomOpenTherm::toFloat(value) - )); - if (!CustomOpenTherm::isValidResponse(response)) { return false; - } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::MaxRelModLevelSetting)) { + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::MConfigMMemberIDcode)) { return false; } - vars.parameters.maxModulation = CustomOpenTherm::getFloat(response); - return true; + //uint8_t rMemberId = response & 0xFF; + //uint8_t rFlags = (response & 0xFFFF) >> 8; + + return CustomOpenTherm::getUInt(response) == request; } bool updateSlaveOtVersion() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::OpenThermVersionSlave, 0 @@ -853,31 +1448,13 @@ class OpenThermTask : public Task { return false; } - vars.parameters.slaveOtVersion = CustomOpenTherm::getFloat(response); - return true; - } - - bool setMasterOtVersion(float version) { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( - OpenThermRequestType::WRITE_DATA, - OpenThermMessageID::OpenThermVersionMaster, - CustomOpenTherm::toFloat(version) - )); - - if (!CustomOpenTherm::isValidResponse(response)) { - return false; - - } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::OpenThermVersionMaster)) { - return false; - } - - vars.parameters.masterOtVersion = CustomOpenTherm::getFloat(response); + vars.slave.protocolVersion = CustomOpenTherm::getFloat(response); return true; } bool updateSlaveVersion() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::SlaveVersion, 0 @@ -890,34 +1467,37 @@ class OpenThermTask : public Task { return false; } - vars.parameters.slaveVersion = response & 0xFF; - vars.parameters.slaveType = (response & 0xFFFF) >> 8; + vars.slave.appVersion = response & 0xFF; + vars.slave.type = (response & 0xFFFF) >> 8; return true; } - bool setMasterVersion(uint8_t version, uint8_t type) { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( - OpenThermRequestType::WRITE_DATA, - OpenThermMessageID::MasterVersion, - (unsigned int) version | (unsigned int) type << 8 + bool updateMinModulationLevel() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermRequestType::READ_DATA, + OpenThermMessageID::MaxCapacityMinModLevel, + 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; - } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::MasterVersion)) { + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::MaxCapacityMinModLevel)) { return false; } - vars.parameters.masterVersion = response & 0xFF; - vars.parameters.masterType = (response & 0xFFFF) >> 8; + vars.slave.modulation.min = response & 0xFF; + vars.slave.power.max = (response & 0xFFFF) >> 8; + vars.slave.power.min = vars.slave.modulation.min > 0 && vars.slave.power.max > 0.1f + ? (vars.slave.modulation.min * 0.01f) * vars.slave.power.max + : 0.0f; return true; } bool updateMinMaxDhwTemp() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::TdhwSetUBTdhwSetLB, 0 @@ -930,12 +1510,12 @@ class OpenThermTask : public Task { return false; } - byte minTemp = response & 0xFF; - byte maxTemp = (response & 0xFFFF) >> 8; + uint8_t minTemp = response & 0xFF; + uint8_t maxTemp = (response & 0xFFFF) >> 8; if (minTemp >= 0 && maxTemp > 0 && maxTemp > minTemp) { - vars.parameters.dhwMinTemp = convertTemp(minTemp, settings.opentherm.unitSystem, settings.system.unitSystem); - vars.parameters.dhwMaxTemp = convertTemp(maxTemp, settings.opentherm.unitSystem, settings.system.unitSystem); + vars.slave.dhw.minTemp = minTemp; + vars.slave.dhw.maxTemp = maxTemp; return true; } @@ -944,7 +1524,7 @@ class OpenThermTask : public Task { } bool updateMinMaxHeatingTemp() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::MaxTSetUBMaxTSetLB, 0 @@ -957,104 +1537,92 @@ class OpenThermTask : public Task { return false; } - byte minTemp = response & 0xFF; - byte maxTemp = (response & 0xFFFF) >> 8; + uint8_t minTemp = response & 0xFF; + uint8_t maxTemp = (response & 0xFFFF) >> 8; if (minTemp >= 0 && maxTemp > 0 && maxTemp > minTemp) { - vars.parameters.heatingMinTemp = convertTemp(minTemp, settings.opentherm.unitSystem, settings.system.unitSystem); - vars.parameters.heatingMaxTemp = convertTemp(maxTemp, settings.opentherm.unitSystem, settings.system.unitSystem); + vars.slave.heating.minTemp = minTemp; + vars.slave.heating.maxTemp = maxTemp; + return true; } return false; } - bool setMaxHeatingTemp(byte value) { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( - OpenThermMessageType::WRITE_DATA, - OpenThermMessageID::MaxTSet, - CustomOpenTherm::temperatureToData(value) + bool updateFaultCode() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermRequestType::READ_DATA, + OpenThermMessageID::ASFflags, + 0 )); - return CustomOpenTherm::isValidResponse(response) && CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::MaxTSet); + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::ASFflags)) { + return false; + } + + vars.slave.fault.code = response & 0xFF; + + return true; } - bool updateOutdoorTemp() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + bool updateDiagCode() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, - OpenThermMessageID::Toutside, + OpenThermMessageID::OEMDiagnosticCode, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; - } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Toutside)) { + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::OEMDiagnosticCode)) { return false; } - - float value = settings.sensors.outdoor.offset + convertTemp( - CustomOpenTherm::getFloat(response), - settings.opentherm.unitSystem, - settings.system.unitSystem - ); - if (settings.opentherm.filterNumValues.enable && fabs(vars.temperatures.outdoor) >= 0.1f) { - vars.temperatures.outdoor += (value - vars.temperatures.outdoor) * settings.opentherm.filterNumValues.factor; - - } else { - vars.temperatures.outdoor = value; - } + vars.slave.diag.code = CustomOpenTherm::getUInt(response); return true; } - bool updateExhaustTemp() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + bool updateModulationLevel() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, - OpenThermMessageID::Texhaust, + OpenThermMessageID::RelModLevel, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; - } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Texhaust)) { + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::RelModLevel)) { return false; } - float value = (float) CustomOpenTherm::getInt(response); - if (!isValidTemp(value, settings.opentherm.unitSystem, -40, 500)) { + float value = CustomOpenTherm::getFloat(response); + if (value < 0) { return false; } - value = convertTemp( - value, - settings.opentherm.unitSystem, - settings.system.unitSystem - ); - - if (settings.opentherm.filterNumValues.enable && fabs(vars.temperatures.exhaust) >= 0.1f) { - vars.temperatures.exhaust += (value - vars.temperatures.exhaust) * settings.opentherm.filterNumValues.factor; - - } else { - vars.temperatures.exhaust = value; - } + vars.slave.modulation.current = value; return true; } - bool updateHeatingTemp() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + bool updateDhwTemp() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermMessageType::READ_DATA, - OpenThermMessageID::Tboiler, + OpenThermMessageID::Tdhw, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; - } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Tboiler)) { + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Tdhw)) { return false; } @@ -1063,65 +1631,76 @@ class OpenThermTask : public Task { return false; } - value = convertTemp( - value, - settings.opentherm.unitSystem, - settings.system.unitSystem - ); + vars.slave.dhw.currentTemp = value; - if (settings.opentherm.filterNumValues.enable && fabs(vars.temperatures.heating) >= 0.1f) { - vars.temperatures.heating += (value - vars.temperatures.heating) * settings.opentherm.filterNumValues.factor; + return true; + } - } else { - vars.temperatures.heating = value; + bool updateDhwTemp2() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermMessageType::READ_DATA, + OpenThermMessageID::Tdhw2, + 0 + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Tdhw2)) { + return false; } + float value = CustomOpenTherm::getFloat(response); + if (value <= 0) { + return false; + } + + vars.slave.dhw.currentTemp2 = value; + return true; } - bool updateHeatingReturnTemp() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + bool updateDhwFlowRate() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermMessageType::READ_DATA, - OpenThermMessageID::Tret, + OpenThermMessageID::DHWFlowRate, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; - } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Tret)) { + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::DHWFlowRate)) { return false; } - float value = convertTemp( - CustomOpenTherm::getFloat(response), - settings.opentherm.unitSystem, - settings.system.unitSystem - ); + float value = CustomOpenTherm::getFloat(response); + if (value < 0) { + return false; + } - if (settings.opentherm.filterNumValues.enable && fabs(vars.temperatures.heatingReturn) >= 0.1f) { - vars.temperatures.heatingReturn += (value - vars.temperatures.heatingReturn) * settings.opentherm.filterNumValues.factor; - - } else { - vars.temperatures.heatingReturn = value; + // no minuscule values + // some boilers send a response of 0.06 when there is no flow + if (value < 0.1f) { + value = 0.0f; } - + vars.slave.dhw.flowRate = value; + return true; } - - bool updateDhwTemp() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + bool updateHeatingTemp() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermMessageType::READ_DATA, - OpenThermMessageID::Tdhw, + OpenThermMessageID::Tboiler, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; - } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Tdhw)) { + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Tboiler)) { return false; } @@ -1130,151 +1709,178 @@ class OpenThermTask : public Task { return false; } - value = convertTemp( - value, - settings.opentherm.unitSystem, - settings.system.unitSystem - ); + vars.slave.heating.currentTemp = value; - if (settings.opentherm.filterNumValues.enable && fabs(vars.temperatures.dhw) >= 0.1f) { - vars.temperatures.dhw += (value - vars.temperatures.dhw) * settings.opentherm.filterNumValues.factor; - - } else { - vars.temperatures.dhw = value; + return true; + } + + bool updateHeatingReturnTemp() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermMessageType::READ_DATA, + OpenThermMessageID::Tret, + 0 + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Tret)) { + return false; } + vars.slave.heating.returnTemp = CustomOpenTherm::getFloat(response); + return true; } - bool updateDhwFlowRate() { + bool updateCh2Temp() { unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( - OpenThermMessageType::READ_DATA, - OpenThermMessageID::DHWFlowRate, + OpenThermRequestType::READ_DATA, + OpenThermMessageID::TflowCH2, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; - } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::DHWFlowRate)) { + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::TflowCH2)) { return false; } - float value = CustomOpenTherm::getFloat(response); - if (value < 0) { + vars.slave.ch2.currentTemp = CustomOpenTherm::getFloat(response); + + return true; + } + + + + bool updateExhaustTemp() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermRequestType::READ_DATA, + OpenThermMessageID::Texhaust, + 0 + )); + + if (!CustomOpenTherm::isValidResponse(response)) { return false; - } - // correction - value = value * settings.opentherm.dhwFlowRateFactor; + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Texhaust)) { + return false; + } - // no minuscule values - // some boilers send a response of 0.06 when there is no flow - if (value < 0.1f) { - value = 0.0f; + float value = (float) CustomOpenTherm::getInt(response); + if (!isValidTemp(value, settings.opentherm.unitSystem, -40, 500)) { + return false; } - // protocol declares a maximum of 16 l/m - //if (value > convertVolume(16.0f, UnitSystem::METRIC, settings.opentherm.unitSystem)) { - // value = 0.0f; - //} + vars.slave.exhaust.temp = value; - vars.sensors.dhwFlowRate = convertVolume( - value, - settings.opentherm.unitSystem, - settings.system.unitSystem - ); - return true; } - bool updateFaultCode() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + bool updateHeatExchangerTemp() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, - OpenThermMessageID::ASFflags, + OpenThermMessageID::TboilerHeatExchanger, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; - } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::ASFflags)) { + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::TboilerHeatExchanger)) { + return false; + } + + float value = (float) CustomOpenTherm::getInt(response); + if (value <= 0) { return false; } - vars.sensors.faultCode = response & 0xFF; + vars.slave.heatExchangerTemp = value; + return true; } - bool updateDiagCode() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + bool updateOutdoorTemp() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, - OpenThermMessageID::OEMDiagnosticCode, + OpenThermMessageID::Toutside, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; - } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::OEMDiagnosticCode)) { + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Toutside)) { return false; } - vars.sensors.diagnosticCode = CustomOpenTherm::getUInt(response); + vars.slave.heating.outdoorTemp = CustomOpenTherm::getFloat(response); + return true; } - bool updateModulationLevel() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( - OpenThermRequestType::READ_DATA, - OpenThermMessageID::RelModLevel, + bool updateSolarStorageTemp() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermMessageType::READ_DATA, + OpenThermMessageID::Tstorage, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; - } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::RelModLevel)) { + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Tstorage)) { return false; } - float value = CustomOpenTherm::getFloat(response); - if (value < 0) { + vars.slave.solar.storage = CustomOpenTherm::getFloat(response); + + return true; + } + + bool updateSolarCollectorTemp() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermMessageType::READ_DATA, + OpenThermMessageID::Tcollector, + 0 + )); + + if (!CustomOpenTherm::isValidResponse(response)) { return false; - } - if (settings.opentherm.filterNumValues.enable && fabs(vars.sensors.modulation) >= 0.1f) { - vars.sensors.modulation += (value - vars.sensors.modulation) * settings.opentherm.filterNumValues.factor; - - } else { - vars.sensors.modulation = value; + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::Tcollector)) { + return false; } + vars.slave.solar.collector = CustomOpenTherm::getFloat(response); + return true; } - bool updateMinModulationLevel() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + bool updateFanSpeed() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, - OpenThermMessageID::MaxCapacityMinModLevel, + OpenThermMessageID::BoilerFanSpeedSetpointAndActual, 0 )); if (!CustomOpenTherm::isValidResponse(response)) { return false; - } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::MaxCapacityMinModLevel)) { + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::BoilerFanSpeedSetpointAndActual)) { return false; } - vars.parameters.minModulation = response & 0xFF; - vars.parameters.maxPower = (response & 0xFFFF) >> 8; + vars.slave.fanSpeed.setpoint = (response & 0xFFFF) >> 8; + vars.slave.fanSpeed.current = response & 0xFF; return true; } bool updatePressure() { - unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( OpenThermRequestType::READ_DATA, OpenThermMessageID::CHPressure, 0 @@ -1292,27 +1898,65 @@ class OpenThermTask : public Task { return false; } - // correction - value = value * settings.opentherm.pressureFactor; + vars.slave.pressure = value; - // protocol declares a maximum of 5 bar - //if (value > convertPressure(5.0f, UnitSystem::METRIC, settings.opentherm.unitSystem)) { - // value = 0.0f; - //} + return true; + } - value = convertPressure( - value, - settings.opentherm.unitSystem, - settings.system.unitSystem - ); + bool updateExhaustCo2() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermRequestType::READ_DATA, + OpenThermMessageID::CO2exhaust, + 0 + )); - if (settings.opentherm.filterNumValues.enable && fabs(vars.sensors.pressure) >= 0.1f) { - vars.sensors.pressure += (value - vars.sensors.pressure) * settings.opentherm.filterNumValues.factor; - - } else { - vars.sensors.pressure = value; + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::CO2exhaust)) { + return false; + } + + vars.slave.exhaust.co2 = CustomOpenTherm::getUInt(response); + + return true; + } + + bool updateExhaustFanSpeed() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermRequestType::READ_DATA, + OpenThermMessageID::RPMexhaust, + 0 + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::RPMexhaust)) { + return false; } + vars.slave.exhaust.fanSpeed = CustomOpenTherm::getUInt(response); + + return true; + } + + bool updateSupplyFanSpeed() { + const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( + OpenThermRequestType::READ_DATA, + OpenThermMessageID::RPMsupply, + 0 + )); + + if (!CustomOpenTherm::isValidResponse(response)) { + return false; + + } else if (!CustomOpenTherm::isValidResponseId(response, OpenThermMessageID::RPMsupply)) { + return false; + } + + vars.slave.fanSpeed.supply = CustomOpenTherm::getUInt(response); + return true; } }; diff --git a/src/PortalTask.h b/src/PortalTask.h index 6309760..f2aec3e 100644 --- a/src/PortalTask.h +++ b/src/PortalTask.h @@ -1,5 +1,5 @@ -#define PORTAL_CACHE_TIME "max-age=86400" -#define PORTAL_CACHE (settings.system.logLevel >= TinyLogger::Level::TRACE ? nullptr : PORTAL_CACHE_TIME) +//#define PORTAL_CACHE "max-age=86400" +#define PORTAL_CACHE nullptr #ifdef ARDUINO_ARCH_ESP8266 #include #include @@ -17,7 +17,7 @@ using WebServer = ESP8266WebServer; using namespace NetworkUtils; extern NetworkMgr* network; -extern FileData fsSettings, fsNetworkSettings; +extern FileData fsNetworkSettings, fsSettings, fsSensorsSettings; extern MqttTask* tMqtt; @@ -72,9 +72,24 @@ class PortalTask : public LeanTask { void setup() { this->dnsServer->setTTL(0); this->dnsServer->setErrorReplyCode(DNSReplyCode::NoError); - #ifdef ARDUINO_ARCH_ESP8266 - this->webServer->enableETag(true); - #endif + this->webServer->enableETag(true, [](FS &fs, const String &fName) -> const String { + char buf[32]; + { + MD5Builder md5; + md5.begin(); + md5.add(fName); + md5.add(" " BUILD_ENV " " BUILD_VERSION " " __DATE__ " " __TIME__); + md5.calculate(); + md5.getChars(buf); + } + + String etag; + etag.reserve(34); + etag += '\"'; + etag.concat(buf, 32); + etag += '\"'; + return etag; + }); // index page /*auto indexPage = (new DynamicPage("/", &LittleFS, "/pages/index.html")) @@ -88,13 +103,13 @@ class PortalTask : public LeanTask { return result; }); this->webServer->addHandler(indexPage);*/ - this->webServer->addHandler(new StaticPage("/", &LittleFS, "/pages/index.html", PORTAL_CACHE)); + this->webServer->addHandler(new StaticPage("/", &LittleFS, F("/pages/index.html"), PORTAL_CACHE)); // dashboard page - auto dashboardPage = (new StaticPage("/dashboard.html", &LittleFS, "/pages/dashboard.html", PORTAL_CACHE)) + auto dashboardPage = (new StaticPage("/dashboard.html", &LittleFS, F("/pages/dashboard.html"), PORTAL_CACHE)) ->setBeforeSendCallback([this]() { - if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - this->webServer->requestAuthentication(DIGEST_AUTH); + if (this->isAuthRequired() && !this->isValidCredentials()) { + this->webServer->requestAuthentication(BASIC_AUTH); return false; } @@ -103,24 +118,22 @@ class PortalTask : public LeanTask { this->webServer->addHandler(dashboardPage); // restart - this->webServer->on("/restart.html", HTTP_GET, [this]() { - if (this->isAuthRequired()) { - if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - this->webServer->send(401); - return; - } + this->webServer->on(F("/restart.html"), HTTP_GET, [this]() { + if (this->isAuthRequired() && !this->isValidCredentials()) { + this->webServer->requestAuthentication(BASIC_AUTH); + return; } vars.actions.restart = true; - this->webServer->sendHeader("Location", "/"); + this->webServer->sendHeader(F("Location"), "/"); this->webServer->send(302); }); // network settings page - auto networkPage = (new StaticPage("/network.html", &LittleFS, "/pages/network.html", PORTAL_CACHE)) + auto networkPage = (new StaticPage("/network.html", &LittleFS, F("/pages/network.html"), PORTAL_CACHE)) ->setBeforeSendCallback([this]() { - if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - this->webServer->requestAuthentication(DIGEST_AUTH); + if (this->isAuthRequired() && !this->isValidCredentials()) { + this->webServer->requestAuthentication(BASIC_AUTH); return false; } @@ -129,10 +142,10 @@ class PortalTask : public LeanTask { this->webServer->addHandler(networkPage); // settings page - auto settingsPage = (new StaticPage("/settings.html", &LittleFS, "/pages/settings.html", PORTAL_CACHE)) + auto settingsPage = (new StaticPage("/settings.html", &LittleFS, F("/pages/settings.html"), PORTAL_CACHE)) ->setBeforeSendCallback([this]() { - if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - this->webServer->requestAuthentication(DIGEST_AUTH); + if (this->isAuthRequired() && !this->isValidCredentials()) { + this->webServer->requestAuthentication(BASIC_AUTH); return false; } @@ -140,11 +153,23 @@ class PortalTask : public LeanTask { }); this->webServer->addHandler(settingsPage); + // sensors page + auto sensorsPage = (new StaticPage("/sensors.html", &LittleFS, F("/pages/sensors.html"), PORTAL_CACHE)) + ->setBeforeSendCallback([this]() { + if (this->isAuthRequired() && !this->isValidCredentials()) { + this->webServer->requestAuthentication(BASIC_AUTH); + return false; + } + + return true; + }); + this->webServer->addHandler(sensorsPage); + // upgrade page - auto upgradePage = (new StaticPage("/upgrade.html", &LittleFS, "/pages/upgrade.html", PORTAL_CACHE)) + auto upgradePage = (new StaticPage("/upgrade.html", &LittleFS, F("/pages/upgrade.html"), PORTAL_CACHE)) ->setBeforeSendCallback([this]() { - if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - this->webServer->requestAuthentication(DIGEST_AUTH); + if (this->isAuthRequired() && !this->isValidCredentials()) { + this->webServer->requestAuthentication(BASIC_AUTH); return false; } @@ -154,14 +179,19 @@ class PortalTask : public LeanTask { // OTA auto upgradeHandler = (new UpgradeHandler("/api/upgrade"))->setCanUploadCallback([this](const String& uri) { - if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - this->webServer->sendHeader("Connection", "close"); + if (this->isAuthRequired() && !this->isValidCredentials()) { + this->webServer->sendHeader(F("Connection"), F("close")); this->webServer->send(401); return false; } return true; })->setBeforeUpgradeCallback([](UpgradeHandler::UpgradeType type) -> bool { + if (vars.states.restarting) { + return false; + } + + vars.states.upgrading = true; return true; })->setAfterUpgradeCallback([this](const UpgradeHandler::UpgradeResult& fwResult, const UpgradeHandler::UpgradeResult& fsResult) { unsigned short status = 200; @@ -172,53 +202,57 @@ class PortalTask : public LeanTask { status = 400; } - String response = "{\"firmware\": {\"status\": "; + String response = F("{\"firmware\": {\"status\": "); response.concat((short int) fwResult.status); - response.concat(", \"error\": \""); + response.concat(F(", \"error\": \"")); response.concat(fwResult.error); - response.concat("\"}, \"filesystem\": {\"status\": "); + response.concat(F("\"}, \"filesystem\": {\"status\": ")); response.concat((short int) fsResult.status); - response.concat(", \"error\": \""); + response.concat(F(", \"error\": \"")); response.concat(fsResult.error); - response.concat("\"}}"); - this->webServer->send(status, "application/json", response); + response.concat(F("\"}}")); + this->webServer->send(status, F("application/json"), response); + + vars.states.upgrading = false; }); this->webServer->addHandler(upgradeHandler); // backup - this->webServer->on("/api/backup/save", HTTP_GET, [this]() { - if (this->isAuthRequired()) { - if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - return this->webServer->send(401); - } + this->webServer->on(F("/api/backup/save"), HTTP_GET, [this]() { + if (this->isAuthRequired() && !this->isValidCredentials()) { + return this->webServer->send(401); } - JsonDocument networkSettingsDoc; - networkSettingsToJson(networkSettings, networkSettingsDoc); - networkSettingsDoc.shrinkToFit(); + JsonDocument doc; + + auto networkDoc = doc[FPSTR(S_NETWORK)].to(); + networkSettingsToJson(networkSettings, networkDoc); - JsonDocument settingsDoc; + auto settingsDoc = doc[FPSTR(S_SETTINGS)].to(); settingsToJson(settings, settingsDoc); - settingsDoc.shrinkToFit(); - JsonDocument doc; - doc["network"] = networkSettingsDoc; - doc["settings"] = settingsDoc; + for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { + auto sensorsettingsDoc = doc[FPSTR(S_SENSORS)][sensorId].to(); + sensorSettingsToJson(sensorId, Sensors::settings[sensorId], sensorsettingsDoc); + } + doc.shrinkToFit(); this->webServer->sendHeader(F("Content-Disposition"), F("attachment; filename=\"backup.json\"")); - this->bufferedWebServer->send(200, "application/json", doc); + this->bufferedWebServer->send(200, F("application/json"), doc); }); - this->webServer->on("/api/backup/restore", HTTP_POST, [this]() { - if (this->isAuthRequired()) { - if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - return this->webServer->send(401); - } + this->webServer->on(F("/api/backup/restore"), HTTP_POST, [this]() { + if (this->isAuthRequired() && !this->isValidCredentials()) { + return this->webServer->send(401); + } + + if (vars.states.restarting) { + return this->webServer->send(503); } - String plain = this->webServer->arg(0); + const String& plain = this->webServer->arg(0); Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/backup/restore %d bytes: %s"), plain.length(), plain.c_str()); if (plain.length() < 5) { @@ -232,7 +266,6 @@ class PortalTask : public LeanTask { JsonDocument doc; DeserializationError dErr = deserializeJson(doc, plain); - plain.clear(); if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) { this->webServer->send(400); @@ -240,13 +273,7 @@ class PortalTask : public LeanTask { } bool changed = false; - if (doc["settings"] && jsonToSettings(doc["settings"], settings)) { - vars.actions.restart = true; - fsSettings.update(); - changed = true; - } - - if (doc["network"] && jsonToNetworkSettings(doc["network"], networkSettings)) { + if (!doc[FPSTR(S_NETWORK)].isNull() && jsonToNetworkSettings(doc[FPSTR(S_NETWORK)], networkSettings)) { fsNetworkSettings.update(); network->setHostname(networkSettings.hostname) ->setStaCredentials(networkSettings.sta.ssid, networkSettings.sta.password, networkSettings.sta.channel) @@ -257,40 +284,66 @@ class PortalTask : public LeanTask { networkSettings.staticConfig.gateway, networkSettings.staticConfig.subnet, networkSettings.staticConfig.dns - ) - ->reconnect(); + ); + changed = true; + } + + if (!doc[FPSTR(S_SETTINGS)].isNull() && jsonToSettings(doc[FPSTR(S_SETTINGS)], settings)) { + fsSettings.update(); changed = true; } + if (!doc[FPSTR(S_SENSORS)].isNull()) { + for (auto sensor : doc[FPSTR(S_SENSORS)].as()) { + if (!isDigit(sensor.key().c_str())) { + continue; + } + + int sensorId = atoi(sensor.key().c_str()); + if (sensorId < 0 || sensorId > 255 || !Sensors::isValidSensorId(sensorId)) { + continue; + } + + if (jsonToSensorSettings(sensorId, sensor.value(), Sensors::settings[sensorId])) { + fsSensorsSettings.update(); + changed = true; + } + } + } + doc.clear(); doc.shrinkToFit(); + if (changed) { + vars.actions.restart = true; + } + this->webServer->send(changed ? 201 : 200); }); // network - this->webServer->on("/api/network/settings", HTTP_GET, [this]() { - if (this->isAuthRequired()) { - if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - return this->webServer->send(401); - } + this->webServer->on(F("/api/network/settings"), HTTP_GET, [this]() { + if (this->isAuthRequired() && !this->isValidCredentials()) { + return this->webServer->send(401); } JsonDocument doc; networkSettingsToJson(networkSettings, doc); doc.shrinkToFit(); - this->bufferedWebServer->send(200, "application/json", doc); + this->bufferedWebServer->send(200, F("application/json"), doc); }); - this->webServer->on("/api/network/settings", HTTP_POST, [this]() { - if (this->isAuthRequired()) { - if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - return this->webServer->send(401); - } + this->webServer->on(F("/api/network/settings"), HTTP_POST, [this]() { + if (this->isAuthRequired() && !this->isValidCredentials()) { + return this->webServer->send(401); + } + + if (vars.states.restarting) { + return this->webServer->send(503); } - String plain = this->webServer->arg(0); + const String& plain = this->webServer->arg(0); Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/network/settings %d bytes: %s"), plain.length(), plain.c_str()); if (plain.length() < 5) { @@ -304,7 +357,6 @@ class PortalTask : public LeanTask { JsonDocument doc; DeserializationError dErr = deserializeJson(doc, plain); - plain.clear(); if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) { this->webServer->send(400); @@ -318,7 +370,7 @@ class PortalTask : public LeanTask { networkSettingsToJson(networkSettings, doc); doc.shrinkToFit(); - this->bufferedWebServer->send(changed ? 201 : 200, "application/json", doc); + this->bufferedWebServer->send(changed ? 201 : 200, F("application/json"), doc); if (changed) { doc.clear(); @@ -339,12 +391,9 @@ class PortalTask : public LeanTask { } }); - this->webServer->on("/api/network/scan", HTTP_GET, [this]() { - if (this->isAuthRequired()) { - if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - this->webServer->send(401); - return; - } + this->webServer->on(F("/api/network/scan"), HTTP_GET, [this]() { + if (this->isAuthRequired() && !this->isValidCredentials()) { + return this->webServer->send(401); } auto apCount = WiFi.scanComplete(); @@ -363,50 +412,50 @@ class PortalTask : public LeanTask { JsonDocument doc; for (short int i = 0; i < apCount; i++) { - String ssid = WiFi.SSID(i); - doc[i]["ssid"] = ssid; - doc[i]["bssid"] = WiFi.BSSIDstr(i); - doc[i]["signalQuality"] = NetworkMgr::rssiToSignalQuality(WiFi.RSSI(i)); - doc[i]["channel"] = WiFi.channel(i); - doc[i]["hidden"] = !ssid.length(); + const String& ssid = WiFi.SSID(i); + doc[i][FPSTR(S_SSID)] = ssid; + doc[i][FPSTR(S_BSSID)] = WiFi.BSSIDstr(i); + doc[i][FPSTR(S_SIGNAL_QUALITY)] = NetworkMgr::rssiToSignalQuality(WiFi.RSSI(i)); + doc[i][FPSTR(S_CHANNEL)] = WiFi.channel(i); + doc[i][FPSTR(S_HIDDEN)] = !ssid.length(); #ifdef ARDUINO_ARCH_ESP8266 const bss_info* info = WiFi.getScanInfoByIndex(i); - doc[i]["auth"] = info->authmode; + doc[i][FPSTR(S_AUTH)] = info->authmode; #else - doc[i]["auth"] = WiFi.encryptionType(i); + doc[i][FPSTR(S_AUTH)] = WiFi.encryptionType(i); #endif } doc.shrinkToFit(); - this->bufferedWebServer->send(200, "application/json", doc); + this->bufferedWebServer->send(200, F("application/json"), doc); WiFi.scanDelete(); }); // settings - this->webServer->on("/api/settings", HTTP_GET, [this]() { - if (this->isAuthRequired()) { - if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - return this->webServer->send(401); - } + this->webServer->on(F("/api/settings"), HTTP_GET, [this]() { + if (this->isAuthRequired() && !this->isValidCredentials()) { + return this->webServer->send(401); } JsonDocument doc; settingsToJson(settings, doc); doc.shrinkToFit(); - this->bufferedWebServer->send(200, "application/json", doc); + this->bufferedWebServer->send(200, F("application/json"), doc); }); - this->webServer->on("/api/settings", HTTP_POST, [this]() { - if (this->isAuthRequired()) { - if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - return this->webServer->send(401); - } + this->webServer->on(F("/api/settings"), HTTP_POST, [this]() { + if (this->isAuthRequired() && !this->isValidCredentials()) { + return this->webServer->send(401); + } + + if (vars.states.restarting) { + return this->webServer->send(503); } - String plain = this->webServer->arg(0); + const String& plain = this->webServer->arg(0); Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/settings %d bytes: %s"), plain.length(), plain.c_str()); if (plain.length() < 5) { @@ -420,7 +469,6 @@ class PortalTask : public LeanTask { JsonDocument doc; DeserializationError dErr = deserializeJson(doc, plain); - plain.clear(); if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) { this->webServer->send(400); @@ -434,7 +482,7 @@ class PortalTask : public LeanTask { settingsToJson(settings, doc); doc.shrinkToFit(); - this->bufferedWebServer->send(changed ? 201 : 200, "application/json", doc); + this->bufferedWebServer->send(changed ? 201 : 200, F("application/json"), doc); if (changed) { doc.clear(); @@ -446,23 +494,149 @@ class PortalTask : public LeanTask { }); + // sensors list + this->webServer->on(F("/api/sensors"), HTTP_GET, [this]() { + if (this->isAuthRequired() && !this->isValidCredentials()) { + return this->webServer->send(401); + } + + bool detailed = false; + if (this->webServer->hasArg(F("detailed"))) { + detailed = this->webServer->arg(F("detailed")).toInt() > 0; + } + + JsonDocument doc; + for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { + if (detailed) { + auto& sSensor = Sensors::settings[sensorId]; + doc[sensorId][FPSTR(S_ENABLED)] = sSensor.enabled; + doc[sensorId][FPSTR(S_NAME)] = sSensor.name; + doc[sensorId][FPSTR(S_PURPOSE)] = static_cast(sSensor.purpose); + sensorResultToJson(sensorId, doc[sensorId]); + + } else { + doc[sensorId] = Sensors::settings[sensorId].name; + } + } + + doc.shrinkToFit(); + this->bufferedWebServer->send(200, F("application/json"), doc); + }); + + // sensor settings + this->webServer->on(F("/api/sensor"), HTTP_GET, [this]() { + if (this->isAuthRequired() && !this->isValidCredentials()) { + return this->webServer->send(401); + } + + if (!this->webServer->hasArg(F("id"))) { + return this->webServer->send(400); + } + + auto id = this->webServer->arg(F("id")); + if (!isDigit(id.c_str())) { + return this->webServer->send(400); + } + + uint8_t sensorId = id.toInt(); + id.clear(); + if (!Sensors::isValidSensorId(sensorId)) { + return this->webServer->send(404); + } + + JsonDocument doc; + sensorSettingsToJson(sensorId, Sensors::settings[sensorId], doc); + doc.shrinkToFit(); + this->bufferedWebServer->send(200, F("application/json"), doc); + }); + + this->webServer->on(F("/api/sensor"), HTTP_POST, [this]() { + if (this->isAuthRequired() && !this->isValidCredentials()) { + return this->webServer->send(401); + } + + if (vars.states.restarting) { + return this->webServer->send(503); + } + + #ifdef ARDUINO_ARCH_ESP8266 + if (!this->webServer->hasArg(F("id")) || this->webServer->args() != 1) { + return this->webServer->send(400); + } + #else + if (!this->webServer->hasArg(F("id")) || this->webServer->args() != 2) { + return this->webServer->send(400); + } + #endif + + auto id = this->webServer->arg(F("id")); + if (!isDigit(id.c_str())) { + return this->webServer->send(400); + } + + uint8_t sensorId = id.toInt(); + id.clear(); + if (!Sensors::isValidSensorId(sensorId)) { + return this->webServer->send(404); + } + + auto plain = this->webServer->arg(1); + Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/sensor/?id=%hhu %d bytes: %s"), sensorId, plain.length(), plain.c_str()); + + if (plain.length() < 5) { + return this->webServer->send(406); + + } else if (plain.length() > 1024) { + return this->webServer->send(413); + } + + bool changed = false; + auto prevSettings = Sensors::settings[sensorId]; + { + JsonDocument doc; + DeserializationError dErr = deserializeJson(doc, plain); + plain.clear(); + + if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) { + return this->webServer->send(400); + } + + if (jsonToSensorSettings(sensorId, doc, Sensors::settings[sensorId])) { + changed = true; + } + } + + { + JsonDocument doc; + auto& sSettings = Sensors::settings[sensorId]; + sensorSettingsToJson(sensorId, sSettings, doc); + doc.shrinkToFit(); + + this->bufferedWebServer->send(changed ? 201 : 200, F("application/json"), doc); + } + + if (changed) { + tMqtt->rebuildHaEntity(sensorId, prevSettings); + fsSensorsSettings.update(); + } + }); + + // vars - this->webServer->on("/api/vars", HTTP_GET, [this]() { + this->webServer->on(F("/api/vars"), HTTP_GET, [this]() { JsonDocument doc; varsToJson(vars, doc); doc.shrinkToFit(); - this->bufferedWebServer->send(200, "application/json", doc); + this->bufferedWebServer->send(200, F("application/json"), doc); }); - this->webServer->on("/api/vars", HTTP_POST, [this]() { - if (this->isAuthRequired()) { - if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) { - return this->webServer->send(401); - } + this->webServer->on(F("/api/vars"), HTTP_POST, [this]() { + if (this->isAuthRequired() && !this->isValidCredentials()) { + return this->webServer->send(401); } - String plain = this->webServer->arg(0); + const String& plain = this->webServer->arg(0); Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/vars %d bytes: %s"), plain.length(), plain.c_str()); if (plain.length() < 5) { @@ -476,7 +650,6 @@ class PortalTask : public LeanTask { JsonDocument doc; DeserializationError dErr = deserializeJson(doc, plain); - plain.clear(); if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) { this->webServer->send(400); @@ -490,7 +663,7 @@ class PortalTask : public LeanTask { varsToJson(vars, doc); doc.shrinkToFit(); - this->bufferedWebServer->send(changed ? 201 : 200, "application/json", doc); + this->bufferedWebServer->send(changed ? 201 : 200, F("application/json"), doc); if (changed) { doc.clear(); @@ -500,78 +673,138 @@ class PortalTask : public LeanTask { } }); - this->webServer->on("/api/info", HTTP_GET, [this]() { + this->webServer->on(F("/api/info"), HTTP_GET, [this]() { bool isConnected = network->isConnected(); JsonDocument doc; - doc["system"]["resetReason"] = getResetReason(); - doc["system"]["uptime"] = millis() / 1000ul; - - doc["network"]["hostname"] = networkSettings.hostname; - doc["network"]["mac"] = network->getStaMac(); - doc["network"]["connected"] = isConnected; - doc["network"]["ssid"] = network->getStaSsid(); - doc["network"]["signalQuality"] = isConnected ? NetworkMgr::rssiToSignalQuality(network->getRssi()) : 0; - doc["network"]["channel"] = isConnected ? network->getStaChannel() : 0; - doc["network"]["ip"] = isConnected ? network->getStaIp().toString() : ""; - doc["network"]["subnet"] = isConnected ? network->getStaSubnet().toString() : ""; - doc["network"]["gateway"] = isConnected ? network->getStaGateway().toString() : ""; - doc["network"]["dns"] = isConnected ? network->getStaDns().toString() : ""; - - doc["build"]["version"] = BUILD_VERSION; - doc["build"]["date"] = __DATE__ " " __TIME__; - doc["build"]["env"] = BUILD_ENV; - - doc["heap"]["total"] = getTotalHeap(); - doc["heap"]["free"] = getFreeHeap(); - doc["heap"]["minFree"] = getFreeHeap(true); - doc["heap"]["maxFreeBlock"] = getMaxFreeBlockHeap(); - doc["heap"]["minMaxFreeBlock"] = getMaxFreeBlockHeap(true); + + auto docSystem = doc[FPSTR(S_SYSTEM)].to(); + docSystem[FPSTR(S_RESET_REASON)] = getResetReason(); + docSystem[FPSTR(S_UPTIME)] = millis() / 1000; + + auto docNetwork = doc[FPSTR(S_NETWORK)].to(); + docNetwork[FPSTR(S_HOSTNAME)] = networkSettings.hostname; + docNetwork[FPSTR(S_MAC)] = network->getStaMac(); + docNetwork[FPSTR(S_CONNECTED)] = isConnected; + docNetwork[FPSTR(S_SSID)] = network->getStaSsid(); + docNetwork[FPSTR(S_SIGNAL_QUALITY)] = isConnected ? NetworkMgr::rssiToSignalQuality(network->getRssi()) : 0; + docNetwork[FPSTR(S_CHANNEL)] = isConnected ? network->getStaChannel() : 0; + docNetwork[FPSTR(S_IP)] = isConnected ? network->getStaIp().toString() : ""; + docNetwork[FPSTR(S_SUBNET)] = isConnected ? network->getStaSubnet().toString() : ""; + docNetwork[FPSTR(S_GATEWAY)] = isConnected ? network->getStaGateway().toString() : ""; + docNetwork[FPSTR(S_DNS)] = isConnected ? network->getStaDns().toString() : ""; + + auto docBuild = doc[FPSTR(S_BUILD)].to(); + docBuild[FPSTR(S_VERSION)] = BUILD_VERSION; + docBuild[FPSTR(S_DATE)] = __DATE__ " " __TIME__; + docBuild[FPSTR(S_ENV)] = BUILD_ENV; + #ifdef ARDUINO_ARCH_ESP8266 + docBuild[FPSTR(S_CORE)] = ESP.getCoreVersion(); + docBuild[FPSTR(S_SDK)] = ESP.getSdkVersion(); + #elif ARDUINO_ARCH_ESP32 + docBuild[FPSTR(S_CORE)] = ESP.getCoreVersion(); + docBuild[FPSTR(S_SDK)] = ESP.getSdkVersion(); + #else + docBuild[FPSTR(S_CORE)] = 0; + docBuild[FPSTR(S_SDK)] = 0; + #endif + + auto docHeap = doc[FPSTR(S_HEAP)].to(); + docHeap[FPSTR(S_TOTAL)] = getTotalHeap(); + docHeap[FPSTR(S_FREE)] = getFreeHeap(); + docHeap[FPSTR(S_MIN_FREE)] = getFreeHeap(true); + docHeap[FPSTR(S_MAX_FREE_BLOCK)] = getMaxFreeBlockHeap(); + docHeap[FPSTR(S_MIN_MAX_FREE_BLOCK)] = getMaxFreeBlockHeap(true); + auto docChip = doc[FPSTR(S_CHIP)].to(); + #ifdef ARDUINO_ARCH_ESP8266 + docChip[FPSTR(S_MODEL)] = esp_is_8285() ? F("ESP8285") : F("ESP8266"); + docChip[FPSTR(S_REV)] = 0; + docChip[FPSTR(S_CORES)] = 1; + docChip[FPSTR(S_FREQ)] = ESP.getCpuFreqMHz(); + #elif ARDUINO_ARCH_ESP32 + docChip[FPSTR(S_MODEL)] = ESP.getChipModel(); + docChip[FPSTR(S_REV)] = ESP.getChipRevision(); + docChip[FPSTR(S_CORES)] = ESP.getChipCores(); + docChip[FPSTR(S_FREQ)] = ESP.getCpuFreqMHz(); + #else + docChip[FPSTR(S_MODEL)] = 0; + docChip[FPSTR(S_REV)] = 0; + docChip[FPSTR(S_CORES)] = 0; + docChip[FPSTR(S_FREQ)] = 0; + #endif + + auto docFlash = doc[FPSTR(S_FLASH)].to(); #ifdef ARDUINO_ARCH_ESP8266 - doc["build"]["core"] = ESP.getCoreVersion(); - doc["build"]["sdk"] = ESP.getSdkVersion(); - doc["chip"]["model"] = esp_is_8285() ? "ESP8285" : "ESP8266"; - doc["chip"]["rev"] = 0; - doc["chip"]["cores"] = 1; - doc["chip"]["freq"] = ESP.getCpuFreqMHz(); - doc["flash"]["size"] = ESP.getFlashChipSize(); - doc["flash"]["realSize"] = ESP.getFlashChipRealSize(); + docFlash[FPSTR(S_SIZE)] = ESP.getFlashChipSize(); + docFlash[FPSTR(S_REAL_SIZE)] = ESP.getFlashChipRealSize(); #elif ARDUINO_ARCH_ESP32 - doc["build"]["core"] = ESP.getCoreVersion(); - doc["build"]["sdk"] = ESP.getSdkVersion(); - doc["chip"]["model"] = ESP.getChipModel(); - doc["chip"]["rev"] = ESP.getChipRevision(); - doc["chip"]["cores"] = ESP.getChipCores(); - doc["chip"]["freq"] = ESP.getCpuFreqMHz(); - doc["flash"]["size"] = ESP.getFlashChipSize(); - doc["flash"]["realSize"] = doc["flash"]["size"]; + docFlash[FPSTR(S_SIZE)] = ESP.getFlashChipSize(); + docFlash[FPSTR(S_REAL_SIZE)] = docFlash[FPSTR(S_SIZE)]; #else - doc["build"]["core"] = 0; - doc["build"]["sdk"] = 0; - doc["chip"]["model"] = 0; - doc["chip"]["rev"] = 0; - doc["chip"]["cores"] = 0; - doc["chip"]["freq"] = 0; - doc["flash"]["size"] = 0; - doc["flash"]["realSize"] = 0; + docFlash[FPSTR(S_SIZE)] = 0; + docFlash[FPSTR(S_REAL_SIZE)] = 0; #endif doc.shrinkToFit(); - this->bufferedWebServer->send(200, "application/json", doc); + this->bufferedWebServer->send(200, F("application/json"), doc); }); - this->webServer->on("/api/debug", HTTP_GET, [this]() { + this->webServer->on(F("/api/debug"), HTTP_GET, [this]() { JsonDocument doc; - doc["build"]["version"] = BUILD_VERSION; - doc["build"]["date"] = __DATE__ " " __TIME__; - doc["build"]["env"] = BUILD_ENV; - doc["heap"]["total"] = getTotalHeap(); - doc["heap"]["free"] = getFreeHeap(); - doc["heap"]["minFree"] = getFreeHeap(true); - doc["heap"]["maxFreeBlock"] = getMaxFreeBlockHeap(); - doc["heap"]["minMaxFreeBlock"] = getMaxFreeBlockHeap(true); + + auto docBuild = doc[FPSTR(S_BUILD)].to(); + docBuild[FPSTR(S_VERSION)] = BUILD_VERSION; + docBuild[FPSTR(S_DATE)] = __DATE__ " " __TIME__; + docBuild[FPSTR(S_ENV)] = BUILD_ENV; + #ifdef ARDUINO_ARCH_ESP8266 + docBuild[FPSTR(S_CORE)] = ESP.getCoreVersion(); + docBuild[FPSTR(S_SDK)] = ESP.getSdkVersion(); + #elif ARDUINO_ARCH_ESP32 + docBuild[FPSTR(S_CORE)] = ESP.getCoreVersion(); + docBuild[FPSTR(S_SDK)] = ESP.getSdkVersion(); + #else + docBuild[FPSTR(S_CORE)] = 0; + docBuild[FPSTR(S_SDK)] = 0; + #endif + + auto docHeap = doc[FPSTR(S_HEAP)].to(); + docHeap[FPSTR(S_TOTAL)] = getTotalHeap(); + docHeap[FPSTR(S_FREE)] = getFreeHeap(); + docHeap[FPSTR(S_MIN_FREE)] = getFreeHeap(true); + docHeap[FPSTR(S_MAX_FREE_BLOCK)] = getMaxFreeBlockHeap(); + docHeap[FPSTR(S_MIN_MAX_FREE_BLOCK)] = getMaxFreeBlockHeap(true); + + auto docChip = doc[FPSTR(S_CHIP)].to(); + #ifdef ARDUINO_ARCH_ESP8266 + docChip[FPSTR(S_MODEL)] = esp_is_8285() ? F("ESP8285") : F("ESP8266"); + docChip[FPSTR(S_REV)] = 0; + docChip[FPSTR(S_CORES)] = 1; + docChip[FPSTR(S_FREQ)] = ESP.getCpuFreqMHz(); + #elif ARDUINO_ARCH_ESP32 + docChip[FPSTR(S_MODEL)] = ESP.getChipModel(); + docChip[FPSTR(S_REV)] = ESP.getChipRevision(); + docChip[FPSTR(S_CORES)] = ESP.getChipCores(); + docChip[FPSTR(S_FREQ)] = ESP.getCpuFreqMHz(); + #else + docChip[FPSTR(S_MODEL)] = 0; + docChip[FPSTR(S_REV)] = 0; + docChip[FPSTR(S_CORES)] = 0; + docChip[FPSTR(S_FREQ)] = 0; + #endif + + auto docFlash = doc[FPSTR(S_FLASH)].to(); + #ifdef ARDUINO_ARCH_ESP8266 + docFlash[FPSTR(S_SIZE)] = ESP.getFlashChipSize(); + docFlash[FPSTR(S_REAL_SIZE)] = ESP.getFlashChipRealSize(); + #elif ARDUINO_ARCH_ESP32 + docFlash[FPSTR(S_SIZE)] = ESP.getFlashChipSize(); + docFlash[FPSTR(S_REAL_SIZE)] = docFlash[FPSTR(S_SIZE)]; + #else + docFlash[FPSTR(S_SIZE)] = 0; + docFlash[FPSTR(S_REAL_SIZE)] = 0; + #endif #if defined(ARDUINO_ARCH_ESP32) auto reason = esp_reset_reason(); @@ -582,58 +815,30 @@ class PortalTask : public LeanTask { #else if (false) { #endif - doc["crash"]["reason"] = getResetReason(); - doc["crash"]["core"] = CrashRecorder::ext.core; - doc["crash"]["heap"] = CrashRecorder::ext.heap; - doc["crash"]["uptime"] = CrashRecorder::ext.uptime; + auto docCrash = doc[FPSTR(S_CRASH)].to(); + docCrash[FPSTR(S_REASON)] = getResetReason(); + docCrash[FPSTR(S_CORE)] = CrashRecorder::ext.core; + docCrash[FPSTR(S_HEAP)] = CrashRecorder::ext.heap; + docCrash[FPSTR(S_UPTIME)] = CrashRecorder::ext.uptime; if (CrashRecorder::backtrace.length > 0 && CrashRecorder::backtrace.length <= CrashRecorder::backtraceMaxLength) { String backtraceStr; arr2str(backtraceStr, CrashRecorder::backtrace.data, CrashRecorder::backtrace.length); - doc["crash"]["backtrace"]["data"] = backtraceStr; - doc["crash"]["backtrace"]["continues"] = CrashRecorder::backtrace.continues; + docCrash[FPSTR(S_BACKTRACE)][FPSTR(S_DATA)] = backtraceStr; + docCrash[FPSTR(S_BACKTRACE)][FPSTR(S_CONTINUES)] = CrashRecorder::backtrace.continues; } if (CrashRecorder::epc.length > 0 && CrashRecorder::epc.length <= CrashRecorder::epcMaxLength) { String epcStr; arr2str(epcStr, CrashRecorder::epc.data, CrashRecorder::epc.length); - doc["crash"]["epc"] = epcStr; + docCrash[FPSTR(S_EPC)] = epcStr; } } - #ifdef ARDUINO_ARCH_ESP8266 - doc["build"]["core"] = ESP.getCoreVersion(); - doc["build"]["sdk"] = ESP.getSdkVersion(); - doc["chip"]["model"] = esp_is_8285() ? "ESP8285" : "ESP8266"; - doc["chip"]["rev"] = 0; - doc["chip"]["cores"] = 1; - doc["chip"]["freq"] = ESP.getCpuFreqMHz(); - doc["flash"]["size"] = ESP.getFlashChipSize(); - doc["flash"]["realSize"] = ESP.getFlashChipRealSize(); - #elif ARDUINO_ARCH_ESP32 - doc["build"]["core"] = ESP.getCoreVersion(); - doc["build"]["sdk"] = ESP.getSdkVersion(); - doc["chip"]["model"] = ESP.getChipModel(); - doc["chip"]["rev"] = ESP.getChipRevision(); - doc["chip"]["cores"] = ESP.getChipCores(); - doc["chip"]["freq"] = ESP.getCpuFreqMHz(); - doc["flash"]["size"] = ESP.getFlashChipSize(); - doc["flash"]["realSize"] = doc["flash"]["size"]; - #else - doc["build"]["core"] = 0; - doc["build"]["sdk"] = 0; - doc["chip"]["model"] = 0; - doc["chip"]["rev"] = 0; - doc["chip"]["cores"] = 0; - doc["chip"]["freq"] = 0; - doc["flash"]["size"] = 0; - doc["flash"]["realSize"] = 0; - #endif - doc.shrinkToFit(); this->webServer->sendHeader(F("Content-Disposition"), F("attachment; filename=\"debug.json\"")); - this->bufferedWebServer->send(200, "application/json", doc, true); + this->bufferedWebServer->send(200, F("application/json"), doc, true); }); @@ -641,15 +846,15 @@ class PortalTask : public LeanTask { this->webServer->onNotFound([this]() { Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Page not found, uri: %s"), this->webServer->uri().c_str()); - const String uri = this->webServer->uri(); - if (uri.equals("/")) { - this->webServer->send(200, "text/plain", F("The file system is not flashed!")); + const String& uri = this->webServer->uri(); + if (uri.equals(F("/"))) { + this->webServer->send(200, F("text/plain"), F("The file system is not flashed!")); } else if (network->isApEnabled()) { this->onCaptivePortal(); } else { - this->webServer->send(404, "text/plain", F("Page not found")); + this->webServer->send(404, F("text/plain"), F("Page not found")); } }); @@ -668,7 +873,7 @@ class PortalTask : public LeanTask { Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Started: AP up or STA connected")); #ifdef ARDUINO_ARCH_ESP8266 - ::delay(0); + ::optimistic_yield(1000); #endif } else if (this->stateWebServer() && !network->isApEnabled() && !network->isStaEnabled()) { @@ -676,7 +881,7 @@ class PortalTask : public LeanTask { Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Stopped: AP and STA down")); #ifdef ARDUINO_ARCH_ESP8266 - ::delay(0); + ::optimistic_yield(1000); #endif } @@ -686,7 +891,7 @@ class PortalTask : public LeanTask { Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Started: AP up")); #ifdef ARDUINO_ARCH_ESP8266 - ::delay(0); + ::optimistic_yield(1000); #endif } else if (this->stateDnsServer() && (!network->isApEnabled() || !this->stateWebServer())) { @@ -694,18 +899,27 @@ class PortalTask : public LeanTask { Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Stopped: AP down")); #ifdef ARDUINO_ARCH_ESP8266 - ::delay(0); + ::optimistic_yield(1000); #endif } if (this->stateDnsServer()) { this->dnsServer->processNextRequest(); + #ifdef ARDUINO_ARCH_ESP8266 - ::delay(0); + ::optimistic_yield(1000); #endif } if (this->stateWebServer()) { + #ifdef ARDUINO_ARCH_ESP32 + // Fix ERR_CONNECTION_RESET for Chrome based browsers + auto& client = this->webServer->client(); + if (!client.getNoDelay()) { + client.setNoDelay(true); + } + #endif + this->webServer->handleClient(); } @@ -718,29 +932,35 @@ class PortalTask : public LeanTask { return !network->isApEnabled() && settings.portal.auth && strlen(settings.portal.password); } + bool isValidCredentials() { + return this->webServer->authenticate(settings.portal.login, settings.portal.password); + } + void onCaptivePortal() { - const String uri = this->webServer->uri(); + const String& uri = this->webServer->uri(); - if (uri.equals("/connecttest.txt")) { + if (uri.equals(F("/connecttest.txt"))) { this->webServer->sendHeader(F("Location"), F("http://logout.net")); this->webServer->send(302); Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Redirect to http://logout.net with 302 code")); - } else if (uri.equals("/wpad.dat")) { + } else if (uri.equals(F("/wpad.dat"))) { this->webServer->send(404); Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Send empty page with 404 code")); - } else if (uri.equals("/success.txt")) { + } else if (uri.equals(F("/success.txt"))) { this->webServer->send(200); Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Send empty page with 200 code")); } else { - String portalUrl = "http://" + network->getApIp().toString() + '/'; + String portalUrl = F("http://"); + portalUrl += network->getApIp().toString().c_str(); + portalUrl += '/'; - this->webServer->sendHeader("Location", portalUrl.c_str()); + this->webServer->sendHeader(F("Location"), portalUrl); this->webServer->send(302); Log.straceln(FPSTR(L_PORTAL_CAPTIVE), F("Redirect to portal page with 302 code")); diff --git a/src/RegulatorTask.h b/src/RegulatorTask.h index c26a66b..e749adf 100644 --- a/src/RegulatorTask.h +++ b/src/RegulatorTask.h @@ -10,9 +10,12 @@ class RegulatorTask : public LeanTask { RegulatorTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) {} protected: - float prevHeatingTarget = 0; - float prevEtResult = 0; - float prevPidResult = 0; + float prevHeatingTarget = 0.0f; + float prevEtResult = 0.0f; + float prevPidResult = 0.0f; + + bool indoorSensorsConnected = false; + //bool outdoorSensorsConnected = false; #if defined(ARDUINO_ARCH_ESP32) const char* getTaskName() override { @@ -29,20 +32,55 @@ class RegulatorTask : public LeanTask { #endif void loop() { - if (!settings.pid.enable && fabs(pidRegulator.integral) > 0.01f) { + if (vars.states.restarting || vars.states.upgrading) { + return; + } + + this->indoorSensorsConnected = Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::INDOOR_TEMP); + //this->outdoorSensorsConnected = Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::OUTDOOR_TEMP); + + if (settings.equitherm.enabled || settings.pid.enabled || settings.opentherm.nativeHeatingControl) { + vars.master.heating.indoorTempControl = true; + vars.master.heating.minTemp = THERMOSTAT_INDOOR_MIN_TEMP; + vars.master.heating.maxTemp = THERMOSTAT_INDOOR_MAX_TEMP; + + } else { + vars.master.heating.indoorTempControl = false; + vars.master.heating.minTemp = settings.heating.minTemp; + vars.master.heating.maxTemp = settings.heating.maxTemp; + } + + if (!settings.pid.enabled && fabsf(pidRegulator.integral) > 0.01f) { pidRegulator.integral = 0.0f; Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset")); } + this->turbo(); + this->hysteresis(); + + vars.master.heating.targetTemp = settings.heating.target; + vars.master.heating.setpointTemp = constrain( + this->getHeatingSetpointTemp(), + this->getHeatingMinSetpointTemp(), + this->getHeatingMaxSetpointTemp() + ); + + Sensors::setValueByType( + Sensors::Type::HEATING_SETPOINT_TEMP, vars.master.heating.setpointTemp, + Sensors::ValueType::PRIMARY, true, true + ); + } + + void turbo() { if (settings.heating.turbo) { - if (!settings.heating.enable || vars.states.emergency || !vars.sensors.indoor.connected) { + if (!settings.heating.enabled || vars.emergency.state || !this->indoorSensorsConnected) { settings.heating.turbo = false; - } else if (!settings.pid.enable && !settings.equitherm.enable) { + } else if (!settings.pid.enabled && !settings.equitherm.enabled) { settings.heating.turbo = false; - } else if (fabs(settings.heating.target - vars.temperatures.indoor) <= 1.0f) { + } else if (fabsf(settings.heating.target - vars.master.heating.indoorTemp) <= 1.0f) { settings.heating.turbo = false; } @@ -50,45 +88,69 @@ class RegulatorTask : public LeanTask { Log.sinfoln(FPSTR(L_REGULATOR), F("Turbo mode auto disabled")); } } + } + void hysteresis() { + bool useHyst = false; + if (settings.heating.hysteresis > 0.01f && this->indoorSensorsConnected) { + useHyst = settings.equitherm.enabled || settings.pid.enabled || settings.opentherm.nativeHeatingControl; + } - float newTemp = vars.states.emergency - ? settings.emergency.target - : this->getNormalModeTemp(); + if (useHyst) { + if (!vars.master.heating.blocking && vars.master.heating.indoorTemp - settings.heating.target + 0.0001f >= settings.heating.hysteresis) { + vars.master.heating.blocking = true; - // Limits - newTemp = constrain( - newTemp, - !settings.opentherm.nativeHeatingControl ? settings.heating.minTemp : THERMOSTAT_INDOOR_MIN_TEMP, - !settings.opentherm.nativeHeatingControl ? settings.heating.maxTemp : THERMOSTAT_INDOOR_MAX_TEMP - ); + } else if (vars.master.heating.blocking && vars.master.heating.indoorTemp - settings.heating.target - 0.0001f <= -(settings.heating.hysteresis)) { + vars.master.heating.blocking = false; + } - if (fabs(vars.parameters.heatingSetpoint - newTemp) > 0.09f) { - vars.parameters.heatingSetpoint = newTemp; + } else if (vars.master.heating.blocking) { + vars.master.heating.blocking = false; } } + inline float getHeatingMinSetpointTemp() { + return settings.opentherm.nativeHeatingControl + ? vars.master.heating.minTemp + : settings.heating.minTemp; + } + + inline float getHeatingMaxSetpointTemp() { + return settings.opentherm.nativeHeatingControl + ? vars.master.heating.maxTemp + : settings.heating.maxTemp; + } - float getNormalModeTemp() { + float getHeatingSetpointTemp() { float newTemp = 0; - if (fabs(prevHeatingTarget - settings.heating.target) > 0.0001f) { + if (fabsf(prevHeatingTarget - settings.heating.target) > 0.0001f) { prevHeatingTarget = settings.heating.target; Log.sinfoln(FPSTR(L_REGULATOR), F("New target: %.2f"), settings.heating.target); - /*if (settings.pid.enable) { + /*if (settings.pid.enabled) { pidRegulator.integral = 0.0f; Log.sinfoln(FPSTR(L_REGULATOR_PID), F("Integral sum has been reset")); }*/ } + if (vars.emergency.state) { + return settings.emergency.target; + + } else if (settings.opentherm.nativeHeatingControl) { + return settings.heating.target; + + } else if (!settings.equitherm.enabled && !settings.pid.enabled) { + return settings.heating.target; + } + // if use equitherm - if (settings.equitherm.enable) { + if (settings.equitherm.enabled) { unsigned short minTemp = settings.heating.minTemp; unsigned short maxTemp = settings.heating.maxTemp; float targetTemp = settings.heating.target; - float indoorTemp = vars.temperatures.indoor; - float outdoorTemp = vars.temperatures.outdoor; + float indoorTemp = vars.master.heating.indoorTemp; + float outdoorTemp = vars.master.heating.outdoorTemp; if (settings.system.unitSystem == UnitSystem::IMPERIAL) { minTemp = f2c(minTemp); @@ -98,7 +160,7 @@ class RegulatorTask : public LeanTask { outdoorTemp = f2c(outdoorTemp); } - if (!vars.sensors.indoor.connected || settings.pid.enable) { + if (!this->indoorSensorsConnected || settings.pid.enabled) { etRegulator.Kt = 0.0f; etRegulator.indoorTemp = 0.0f; @@ -118,7 +180,7 @@ class RegulatorTask : public LeanTask { etResult = c2f(etResult); } - if (fabs(prevEtResult - etResult) > 0.09f) { + if (fabsf(prevEtResult - etResult) > 0.09f) { prevEtResult = etResult; newTemp += etResult; @@ -130,18 +192,18 @@ class RegulatorTask : public LeanTask { } // if use pid - if (settings.pid.enable) { + if (settings.pid.enabled) { //if (vars.parameters.heatingEnabled) { - if (settings.heating.enable && vars.sensors.indoor.connected) { + if (settings.heating.enabled && this->indoorSensorsConnected) { pidRegulator.Kp = settings.heating.turbo ? 0.0f : settings.pid.p_factor; pidRegulator.Kd = settings.pid.d_factor; pidRegulator.setLimits(settings.pid.minTemp, settings.pid.maxTemp); pidRegulator.setDt(settings.pid.dt * 1000u); - pidRegulator.input = vars.temperatures.indoor; + pidRegulator.input = vars.master.heating.indoorTemp; pidRegulator.setpoint = settings.heating.target; - if (fabs(pidRegulator.Ki - settings.pid.i_factor) >= 0.0001f) { + if (fabsf(pidRegulator.Ki - settings.pid.i_factor) >= 0.0001f) { pidRegulator.Ki = settings.pid.i_factor; pidRegulator.integral = 0.0f; pidRegulator.getResultNow(); @@ -150,7 +212,7 @@ class RegulatorTask : public LeanTask { } float pidResult = pidRegulator.getResultTimer(); - if (fabs(prevPidResult - pidResult) > 0.09f) { + if (fabsf(prevPidResult - pidResult) > 0.09f) { prevPidResult = pidResult; newTemp += pidResult; @@ -167,19 +229,14 @@ class RegulatorTask : public LeanTask { } // Turbo mode - if (settings.heating.turbo && (settings.equitherm.enable || settings.pid.enable)) { + if (settings.heating.turbo && (settings.equitherm.enabled || settings.pid.enabled)) { newTemp += constrain( - settings.heating.target - vars.temperatures.indoor, + settings.heating.target - vars.master.heating.indoorTemp, -3.0f, 3.0f ) * settings.heating.turboFactor; } - // default temp, manual mode - if (!settings.equitherm.enable && !settings.pid.enable) { - newTemp = settings.heating.target; - } - return newTemp; } }; diff --git a/src/Sensors.h b/src/Sensors.h new file mode 100644 index 0000000..dadb63e --- /dev/null +++ b/src/Sensors.h @@ -0,0 +1,464 @@ +#pragma once + +class Sensors { +protected: + static uint8_t maxSensors; + +public: + enum class Type : uint8_t { + OT_OUTDOOR_TEMP = 0, + OT_HEATING_TEMP = 1, + OT_HEATING_RETURN_TEMP = 2, + OT_DHW_TEMP = 3, + OT_DHW_TEMP2 = 4, + OT_DHW_FLOW_RATE = 5, + OT_CH2_TEMP = 6, + OT_EXHAUST_TEMP = 7, + OT_HEAT_EXCHANGER_TEMP = 8, + OT_PRESSURE = 9, + OT_MODULATION_LEVEL = 10, + OT_CURRENT_POWER = 11, + OT_EXHAUST_CO2 = 12, + OT_EXHAUST_FAN_SPEED = 13, + OT_SUPPLY_FAN_SPEED = 14, + OT_SOLAR_STORAGE_TEMP = 15, + OT_SOLAR_COLLECTOR_TEMP = 16, + OT_FAN_SPEED_SETPOINT = 17, + OT_FAN_SPEED_CURRENT = 18, + + NTC_10K_TEMP = 50, + DALLAS_TEMP = 51, + BLUETOOTH = 52, + + HEATING_SETPOINT_TEMP = 253, + MANUAL = 254, + NOT_CONFIGURED = 255 + }; + + enum class Purpose : uint8_t { + OUTDOOR_TEMP = 0, + INDOOR_TEMP = 1, + HEATING_TEMP = 2, + HEATING_RETURN_TEMP = 3, + DHW_TEMP = 4, + DHW_RETURN_TEMP = 5, + DHW_FLOW_RATE = 6, + EXHAUST_TEMP = 7, + MODULATION_LEVEL = 8, + + POWER_FACTOR = 248, + POWER = 249, + FAN_SPEED = 250, + CO2 = 251, + PRESSURE = 252, + HUMIDITY = 253, + TEMPERATURE = 254, + NOT_CONFIGURED = 255 + }; + + enum class ValueType : uint8_t { + PRIMARY = 0, + TEMPERATURE = 0, + HUMIDITY = 1, + BATTERY = 2, + RSSI = 3 + }; + + typedef struct { + bool enabled = false; + char name[33]; + Purpose purpose = Purpose::NOT_CONFIGURED; + Type type = Type::NOT_CONFIGURED; + uint8_t gpio = GPIO_IS_NOT_CONFIGURED; + uint8_t address[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + float offset = 0.0f; + float factor = 1.0f; + bool filtering = false; + float filteringFactor = 0.15f; + } Settings; + + typedef struct { + bool connected = false; + unsigned long activityTime = 0; + uint8_t signalQuality = 100; + //float raw[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + float values[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + } Result; + + + static Settings* settings; + static Result* results; + + static inline void setMaxSensors(uint8_t value) { + maxSensors = value; + } + + static inline uint8_t getMaxSensors() { + return maxSensors; + } + + static uint8_t getMaxSensorId() { + uint8_t maxSensors = getMaxSensors(); + return maxSensors > 1 ? (maxSensors - 1) : 0; + } + + static inline bool isValidSensorId(const uint8_t id) { + return id >= 0 && id <= getMaxSensorId(); + } + + static inline bool isValidValueId(const uint8_t id) { + return id >= (uint8_t) ValueType::TEMPERATURE && id <= (uint8_t) ValueType::RSSI; + } + + static bool hasEnabledAndValid(const uint8_t id) { + if (!isValidSensorId(id) || !settings[id].enabled) { + return false; + } + + if (settings[id].type == Type::NOT_CONFIGURED || settings[id].purpose == Purpose::NOT_CONFIGURED) { + return false; + } + + return true; + } + + static uint8_t getAmountByType(Type type, bool onlyEnabled = false) { + if (settings == nullptr) { + return 0; + } + + uint8_t amount = 0; + for (uint8_t id = 0; id < getMaxSensorId(); id++) { + if (settings[id].type == type && (!onlyEnabled || settings[id].enabled)) { + amount++; + } + } + + return amount; + } + + static int16_t getIdByName(const char* name) { + if (settings == nullptr) { + return 0; + } + + for (uint8_t id = 0; id < getMaxSensorId(); id++) { + if (strcmp(settings[id].name, name) == 0) { + return id; + } + } + + return -1; + } + + static int16_t getIdByObjectId(const char* objectId) { + if (settings == nullptr) { + return 0; + } + + String refObjectId; + for (uint8_t id = 0; id < getMaxSensorId(); id++) { + Sensors::makeObjectId(refObjectId, settings[id].name); + if (refObjectId.equals(objectId)) { + return id; + } + } + + return -1; + } + + static bool setValueById(const uint8_t sensorId, float value, const ValueType valueType, const bool updateActivityTime = false, const bool markConnected = false) { + if (settings == nullptr || results == nullptr) { + return false; + } + + uint8_t valueId = (uint8_t) valueType; + if (!isValidSensorId(sensorId) || !isValidValueId(valueId)) { + return false; + } + + auto& sSensor = settings[sensorId]; + auto& rSensor = results[sensorId]; + + float compensatedValue = value; + if (sSensor.type == Type::HEATING_SETPOINT_TEMP || sSensor.type == Type::MANUAL) { + rSensor.values[valueId] = compensatedValue; + + } else { + if (valueType == ValueType::PRIMARY) { + if (fabsf(sSensor.factor) > 0.001f) { + compensatedValue *= sSensor.factor; + } + + if (fabsf(sSensor.offset) > 0.001f) { + compensatedValue += sSensor.offset; + } + + } else if (valueType == ValueType::RSSI) { + if (sSensor.type == Type::BLUETOOTH) { + rSensor.signalQuality = Sensors::bluetoothRssiToQuality(value); + } + } + + if (sSensor.filtering && fabsf(rSensor.values[valueId]) >= 0.1f) { + rSensor.values[valueId] += (compensatedValue - rSensor.values[valueId]) * sSensor.filteringFactor; + + } else { + rSensor.values[valueId] = compensatedValue; + } + } + + if (updateActivityTime) { + rSensor.activityTime = millis(); + } + + if (markConnected && !rSensor.connected) { + rSensor.connected = true; + + Log.snoticeln( + FPSTR(L_SENSORS), F("#%hhu '%s' new status: CONNECTED"), + sensorId, sSensor.name + ); + } + + Log.snoticeln( + FPSTR(L_SENSORS), F("#%hhu '%s' new value %hhu: %.2f, compensated: %.2f, raw: %.2f"), + sensorId, sSensor.name, valueId, rSensor.values[valueId], compensatedValue, value + ); + + return true; + } + + static uint8_t setValueByType(Type type, float value, const ValueType valueType, const bool updateActivityTime = false, const bool markConnected = false) { + if (settings == nullptr) { + return 0; + } + + uint8_t updated = 0; + + // read sensors data for current instance + for (uint8_t sensorId = 0; sensorId < getMaxSensorId(); sensorId++) { + auto& sSensor = settings[sensorId]; + + // only target & valid sensors + if (!sSensor.enabled || sSensor.type != type) { + continue; + } + + if (setValueById(sensorId, value, valueType, updateActivityTime, markConnected)) { + updated++; + } + } + + return updated; + } + + static bool getConnectionStatusById(const uint8_t sensorId) { + if (settings == nullptr || results == nullptr) { + return false; + } + + if (!isValidSensorId(sensorId)) { + return false; + } + + return results[sensorId].connected; + } + + static bool setConnectionStatusById(const uint8_t sensorId, const bool status, const bool updateActivityTime = true) { + if (settings == nullptr || results == nullptr) { + return false; + } + + if (!isValidSensorId(sensorId)) { + return false; + } + + auto& sSensor = settings[sensorId]; + auto& rSensor = results[sensorId]; + + if (rSensor.connected != status) { + Log.snoticeln( + FPSTR(L_SENSORS), F("#%hhu '%s' new status: %s"), + sensorId, sSensor.name, status ? F("CONNECTED") : F("DISCONNECTED") + ); + + rSensor.connected = status; + } + + if (updateActivityTime) { + rSensor.activityTime = millis(); + } + + return true; + } + + static uint8_t setConnectionStatusByType(Type type, const bool status, const bool updateActivityTime = true) { + if (settings == nullptr) { + return 0; + } + + uint8_t updated = 0; + + // read sensors data for current instance + for (uint8_t sensorId = 0; sensorId < getMaxSensorId(); sensorId++) { + auto& sSensor = settings[sensorId]; + + // only target & valid sensors + if (!sSensor.enabled || sSensor.type != type) { + continue; + } + + if (setConnectionStatusById(sensorId, status, updateActivityTime)) { + updated++; + } + } + + return updated; + } + + static float getMeanValueByPurpose(Purpose purpose, const ValueType valueType, bool onlyConnected = true) { + if (settings == nullptr || results == nullptr) { + return 0; + } + + uint8_t valueId = (uint8_t) valueType; + if (!isValidValueId(valueId)) { + return false; + } + + float value = 0.0f; + uint8_t amount = 0; + + for (uint8_t id = 0; id < getMaxSensorId(); id++) { + auto& sSensor = settings[id]; + auto& rSensor = results[id]; + + if (sSensor.purpose == purpose && (!onlyConnected || rSensor.connected)) { + value += rSensor.values[valueId]; + amount++; + } + } + + if (!amount) { + return 0.0f; + + } else if (amount == 1) { + return value; + + } else { + return value / amount; + } + } + + static bool existsConnectedSensorsByPurpose(Purpose purpose) { + if (settings == nullptr || results == nullptr) { + return 0; + } + + for (uint8_t id = 0; id < getMaxSensorId(); id++) { + if (settings[id].purpose == purpose && results[id].connected) { + return true; + } + } + + return false; + } + + static String& cleanName(String& value, char space = ' ') { + // only valid symbols + for (uint8_t pos = 0; pos < value.length(); pos++) { + char symbol = value.charAt(pos); + + // 0..9 + if (symbol >= 48 && symbol <= 57) { + continue; + } + + // A..Z + if (symbol >= 65 && symbol <= 90) { + continue; + } + + // a..z + if (symbol >= 97 && symbol <= 122) { + continue; + } + + // _- + if (symbol == 95 || symbol == 45 || symbol == space) { + continue; + } + + value.setCharAt(pos, space); + } + + value.trim(); + + return value; + } + + template + static String cleanName(T value, char space = ' ') { + String res = value; + return cleanName(res, space); + } + + template + static String& makeObjectId(String& res, T value, char separator = '_') { + res = value; + cleanName(res); + res.toLowerCase(); + res.replace(' ', separator); + + return res; + } + + template + static String makeObjectId(T value, char separator = '_') { + String res; + makeObjectId(res, value, separator); + + return res; + } + + template + static String& makeObjectIdWithSuffix(String& res, TV value, TS suffix, char separator = '_') { + res.clear(); + makeObjectId(res, value, separator); + res += separator; + res += suffix; + + return res; + } + + template + static String makeObjectIdWithSuffix(TV value, TS suffix, char separator = '_') { + String res; + makeObjectIdWithSuffix(res, value, suffix, separator); + return res; + } + + template + static String& makeObjectIdWithPrefix(String& res, TV value, TP prefix, char separator = '_') { + res = prefix; + res += separator; + res += makeObjectId(value, separator).c_str(); + + return res; + } + + template + static String makeObjectIdWithPrefix(TV value, TP prefix, char separator = '_') { + String res; + return makeObjectIdWithPrefix(res, value, prefix, separator); + } + + static uint8_t bluetoothRssiToQuality(int rssi) { + return constrain(map(rssi, -110, -50, 0, 100), 0, 100);; + } +}; + +uint8_t Sensors::maxSensors = 0; +Sensors::Settings* Sensors::settings = nullptr; +Sensors::Result* Sensors::results = nullptr; \ No newline at end of file diff --git a/src/SensorsTask.h b/src/SensorsTask.h index 0f9d81c..892112a 100644 --- a/src/SensorsTask.h +++ b/src/SensorsTask.h @@ -1,3 +1,4 @@ +#include #include #include @@ -5,50 +6,40 @@ #include #endif +extern FileData fsSensorsSettings; + class SensorsTask : public LeanTask { public: SensorsTask(bool _enabled = false, unsigned long _interval = 0) : LeanTask(_enabled, _interval) { - this->oneWireOutdoorSensor = new OneWire(); - this->outdoorSensor = new DallasTemperature(this->oneWireOutdoorSensor); - this->outdoorSensor->setWaitForConversion(false); - - this->oneWireIndoorSensor = new OneWire(); - this->indoorSensor = new DallasTemperature(this->oneWireIndoorSensor); - this->indoorSensor->setWaitForConversion(false); + this->owInstances.reserve(2); + this->dallasInstances.reserve(2); + this->dallasSearchTime.reserve(2); + this->dallasPolling.reserve(2); + this->dallasLastPollingTime.reserve(2); } ~SensorsTask() { - delete this->outdoorSensor; - delete this->oneWireOutdoorSensor; - delete this->indoorSensor; - delete this->oneWireIndoorSensor; + this->dallasInstances.clear(); + this->owInstances.clear(); + this->dallasSearchTime.clear(); + this->dallasPolling.clear(); + this->dallasLastPollingTime.clear(); } protected: - OneWire* oneWireOutdoorSensor = nullptr; - OneWire* oneWireIndoorSensor = nullptr; - - DallasTemperature* outdoorSensor = nullptr; - DallasTemperature* indoorSensor = nullptr; - - bool initOutdoorSensor = false; - unsigned long initOutdoorSensorTime = 0; - unsigned long startOutdoorConversionTime = 0; - float filteredOutdoorTemp = 0; - float prevFilteredOutdoorTemp = 0; - - bool initIndoorSensor = false; - unsigned long initIndoorSensorTime = 0; - unsigned long startIndoorConversionTime = 0; - float filteredIndoorTemp = 0; - float prevFilteredIndoorTemp = 0; + const unsigned int disconnectedTimeout = 120000; + const unsigned short dallasSearchInterval = 60000; + const unsigned short dallasPollingInterval = 10000; + const unsigned short globalPollingInterval = 15000; + + std::unordered_map owInstances; + std::unordered_map dallasInstances; + std::unordered_map dallasSearchTime; + std::unordered_map dallasPolling; + std::unordered_map dallasLastPollingTime; + unsigned long globalLastPollingTime = 0; #if defined(ARDUINO_ARCH_ESP32) - #if USE_BLE - unsigned long outdoorConnectedTime = 0; - unsigned long indoorConnectedTime = 0; - #endif - const char* getTaskName() override { return "Sensors"; } @@ -68,102 +59,409 @@ class SensorsTask : public LeanTask { #endif void loop() { - #if USE_BLE - if (!NimBLEDevice::getInitialized() && millis() > 5000) { - Log.sinfoln(FPSTR(L_SENSORS_BLE), F("Init BLE")); - BLEDevice::init(""); - NimBLEDevice::setPower(ESP_PWR_LVL_P9); + if (vars.states.restarting || vars.states.upgrading) { + return; } - #endif - if (settings.sensors.outdoor.type == SensorType::DS18B20 && GPIO_IS_VALID(settings.sensors.outdoor.gpio)) { - outdoorDallasSensor(); + if (isPollingDallasSensors()) { + pollingDallasSensors(false); + this->yield(); } - #if USE_BLE - else if (settings.sensors.outdoor.type == SensorType::BLUETOOTH) { - bool connected = this->bluetoothSensor( - BLEAddress(settings.sensors.outdoor.bleAddress), - &vars.sensors.outdoor.rssi, - &this->filteredOutdoorTemp, - &vars.sensors.outdoor.humidity, - &vars.sensors.outdoor.battery - ); - if (connected) { - this->outdoorConnectedTime = millis(); - vars.sensors.outdoor.connected = true; + if (millis() - this->globalLastPollingTime > this->globalPollingInterval) { + cleanDallasInstances(); + makeDallasInstances(); + this->yield(); + + searchDallasSensors(); + fillingAddressesDallasSensors(); + this->yield(); + + pollingDallasSensors(); + this->yield(); + + pollingNtcSensors(); + this->yield(); - } else if (millis() - this->outdoorConnectedTime > 60000) { - vars.sensors.outdoor.connected = false; + pollingBleSensors(); + this->yield(); + + this->globalLastPollingTime = millis(); + } + + updateConnectionStatus(); + updateMasterValues(); + } + + void updateMasterValues() { + vars.master.heating.outdoorTemp = Sensors::getMeanValueByPurpose(Sensors::Purpose::OUTDOOR_TEMP, Sensors::ValueType::PRIMARY); + vars.master.heating.indoorTemp = Sensors::getMeanValueByPurpose(Sensors::Purpose::INDOOR_TEMP, Sensors::ValueType::PRIMARY); + + vars.master.heating.currentTemp = Sensors::getMeanValueByPurpose(Sensors::Purpose::HEATING_TEMP, Sensors::ValueType::PRIMARY); + vars.master.heating.returnTemp = Sensors::getMeanValueByPurpose(Sensors::Purpose::HEATING_RETURN_TEMP, Sensors::ValueType::PRIMARY); + + vars.master.dhw.currentTemp = Sensors::getMeanValueByPurpose(Sensors::Purpose::DHW_TEMP, Sensors::ValueType::PRIMARY); + vars.master.dhw.returnTemp = Sensors::getMeanValueByPurpose(Sensors::Purpose::DHW_RETURN_TEMP, Sensors::ValueType::PRIMARY); + } + + void makeDallasInstances() { + for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { + auto& sSensor = Sensors::settings[sensorId]; + + if (!sSensor.enabled || sSensor.type != Sensors::Type::DALLAS_TEMP || sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) { + continue; + + } else if (this->dallasInstances.count(sSensor.gpio)) { + // no need to make instances + continue; } + + auto& owInstance = this->owInstances[sSensor.gpio]; + owInstance.begin(sSensor.gpio); + owInstance.reset(); + + this->dallasSearchTime[sSensor.gpio] = 0; + this->dallasPolling[sSensor.gpio] = false; + this->dallasLastPollingTime[sSensor.gpio] = 0; + + auto& instance = this->dallasInstances[sSensor.gpio]; + instance.setOneWire(&owInstance); + instance.setWaitForConversion(false); + + Log.sinfoln(FPSTR(L_SENSORS_DALLAS), F("Started on GPIO %hhu"), sSensor.gpio); } - #endif + } + + void cleanDallasInstances() { + // for (auto& [gpio, instance] : this->dallasInstances) { + auto it = this->dallasInstances.begin(); + while (it != this->dallasInstances.end()) { + auto gpio = it->first; + bool instanceUsed = false; + + for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { + auto& sSensor = Sensors::settings[sensorId]; + + if (!sSensor.enabled || sSensor.type != Sensors::Type::DALLAS_TEMP || sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) { + continue; + } + + if (Sensors::settings[sensorId].gpio == gpio) { + instanceUsed = true; + break; + } + } + + if (!instanceUsed) { + it = this->dallasInstances.erase(it); + this->owInstances.erase(gpio); + this->dallasSearchTime.erase(gpio); + this->dallasPolling.erase(gpio); + this->dallasLastPollingTime.erase(gpio); - if (settings.sensors.indoor.type == SensorType::DS18B20 && GPIO_IS_VALID(settings.sensors.indoor.gpio)) { - indoorDallasSensor(); + Log.sinfoln(FPSTR(L_SENSORS_DALLAS), F("Stopped on GPIO %hhu"), gpio); + continue; + } + + it++; } - #if USE_BLE - else if (settings.sensors.indoor.type == SensorType::BLUETOOTH) { - bool connected = this->bluetoothSensor( - BLEAddress(settings.sensors.indoor.bleAddress), - &vars.sensors.indoor.rssi, - &this->filteredIndoorTemp, - &vars.sensors.indoor.humidity, - &vars.sensors.indoor.battery - ); + } + + void searchDallasSensors() { + // search sensors on bus + for (auto& [gpio, instance] : this->dallasInstances) { + // do not search if polling! + if (this->dallasPolling[gpio]) { + continue; + } - if (connected) { - this->indoorConnectedTime = millis(); - vars.sensors.indoor.connected = true; + if (millis() - this->dallasSearchTime[gpio] > this->dallasSearchInterval) { + this->dallasSearchTime[gpio] = millis(); + instance.begin(); - } else if (millis() - this->indoorConnectedTime > 60000) { - vars.sensors.indoor.connected = false; + Log.straceln( + FPSTR(L_SENSORS_DALLAS), + F("GPIO %hhu, devices on bus: %hhu, DS18* devices: %hhu"), + gpio, instance.getDeviceCount(), instance.getDS18Count() + ); } } - #endif + } - // convert - if (fabs(this->prevFilteredOutdoorTemp - this->filteredOutdoorTemp) >= 0.1f) { - float newTemp = settings.sensors.outdoor.offset; - if (settings.system.unitSystem == UnitSystem::METRIC) { - newTemp += this->filteredOutdoorTemp; + void fillingAddressesDallasSensors() { + // check & filling sensors address + for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { + auto& sSensor = Sensors::settings[sensorId]; + + if (!sSensor.enabled || sSensor.type != Sensors::Type::DALLAS_TEMP || sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) { + continue; + + } else if (!this->dallasInstances.count(sSensor.gpio)) { + continue; + } - } else if (settings.system.unitSystem == UnitSystem::IMPERIAL) { - newTemp += c2f(this->filteredOutdoorTemp); + // do nothing if address not empty + if (!isEmptyAddress(sSensor.address)) { + continue; } - if (fabs(vars.temperatures.outdoor - newTemp) > 0.099f) { - vars.temperatures.outdoor = newTemp; - Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("New temp: %f"), vars.temperatures.outdoor); + // do nothing if polling + if (this->dallasPolling[sSensor.gpio]) { + continue; + } + + auto& instance = this->dallasInstances[sSensor.gpio]; + DeviceAddress devAddr; + for (uint8_t devId = 0; devId < instance.getDeviceCount(); devId++) { + if (!instance.getAddress(devAddr, devId)) { + continue; + } + + bool freeAddress = true; + + // checking address usage + for (uint8_t checkingSensorId = 0; checkingSensorId <= Sensors::getMaxSensorId(); checkingSensorId++) { + auto& sCheckingSensor = Sensors::settings[checkingSensorId]; + if (sCheckingSensor.type != Sensors::Type::DALLAS_TEMP || checkingSensorId == sensorId) { + continue; + } + + if (sCheckingSensor.gpio != sSensor.gpio || isEmptyAddress(sCheckingSensor.address)) { + continue; + } + + if (isEqualAddress(sCheckingSensor.address, devAddr)) { + freeAddress = false; + break; + } + } + + // address already in use + if (!freeAddress) { + continue; + } + + // set address + for (uint8_t i = 0; i < 8; i++) { + sSensor.address[i] = devAddr[i]; + } + + fsSensorsSettings.update(); + Log.straceln( + FPSTR(L_SENSORS_DALLAS), F("GPIO %hhu, sensor #%hhu '%s', set address: %hhX:%hhX:%hhX:%hhX:%hhX:%hhX:%hhX:%hhX"), + sSensor.gpio, sensorId, sSensor.name, + sSensor.address[0], sSensor.address[1], sSensor.address[2], sSensor.address[3], + sSensor.address[4], sSensor.address[5], sSensor.address[6], sSensor.address[7] + ); + + break; } + } + } - this->prevFilteredOutdoorTemp = this->filteredOutdoorTemp; + bool isPollingDallasSensors() { + for (auto& [gpio, instance] : this->dallasInstances) { + if (this->dallasPolling.count(gpio) && this->dallasPolling[gpio]) { + return true; + } } - if (fabs(this->prevFilteredIndoorTemp - this->filteredIndoorTemp) > 0.1f) { - float newTemp = settings.sensors.indoor.offset; - if (settings.system.unitSystem == UnitSystem::METRIC) { - newTemp += this->filteredIndoorTemp; + return false; + } + + void pollingDallasSensors(bool newPolling = true) { + for (auto& [gpio, instance] : this->dallasInstances) { + unsigned long ts = millis(); + + if (this->dallasPolling[gpio]) { + auto minPollingTime = instance.millisToWaitForConversion(12); + unsigned long estimatePollingTime = ts - this->dallasLastPollingTime[gpio]; + + // check conversion time + if (estimatePollingTime < minPollingTime) { + continue; + } + + // check conversion + bool conversionComplete = instance.isConversionComplete(); + if (!conversionComplete) { + if (estimatePollingTime > (minPollingTime * 2)) { + this->dallasPolling[gpio] = false; + + Log.swarningln(FPSTR(L_SENSORS_DALLAS), F("GPIO %hhu, timeout receiving data"), gpio); + } + + continue; + } + + // read sensors data for current instance + for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { + auto& sSensor = Sensors::settings[sensorId]; + + // only target & valid sensors + if (!sSensor.enabled || sSensor.type != Sensors::Type::DALLAS_TEMP || sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) { + continue; + + } else if (sSensor.gpio != gpio || isEmptyAddress(sSensor.address)) { + continue; + } + + auto& rSensor = Sensors::results[sensorId]; + float value = instance.getTempC(sSensor.address); + if (value == DEVICE_DISCONNECTED_C) { + Log.swarningln( + FPSTR(L_SENSORS_DALLAS), F("GPIO %hhu, sensor #%hhu '%s': failed receiving data"), + sSensor.gpio, sensorId, sSensor.name + ); + + if (rSensor.signalQuality > 0) { + rSensor.signalQuality--; + } + + continue; + } + + Log.straceln( + FPSTR(L_SENSORS_DALLAS), F("GPIO %hhu, sensor #%hhu '%s', received data: %.2f"), + sSensor.gpio, sensorId, sSensor.name, value + ); + + if (rSensor.signalQuality < 100) { + rSensor.signalQuality++; + } + + // set sensor value + Sensors::setValueById(sensorId, value, Sensors::ValueType::TEMPERATURE, true, true); + } + + // reset polling flag + this->dallasPolling[gpio] = false; + + } else if (newPolling) { + auto estimateLastPollingTime = ts - this->dallasLastPollingTime[gpio]; + + // check last polling time + if (estimateLastPollingTime < this->dallasPollingInterval) { + continue; + } + + // check sensors on bus + if (!instance.getDeviceCount()) { + for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { + auto& sSensor = Sensors::settings[sensorId]; + + // only target & valid sensors + if (!sSensor.enabled || sSensor.type != Sensors::Type::DALLAS_TEMP || sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) { + continue; + + } else if (sSensor.gpio != gpio || isEmptyAddress(sSensor.address)) { + continue; + } + + auto& rSensor = Sensors::results[sensorId]; + if (rSensor.signalQuality > 0) { + rSensor.signalQuality--; + } + } + + continue; + } + + // start polling + instance.setResolution(12); + instance.requestTemperatures(); + this->dallasPolling[gpio] = true; + this->dallasLastPollingTime[gpio] = ts; - } else if (settings.system.unitSystem == UnitSystem::IMPERIAL) { - newTemp += c2f(this->filteredIndoorTemp); + Log.straceln(FPSTR(L_SENSORS_DALLAS), F("GPIO %hhu, polling..."), gpio); } + } + } - if (fabs(vars.temperatures.indoor - newTemp) > 0.099f) { - vars.temperatures.indoor = newTemp; - Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("New temp: %f"), vars.temperatures.indoor); + void pollingBleSensors() { + #if USE_BLE + if (!NimBLEDevice::getInitialized() && millis() > 5000) { + Log.sinfoln(FPSTR(L_SENSORS_BLE), F("Initialized")); + BLEDevice::init(""); + NimBLEDevice::setPower(ESP_PWR_LVL_P9); + } + + for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { + auto& sSensor = Sensors::settings[sensorId]; + + if (!sSensor.enabled || sSensor.type != Sensors::Type::BLUETOOTH || sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) { + continue; } - this->prevFilteredIndoorTemp = this->filteredIndoorTemp; + connectToBleDevice(sensorId); } + #endif } -#if USE_BLE - bool bluetoothSensor(const BLEAddress& address, int8_t* const pRssi, float* const pTemperature, float* const pHumidity = nullptr, float* const pBattery = nullptr) { + void pollingNtcSensors() { + for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { + auto& sSensor = Sensors::settings[sensorId]; + + if (!sSensor.enabled || sSensor.type != Sensors::Type::NTC_10K_TEMP || sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) { + continue; + } + + #ifdef ARDUINO_ARCH_ESP32 + const auto value = analogReadMilliVolts(sSensor.gpio); + #else + const auto value = analogRead(sSensor.gpio) / 1023 * DEFAULT_NTC_VREF; + #endif + + if (value < DEFAULT_NTC_VLOW_TRESHOLD || value > DEFAULT_NTC_VHIGH_TRESHOLD) { + if (Sensors::getConnectionStatusById(sensorId)) { + Sensors::setConnectionStatusById(sensorId, false, false); + } + + Log.swarningln( + FPSTR(L_SENSORS_NTC), F("GPIO %hhu, sensor #%hhu '%s', voltage is out of threshold: %.3f"), + sSensor.gpio, sensorId, sSensor.name, (value / 1000.0f) + ); + + continue; + } + + const float sensorResistance = value > 0.001f + ? DEFAULT_NTC_REF_RESISTANCE / (DEFAULT_NTC_VREF / (float) value - 1.0f) + : 0.0f; + const float rawTemp = 1.0f / ( + 1.0f / (DEFAULT_NTC_NOMINAL_TEMP + 273.15f) + + log(sensorResistance / DEFAULT_NTC_NOMINAL_RESISTANCE) / DEFAULT_NTC_BETA_FACTOR + ) - 273.15f; + + Log.straceln( + FPSTR(L_SENSORS_NTC), F("GPIO %hhu, sensor #%hhu '%s', raw temp: %.2f, raw voltage: %.3f, raw resistance: %.2f"), + sSensor.gpio, sensorId, sSensor.name, rawTemp, (value / 1000.0f), sensorResistance + ); + + // set temp + Sensors::setValueById(sensorId, rawTemp, Sensors::ValueType::TEMPERATURE, true, true); + } + } + + bool connectToBleDevice(const uint8_t sensorId) { + #if USE_BLE if (!NimBLEDevice::getInitialized()) { return false; } + auto& sSensor = Sensors::settings[sensorId]; + auto& rSensor = Sensors::results[sensorId]; + + if (!sSensor.enabled || sSensor.type != Sensors::Type::BLUETOOTH || sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) { + return false; + } + + uint8_t addr[6] = { + sSensor.address[0], sSensor.address[1], sSensor.address[2], + sSensor.address[3], sSensor.address[4], sSensor.address[5] + }; + const NimBLEAddress address = NimBLEAddress(addr); + NimBLEClient* pClient = nullptr; pClient = NimBLEDevice::getClientByPeerAddress(address); @@ -181,18 +479,28 @@ class SensorsTask : public LeanTask { } if(pClient->isConnected()) { - *pRssi = pClient->getRssi(); + if (!rSensor.connected) { + rSensor.connected = true; + } + return true; } if (!pClient->connect(address)) { - Log.swarningln(FPSTR(L_SENSORS_BLE), F("Device %s: failed connecting"), address.toString().c_str()); + Log.swarningln( + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed connecting to %s"), + sensorId, sSensor.name, address.toString().c_str() + ); NimBLEDevice::deleteClient(pClient); return false; } - Log.sinfoln(FPSTR(L_SENSORS_BLE), F("Device %s: connected"), address.toString().c_str()); + Log.sinfoln( + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': connected to %s"), + sensorId, sSensor.name, address.toString().c_str() + ); + NimBLERemoteService* pService = nullptr; NimBLERemoteCharacteristic* pChar = nullptr; @@ -201,21 +509,16 @@ class SensorsTask : public LeanTask { pService = pClient->getService(serviceUuid); if (!pService) { Log.straceln( - FPSTR(L_SENSORS_BLE), - F("Device %s: failed to find env service (%s)"), - address.toString().c_str(), - serviceUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed to find env service (%s) on device %s"), + sensorId, sSensor.name, serviceUuid.toString().c_str(), address.toString().c_str() ); } else { Log.straceln( - FPSTR(L_SENSORS_BLE), - F("Device %s: found env service (%s)"), - address.toString().c_str(), - serviceUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found env service (%s) on device %s"), + sensorId, sSensor.name, serviceUuid.toString().c_str(), address.toString().c_str() ); - // 0x2A6E - Notify temperature x0.01C (pvvx) bool tempNotifyCreated = false; if (!tempNotifyCreated) { @@ -224,13 +527,11 @@ class SensorsTask : public LeanTask { if (pChar && pChar->canNotify()) { Log.straceln( - FPSTR(L_SENSORS_BLE), - F("Device %s: found temperature char (%s) in env service"), - address.toString().c_str(), - charUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found temp char (%s) in env service on device %s"), + sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str() ); - tempNotifyCreated = pChar->subscribe(true, [pTemperature](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { + tempNotifyCreated = pChar->subscribe(true, [sensorId](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { if (pChar == nullptr) { return; } @@ -245,48 +546,47 @@ class SensorsTask : public LeanTask { return; } + auto& sSensor = Sensors::settings[sensorId]; + if (length != 2) { Log.swarningln( FPSTR(L_SENSORS_BLE), - F("Device %s: invalid notification data at temperature char (%s)"), - pClient->getPeerAddress().toString().c_str(), - pChar->getUUID().toString().c_str() + F("Sensor #%hhu '%s': invalid notification data at temp char (%s) on device %s"), + sensorId, + sSensor.name, + pChar->getUUID().toString().c_str(), + pClient->getPeerAddress().toString().c_str() ); + return; } - float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.01f); + float rawTemp = (pChar->getValue() * 0.01f); Log.straceln( - FPSTR(L_SENSORS_INDOOR), - F("Device %s: raw temp %f"), - pClient->getPeerAddress().toString().c_str(), - rawTemp + FPSTR(L_SENSORS_BLE), + F("Sensor #%hhu '%s': received temp: %.2f"), + sensorId, sSensor.name, rawTemp ); - if (fabs(*pTemperature) < 0.1f) { - *pTemperature = rawTemp; - - } else { - *pTemperature += (rawTemp - (*pTemperature)) * EXT_SENSORS_FILTER_K; - } + // set temp + Sensors::setValueById(sensorId, rawTemp, Sensors::ValueType::TEMPERATURE, true, true); - *pTemperature = floor((*pTemperature) * 100) / 100; + // update rssi + Sensors::setValueById(sensorId, pClient->getRssi(), Sensors::ValueType::RSSI, false, false); }); if (tempNotifyCreated) { Log.straceln( - FPSTR(L_SENSORS_BLE), - F("Device %s: subscribed to temperature char (%s) in env service"), - address.toString().c_str(), - charUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': subscribed to temp char (%s) in env service on device %s"), + sensorId, sSensor.name, + charUuid.toString().c_str(), address.toString().c_str() ); } else { Log.swarningln( - FPSTR(L_SENSORS_BLE), - F("Device %s: failed to subscribe to temperature char (%s) in env service"), - address.toString().c_str(), - charUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed to subscribe to temp char (%s) in env service on device %s"), + sensorId, sSensor.name, + charUuid.toString().c_str(), address.toString().c_str() ); } } @@ -300,13 +600,11 @@ class SensorsTask : public LeanTask { if (pChar && pChar->canNotify()) { Log.straceln( - FPSTR(L_SENSORS_BLE), - F("Device %s: found temperature char (%s) in env service"), - address.toString().c_str(), - charUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found temp char (%s) in env service on device %s"), + sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str() ); - tempNotifyCreated = pChar->subscribe(true, [pTemperature](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { + tempNotifyCreated = pChar->subscribe(true, [sensorId](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { if (pChar == nullptr) { return; } @@ -321,48 +619,47 @@ class SensorsTask : public LeanTask { return; } + auto& sSensor = Sensors::settings[sensorId]; + if (length != 2) { Log.swarningln( FPSTR(L_SENSORS_BLE), - F("Device %s: invalid notification data at temperature char (%s)"), - pClient->getPeerAddress().toString().c_str(), - pChar->getUUID().toString().c_str() + F("Sensor #%hhu '%s': invalid notification data at temp char (%s) on device %s"), + sensorId, + sSensor.name, + pChar->getUUID().toString().c_str(), + pClient->getPeerAddress().toString().c_str() ); + return; } - float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.1f); + float rawTemp = (pChar->getValue() * 0.1f); Log.straceln( - FPSTR(L_SENSORS_INDOOR), - F("Device %s: raw temp %f"), - pClient->getPeerAddress().toString().c_str(), - rawTemp + FPSTR(L_SENSORS_BLE), + F("Sensor #%hhu '%s': received temp: %.2f"), + sensorId, sSensor.name, rawTemp ); - if (fabs(*pTemperature) < 0.1f) { - *pTemperature = rawTemp; + // set temp + Sensors::setValueById(sensorId, rawTemp, Sensors::ValueType::TEMPERATURE, true, true); - } else { - *pTemperature += (rawTemp - (*pTemperature)) * EXT_SENSORS_FILTER_K; - } - - *pTemperature = floor((*pTemperature) * 100) / 100; + // update rssi + Sensors::setValueById(sensorId, pClient->getRssi(), Sensors::ValueType::RSSI, false, false); }); if (tempNotifyCreated) { Log.straceln( - FPSTR(L_SENSORS_BLE), - F("Device %s: subscribed to temperature char (%s) in env service"), - address.toString().c_str(), - charUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': subscribed to temp char (%s) in env service on device %s"), + sensorId, sSensor.name, + charUuid.toString().c_str(), address.toString().c_str() ); } else { Log.swarningln( - FPSTR(L_SENSORS_BLE), - F("Device %s: failed to subscribe to temperature char (%s) in env service"), - address.toString().c_str(), - charUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed to subscribe to temp char (%s) in env service on device %s"), + sensorId, sSensor.name, + charUuid.toString().c_str(), address.toString().c_str() ); } } @@ -370,9 +667,8 @@ class SensorsTask : public LeanTask { if (!tempNotifyCreated) { Log.swarningln( - FPSTR(L_SENSORS_BLE), - F("Device %s: not found supported temperature chars in env service"), - address.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': not found supported temp chars in env service on device %s"), + sensorId, sSensor.name, address.toString().c_str() ); pClient->disconnect(); @@ -381,7 +677,7 @@ class SensorsTask : public LeanTask { // 0x2A6F - Notify about humidity x0.01% (pvvx) - if (pHumidity != nullptr) { + { bool humidityNotifyCreated = false; if (!humidityNotifyCreated) { NimBLEUUID charUuid((uint16_t) 0x2A6F); @@ -389,13 +685,11 @@ class SensorsTask : public LeanTask { if (pChar && pChar->canNotify()) { Log.straceln( - FPSTR(L_SENSORS_BLE), - F("Device %s: found humidity char (%s) in env service"), - address.toString().c_str(), - charUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found humidity char (%s) in env service on device %s"), + sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str() ); - humidityNotifyCreated = pChar->subscribe(true, [pHumidity](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { + humidityNotifyCreated = pChar->subscribe(true, [sensorId](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { if (pChar == nullptr) { return; } @@ -410,48 +704,47 @@ class SensorsTask : public LeanTask { return; } + auto& sSensor = Sensors::settings[sensorId]; + if (length != 2) { Log.swarningln( FPSTR(L_SENSORS_BLE), - F("Device %s: invalid notification data at humidity char (%s)"), - pClient->getPeerAddress().toString().c_str(), - pChar->getUUID().toString().c_str() + F("Sensor #%hhu '%s': invalid notification data at humidity char (%s) on device %s"), + sensorId, + sSensor.name, + pChar->getUUID().toString().c_str(), + pClient->getPeerAddress().toString().c_str() ); + return; } - float rawHumidity = ((pData[0] | (pData[1] << 8)) * 0.01f); + float rawHumidity = (pChar->getValue() * 0.01f); Log.straceln( - FPSTR(L_SENSORS_INDOOR), - F("Device %s: raw humidity %f"), - pClient->getPeerAddress().toString().c_str(), - rawHumidity + FPSTR(L_SENSORS_BLE), + F("Sensor #%hhu '%s': received humidity: %.2f"), + sensorId, sSensor.name, rawHumidity ); - if (fabs(*pHumidity) < 0.1f) { - *pHumidity = rawHumidity; - - } else { - *pHumidity += (rawHumidity - (*pHumidity)) * EXT_SENSORS_FILTER_K; - } + // set humidity + Sensors::setValueById(sensorId, rawHumidity, Sensors::ValueType::HUMIDITY, true, true); - *pHumidity = floor((*pHumidity) * 100) / 100; + // update rssi + Sensors::setValueById(sensorId, pClient->getRssi(), Sensors::ValueType::RSSI, false, false); }); if (humidityNotifyCreated) { Log.straceln( - FPSTR(L_SENSORS_BLE), - F("Device %s: subscribed to humidity char (%s) in env service"), - address.toString().c_str(), - charUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': subscribed to humidity char (%s) in env service on device %s"), + sensorId, sSensor.name, + charUuid.toString().c_str(), address.toString().c_str() ); } else { Log.swarningln( - FPSTR(L_SENSORS_BLE), - F("Device %s: failed to subscribe to humidity char (%s) in env service"), - address.toString().c_str(), - charUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed to subscribe to humidity char (%s) in env service on device %s"), + sensorId, sSensor.name, + charUuid.toString().c_str(), address.toString().c_str() ); } } @@ -459,9 +752,8 @@ class SensorsTask : public LeanTask { if (!humidityNotifyCreated) { Log.swarningln( - FPSTR(L_SENSORS_BLE), - F("Device %s: not found supported humidity chars in env service"), - address.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': not found supported humidity chars in env service on device %s"), + sensorId, sSensor.name, address.toString().c_str() ); } } @@ -469,23 +761,19 @@ class SensorsTask : public LeanTask { // Battery Service (0x180F) - if (pBattery != nullptr) { + { NimBLEUUID serviceUuid((uint16_t) 0x180F); pService = pClient->getService(serviceUuid); if (!pService) { Log.straceln( - FPSTR(L_SENSORS_BLE), - F("Device %s: failed to find battery service (%s)"), - address.toString().c_str(), - serviceUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed to find battery service (%s) on device %s"), + sensorId, sSensor.name, serviceUuid.toString().c_str(), address.toString().c_str() ); } else { Log.straceln( - FPSTR(L_SENSORS_BLE), - F("Device %s: found battery service (%s)"), - address.toString().c_str(), - serviceUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found battery service (%s) on device %s"), + sensorId, sSensor.name, serviceUuid.toString().c_str(), address.toString().c_str() ); // 0x2A19 - Notify the battery charge level 0..99% (pvvx) @@ -496,13 +784,11 @@ class SensorsTask : public LeanTask { if (pChar && pChar->canNotify()) { Log.straceln( - FPSTR(L_SENSORS_BLE), - F("Device %s: found battery char (%s) in battery service"), - address.toString().c_str(), - charUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found battery char (%s) in battery service on device %s"), + sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str() ); - batteryNotifyCreated = pChar->subscribe(true, [pBattery](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { + batteryNotifyCreated = pChar->subscribe(true, [sensorId](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t length, bool isNotify) { if (pChar == nullptr) { return; } @@ -517,48 +803,47 @@ class SensorsTask : public LeanTask { return; } + auto& sSensor = Sensors::settings[sensorId]; + if (length != 1) { Log.swarningln( FPSTR(L_SENSORS_BLE), - F("Device %s: invalid notification data at battery char (%s)"), - pClient->getPeerAddress().toString().c_str(), - pChar->getUUID().toString().c_str() + F("Sensor #%hhu '%s': invalid notification data at battery char (%s) on device %s"), + sensorId, + sSensor.name, + pChar->getUUID().toString().c_str(), + pClient->getPeerAddress().toString().c_str() ); + return; } - uint8_t rawBattery = pData[0]; + auto rawBattery = pChar->getValue(); Log.straceln( - FPSTR(L_SENSORS_INDOOR), - F("Device %s: raw battery %hhu"), - pClient->getPeerAddress().toString().c_str(), - rawBattery + FPSTR(L_SENSORS_BLE), + F("Sensor #%hhu '%s': received battery: %.2f"), + sensorId, sSensor.name, rawBattery ); - if (fabs(*pBattery) < 0.1f) { - *pBattery = rawBattery; - - } else { - *pBattery += (rawBattery - (*pBattery)) * EXT_SENSORS_FILTER_K; - } - - *pBattery = floor((*pBattery) * 100) / 100; + // set battery + Sensors::setValueById(sensorId, rawBattery, Sensors::ValueType::BATTERY, true, true); + + // update rssi + Sensors::setValueById(sensorId, pClient->getRssi(), Sensors::ValueType::RSSI, false, false); }); if (batteryNotifyCreated) { Log.straceln( - FPSTR(L_SENSORS_BLE), - F("Device %s: subscribed to battery char (%s) in battery service"), - address.toString().c_str(), - charUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': subscribed to battery char (%s) in battery service on device %s"), + sensorId, sSensor.name, + charUuid.toString().c_str(), address.toString().c_str() ); } else { Log.swarningln( - FPSTR(L_SENSORS_BLE), - F("Device %s: failed to subscribe to battery char (%s) in battery service"), - address.toString().c_str(), - charUuid.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed to subscribe to battery char (%s) in battery service on device %s"), + sensorId, sSensor.name, + charUuid.toString().c_str(), address.toString().c_str() ); } } @@ -566,173 +851,65 @@ class SensorsTask : public LeanTask { if (!batteryNotifyCreated) { Log.swarningln( - FPSTR(L_SENSORS_BLE), - F("Device %s: not found supported battery chars in battery service"), - address.toString().c_str() + FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': not found supported battery chars in battery service on device %s"), + sensorId, sSensor.name, address.toString().c_str() ); } } } return true; + #else + return false; + #endif } -#endif - - void outdoorDallasSensor() { - if (!this->initOutdoorSensor) { - if (this->initOutdoorSensorTime && millis() - this->initOutdoorSensorTime < EXT_SENSORS_INTERVAL * 10) { - return; - } - - Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("Starting on GPIO %hhu..."), settings.sensors.outdoor.gpio); - - this->oneWireOutdoorSensor->begin(settings.sensors.outdoor.gpio); - this->oneWireOutdoorSensor->reset(); - this->outdoorSensor->begin(); - this->initOutdoorSensorTime = millis(); - - Log.straceln( - FPSTR(L_SENSORS_OUTDOOR), - F("Devices on bus: %hhu, DS18* devices: %hhu"), - this->outdoorSensor->getDeviceCount(), - this->outdoorSensor->getDS18Count() - ); - - if (this->outdoorSensor->getDeviceCount() > 0) { - this->initOutdoorSensor = true; - this->outdoorSensor->setResolution(12); - this->outdoorSensor->requestTemperatures(); - this->startOutdoorConversionTime = millis(); - - Log.sinfoln(FPSTR(L_SENSORS_OUTDOOR), F("Started")); - - } else { - if (vars.sensors.outdoor.connected) { - vars.sensors.outdoor.connected = false; - } - - return; - } - } - - unsigned long estimateConversionTime = millis() - this->startOutdoorConversionTime; - if (estimateConversionTime < this->outdoorSensor->millisToWaitForConversion()) { - return; - } - bool completed = this->outdoorSensor->isConversionComplete(); - if (!completed && estimateConversionTime >= 1000) { - this->initOutdoorSensor = false; + void updateConnectionStatus() { + for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) { + auto& sSensor = Sensors::settings[sensorId]; + auto& rSensor = Sensors::results[sensorId]; - Log.serrorln(FPSTR(L_SENSORS_OUTDOOR), F("Could not read temperature data (no response)")); - } + if (rSensor.connected && !sSensor.enabled) { + rSensor.connected = false; - if (!completed) { - return; - } + } else if (rSensor.connected && sSensor.type == Sensors::Type::NOT_CONFIGURED) { + rSensor.connected = false; - float rawTemp = this->outdoorSensor->getTempCByIndex(0); - if (rawTemp == DEVICE_DISCONNECTED_C) { - this->initOutdoorSensor = false; + } else if (rSensor.connected && sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) { + rSensor.connected = false; - Log.serrorln(FPSTR(L_SENSORS_OUTDOOR), F("Could not read temperature data (not connected)")); - - } else { - Log.straceln(FPSTR(L_SENSORS_OUTDOOR), F("Raw temp: %f"), rawTemp); - - if (!vars.sensors.outdoor.connected) { - vars.sensors.outdoor.connected = true; - } - - if (fabs(this->filteredOutdoorTemp) < 0.1f) { - this->filteredOutdoorTemp = rawTemp; - - } else { - this->filteredOutdoorTemp += (rawTemp - this->filteredOutdoorTemp) * EXT_SENSORS_FILTER_K; - } + } else if (sSensor.type != Sensors::Type::MANUAL && rSensor.connected && (millis() - rSensor.activityTime) > this->disconnectedTimeout) { + rSensor.connected = false; - this->filteredOutdoorTemp = floor(this->filteredOutdoorTemp * 100) / 100; - this->outdoorSensor->requestTemperatures(); - this->startOutdoorConversionTime = millis(); + }/* else if (!rSensor.connected) { + rSensor.connected = true; + }*/ } } - void indoorDallasSensor() { - if (!this->initIndoorSensor) { - if (this->initIndoorSensorTime && millis() - this->initIndoorSensorTime < EXT_SENSORS_INTERVAL * 10) { - return; - } - - Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("Starting on GPIO %hhu..."), settings.sensors.indoor.gpio); - - this->oneWireIndoorSensor->begin(settings.sensors.indoor.gpio); - this->oneWireIndoorSensor->reset(); - this->indoorSensor->begin(); - this->initIndoorSensorTime = millis(); - - Log.straceln( - FPSTR(L_SENSORS_INDOOR), - F("Devices on bus: %hhu, DS18* devices: %hhu"), - this->indoorSensor->getDeviceCount(), - this->indoorSensor->getDS18Count() - ); - - if (this->indoorSensor->getDeviceCount() > 0) { - this->initIndoorSensor = true; - this->indoorSensor->setResolution(12); - this->indoorSensor->requestTemperatures(); - this->startIndoorConversionTime = millis(); + static bool isEqualAddress(const uint8_t *addr1, const uint8_t *addr2, const uint8_t length = 8) { + bool result = true; - Log.sinfoln(FPSTR(L_SENSORS_INDOOR), F("Started")); - - } else { - if (vars.sensors.indoor.connected) { - vars.sensors.indoor.connected = false; - } - - return; + for (uint8_t i = 0; i < length; i++) { + if (addr1[i] != addr2[i]) { + result = false; + break; } } - unsigned long estimateConversionTime = millis() - this->startIndoorConversionTime; - if (estimateConversionTime < this->indoorSensor->millisToWaitForConversion()) { - return; - } - - bool completed = this->indoorSensor->isConversionComplete(); - if (!completed && estimateConversionTime >= 1000) { - this->initIndoorSensor = false; - - Log.serrorln(FPSTR(L_SENSORS_INDOOR), F("Could not read temperature data (no response)")); - } - - if (!completed) { - return; - } - - float rawTemp = this->indoorSensor->getTempCByIndex(0); - if (rawTemp == DEVICE_DISCONNECTED_C) { - this->initIndoorSensor = false; - - Log.serrorln(FPSTR(L_SENSORS_INDOOR), F("Could not read temperature data (not connected)")); - - } else { - Log.straceln(FPSTR(L_SENSORS_INDOOR), F("Raw temp: %f"), rawTemp); - - if (!vars.sensors.indoor.connected) { - vars.sensors.indoor.connected = true; - } + return result; + } - if (fabs(this->filteredIndoorTemp) < 0.1f) { - this->filteredIndoorTemp = rawTemp; + static bool isEmptyAddress(const uint8_t *addr, const uint8_t length = 8) { + bool result = true; - } else { - this->filteredIndoorTemp += (rawTemp - this->filteredIndoorTemp) * EXT_SENSORS_FILTER_K; + for (uint8_t i = 0; i < length; i++) { + if (addr[i] != 0) { + result = false; + break; } - - this->filteredIndoorTemp = floor(this->filteredIndoorTemp * 100) / 100; - this->indoorSensor->requestTemperatures(); - this->startIndoorConversionTime = millis(); } + + return result; } }; \ No newline at end of file diff --git a/src/Settings.h b/src/Settings.h index 7cf5650..3830942 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -27,12 +27,12 @@ struct Settings { uint8_t logLevel = DEFAULT_LOG_LEVEL; struct { - bool enable = DEFAULT_SERIAL_ENABLE; + bool enabled = DEFAULT_SERIAL_ENABLED; unsigned int baudrate = DEFAULT_SERIAL_BAUD; } serial; struct { - bool enable = DEFAULT_TELNET_ENABLE; + bool enabled = DEFAULT_TELNET_ENABLED; unsigned short port = DEFAULT_TELNET_PORT; } telnet; @@ -51,12 +51,12 @@ struct Settings { byte inGpio = DEFAULT_OT_IN_GPIO; byte outGpio = DEFAULT_OT_OUT_GPIO; byte rxLedGpio = DEFAULT_OT_RX_LED_GPIO; - unsigned int memberIdCode = 0; + uint8_t memberId = 0; + uint8_t flags = 0; uint8_t maxModulation = 100; - float pressureFactor = 1.0f; - float dhwFlowRateFactor = 1.0f; float minPower = 0.0f; float maxPower = 0.0f; + bool dhwPresent = true; bool summerWinterMode = false; bool heatingCh2Enabled = true; @@ -67,15 +67,10 @@ struct Settings { bool getMinMaxTemp = true; bool nativeHeatingControl = false; bool immergasFix = false; - - struct { - bool enable = false; - float factor = 0.1f; - } filterNumValues; } opentherm; struct { - bool enable = false; + bool enabled = DEFAULT_MQTT_ENABLED; char server[81] = DEFAULT_MQTT_SERVER; unsigned short port = DEFAULT_MQTT_PORT; char user[33] = DEFAULT_MQTT_USER; @@ -91,7 +86,7 @@ struct Settings { } emergency; struct { - bool enable = true; + bool enabled = true; bool turbo = false; float target = DEFAULT_HEATING_TARGET_TEMP; float hysteresis = 0.5f; @@ -101,14 +96,14 @@ struct Settings { } heating; struct { - bool enable = true; + bool enabled = true; float target = DEFAULT_DHW_TARGET_TEMP; byte minTemp = DEFAULT_DHW_MIN_TEMP; byte maxTemp = DEFAULT_DHW_MAX_TEMP; } dhw; struct { - bool enable = false; + bool enabled = false; float p_factor = 2.0f; float i_factor = 0.0055f; float d_factor = 0.0f; @@ -118,28 +113,12 @@ struct Settings { } pid; struct { - bool enable = false; + bool enabled = false; float n_factor = 0.7f; float k_factor = 3.0f; float t_factor = 2.0f; } equitherm; - struct { - struct { - SensorType type = SensorType::BOILER_OUTDOOR; - byte gpio = DEFAULT_SENSOR_OUTDOOR_GPIO; - uint8_t bleAddress[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - float offset = 0.0f; - } outdoor; - - struct { - SensorType type = SensorType::MANUAL; - byte gpio = DEFAULT_SENSOR_INDOOR_GPIO; - uint8_t bleAddress[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - float offset = 0.0f; - } indoor; - } sensors; - struct { bool use = false; byte gpio = DEFAULT_EXT_PUMP_GPIO; @@ -150,14 +129,14 @@ struct Settings { struct { struct { - bool enable = false; + bool enabled = false; byte gpio = GPIO_IS_NOT_CONFIGURED; byte invertState = false; unsigned short thresholdTime = 60; } input; struct { - bool enable = false; + bool enabled = false; byte gpio = GPIO_IS_NOT_CONFIGURED; byte invertState = false; unsigned short thresholdTime = 60; @@ -170,51 +149,95 @@ struct Settings { char validationValue[8] = SETTINGS_VALID_VALUE; } settings; -struct Variables { - struct { - bool otStatus = false; - bool emergency = false; - bool heating = false; - bool dhw = false; - bool flame = false; - bool fault = false; - bool diagnostic = false; - bool externalPump = false; - bool mqtt = false; - } states; +Sensors::Settings sensorsSettings[SENSORS_AMOUNT] = { + { + false, + "Outdoor temp", + Sensors::Purpose::OUTDOOR_TEMP, + Sensors::Type::OT_OUTDOOR_TEMP, + DEFAULT_SENSOR_OUTDOOR_GPIO + }, + { + false, + "Indoor temp", + Sensors::Purpose::INDOOR_TEMP, + Sensors::Type::DALLAS_TEMP, + DEFAULT_SENSOR_INDOOR_GPIO + }, + { + true, + "Heating temp", + Sensors::Purpose::HEATING_TEMP, + Sensors::Type::OT_HEATING_TEMP, + }, + { + true, + "Heating return temp", + Sensors::Purpose::HEATING_RETURN_TEMP, + Sensors::Type::OT_HEATING_RETURN_TEMP, + }, + { + true, + "Heating setpoint temp", + Sensors::Purpose::TEMPERATURE, + Sensors::Type::HEATING_SETPOINT_TEMP, + }, + { + true, + "DHW temp", + Sensors::Purpose::DHW_TEMP, + Sensors::Type::OT_DHW_TEMP, + }, + { + true, + "DHW flow rate", + Sensors::Purpose::DHW_FLOW_RATE, + Sensors::Type::OT_DHW_FLOW_RATE, + }, + { + true, + "Exhaust temp", + Sensors::Purpose::EXHAUST_TEMP, + Sensors::Type::OT_EXHAUST_TEMP, + }, + { + true, + "Pressure", + Sensors::Purpose::PRESSURE, + Sensors::Type::OT_PRESSURE, + }, + { + true, + "Modulation level", + Sensors::Purpose::MODULATION_LEVEL, + Sensors::Type::OT_MODULATION_LEVEL, + }, + { + true, + "Power", + Sensors::Purpose::POWER, + Sensors::Type::OT_CURRENT_POWER, + } +}; +struct Variables { struct { - float modulation = 0.0f; - float pressure = 0.0f; - float dhwFlowRate = 0.0f; - float power = 0.0f; - byte faultCode = 0; - unsigned short diagnosticCode = 0; + bool connected = false; int8_t rssi = 0; + } network; - struct { - bool connected = false; - int8_t rssi = 0; - float battery = 0.0f; - float humidity = 0.0f; - } outdoor; + struct { + bool connected = false; + } mqtt; - struct { - bool connected = false; - int8_t rssi = 0; - float battery = 0.0f; - float humidity = 0.0f; - } indoor; - } sensors; + struct { + bool state = false; + } emergency; struct { - float indoor = 0.0f; - float outdoor = 0.0f; - float heating = 0.0f; - float heatingReturn = 0.0f; - float dhw = 0.0f; - float exhaust = 0.0f; - } temperatures; + bool state = false; + unsigned long lastEnabledTime = 0; + } externalPump; struct { bool input = false; @@ -222,31 +245,130 @@ struct Variables { } cascadeControl; struct { - bool heatingEnabled = false; - byte heatingMinTemp = DEFAULT_HEATING_MIN_TEMP; - byte heatingMaxTemp = DEFAULT_HEATING_MAX_TEMP; - float heatingSetpoint = 0; - unsigned long extPumpLastEnableTime = 0; - byte dhwMinTemp = DEFAULT_DHW_MIN_TEMP; - byte dhwMaxTemp = DEFAULT_DHW_MAX_TEMP; - byte minModulation = 0; - byte maxModulation = 0; - uint8_t maxPower = 0; - uint8_t slaveMemberId = 0; - uint8_t slaveFlags = 0; - uint8_t slaveType = 0; - uint8_t slaveVersion = 0; - float slaveOtVersion = 0.0f; - uint8_t masterMemberId = 0; - uint8_t masterFlags = 0; - uint8_t masterType = 0; - uint8_t masterVersion = 0; - float masterOtVersion = 0; - } parameters; + uint8_t memberId = 0; + uint8_t flags = 0; + uint8_t type = 0; + uint8_t appVersion = 0; + float protocolVersion = 0.0f; + + struct { + bool blocking = false; + bool enabled = false; + bool indoorTempControl = false; + float setpointTemp = 0.0f; + float targetTemp = 0.0f; + float currentTemp = 0.0f; + float returnTemp = 0.0f; + float indoorTemp = 0.0f; + float outdoorTemp = 0.0f; + float minTemp = 0.0f; + float maxTemp = 0.0f; + } heating; + + struct { + bool enabled = false; + float targetTemp = 0.0f; + float currentTemp = 0.0f; + float returnTemp = 0.0f; + } dhw; + + struct { + bool enabled = false; + float targetTemp = 0.0f; + } ch2; + } master; + + struct { + uint8_t memberId = 0; + uint8_t flags = 0; + uint8_t type = 0; + uint8_t appVersion = 0; + float protocolVersion = 0.0f; + + bool connected = false; + bool flame = false; + float pressure = 0.0f; + float heatExchangerTemp = 0.0f; + + struct { + bool active = false; + uint8_t code = 0; + } fault; + + struct { + bool active = false; + uint16_t code = 0; + } diag; + + struct { + uint8_t current = 0; + uint8_t min = 0; + uint8_t max = 100; + } modulation; + + struct { + float current = 0.0f; + float min = 0.0f; + float max = 0.0f; + } power; + + struct { + float temp = 0.0f; + uint16_t co2 = 0; + uint16_t fanSpeed = 0; + } exhaust; + + struct { + float storage = 0.0f; + float collector = 0.0f; + } solar; + + struct { + uint8_t setpoint = 0; + uint8_t current = 0; + uint16_t supply = 0; + } fanSpeed; + + struct { + bool active = false; + bool enabled = false; + float targetTemp = 0.0f; + float currentTemp = 0.0f; + float returnTemp = 0.0f; + float indoorTemp = 0.0f; + float outdoorTemp = 0.0f; + uint8_t minTemp = DEFAULT_HEATING_MIN_TEMP; + uint8_t maxTemp = DEFAULT_HEATING_MAX_TEMP; + } heating; + + struct { + bool active = false; + bool enabled = false; + float targetTemp = 0.0f; + float currentTemp = 0.0f; + float currentTemp2 = 0.0f; + float returnTemp = 0.0f; + float flowRate = 0.0f; + uint8_t minTemp = DEFAULT_DHW_MIN_TEMP; + uint8_t maxTemp = DEFAULT_DHW_MAX_TEMP; + } dhw; + + struct { + bool enabled = false; + float targetTemp = 0.0f; + float currentTemp = 0.0f; + float indoorTemp = 0.0f; + } ch2; + } slave; struct { bool restart = false; bool resetFault = false; bool resetDiagnostic = false; } actions; + + struct { + bool restarting = false; + bool upgrading = false; + } states; } vars; \ No newline at end of file diff --git a/src/defines.h b/src/defines.h index 599e509..8167589 100644 --- a/src/defines.h +++ b/src/defines.h @@ -2,10 +2,6 @@ #define PROJECT_REPO "https://github.com/Laxilef/OTGateway" #define MQTT_RECONNECT_INTERVAL 15000 - -#define EXT_SENSORS_INTERVAL 5000 -#define EXT_SENSORS_FILTER_K 0.15 - #define CONFIG_URL "http://%s/" #define SETTINGS_VALID_VALUE "stvalid" // only 8 chars! #define GPIO_IS_NOT_CONFIGURED 0xff @@ -22,6 +18,14 @@ #define THERMOSTAT_INDOOR_MIN_TEMP 5 #define THERMOSTAT_INDOOR_MAX_TEMP 30 +#define DEFAULT_NTC_NOMINAL_RESISTANCE 10000.0f +#define DEFAULT_NTC_NOMINAL_TEMP 25.0f +#define DEFAULT_NTC_REF_RESISTANCE 10000.0f +#define DEFAULT_NTC_BETA_FACTOR 3950.0f +#define DEFAULT_NTC_VREF 3300.0f +#define DEFAULT_NTC_VLOW_TRESHOLD 25.0f +#define DEFAULT_NTC_VHIGH_TRESHOLD 3298.0f + #ifndef BUILD_VERSION #define BUILD_VERSION "0.0.0" #endif @@ -30,16 +34,16 @@ #define BUILD_ENV "undefined" #endif -#ifndef DEFAULT_SERIAL_ENABLE - #define DEFAULT_SERIAL_ENABLE true +#ifndef DEFAULT_SERIAL_ENABLED + #define DEFAULT_SERIAL_ENABLED true #endif #ifndef DEFAULT_SERIAL_BAUD #define DEFAULT_SERIAL_BAUD 115200 #endif -#ifndef DEFAULT_TELNET_ENABLE - #define DEFAULT_TELNET_ENABLE true +#ifndef DEFAULT_TELNET_ENABLED + #define DEFAULT_TELNET_ENABLED true #endif #ifndef DEFAULT_TELNET_PORT @@ -86,6 +90,10 @@ #define DEFAULT_PORTAL_PASSWORD "" #endif +#ifndef DEFAULT_MQTT_ENABLED + #define DEFAULT_MQTT_ENABLED false +#endif + #ifndef DEFAULT_MQTT_SERVER #define DEFAULT_MQTT_SERVER "" #endif @@ -130,6 +138,10 @@ #define DEFAULT_SENSOR_INDOOR_GPIO GPIO_IS_NOT_CONFIGURED #endif +#ifndef SENSORS_AMOUNT + #define SENSORS_AMOUNT 20 +#endif + #ifndef DEFAULT_EXT_PUMP_GPIO #define DEFAULT_EXT_PUMP_GPIO GPIO_IS_NOT_CONFIGURED #endif @@ -141,22 +153,14 @@ #ifdef ARDUINO_ARCH_ESP32 #include #elif !defined(GPIO_IS_VALID_GPIO) - #define GPIO_IS_VALID_GPIO(gpioNum) (gpioNum >= 0 && gpioNum <= 16) + #define GPIO_IS_VALID_GPIO(gpioNum) (gpioNum >= 0 && gpioNum <= 17) #endif #define GPIO_IS_VALID(gpioNum) (gpioNum != GPIO_IS_NOT_CONFIGURED && GPIO_IS_VALID_GPIO(gpioNum)) -enum class SensorType : byte { - BOILER_OUTDOOR = 0, - BOILER_RETURN = 4, - MANUAL = 1, - DS18B20 = 2, - BLUETOOTH = 3 -}; - -enum class UnitSystem : byte { - METRIC, - IMPERIAL +enum class UnitSystem : uint8_t { + METRIC = 0, + IMPERIAL = 1 }; char buffer[255]; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index c5c2f94..1aa5e8a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,13 +1,19 @@ +#define ARDUINOJSON_USE_DOUBLE 0 +#define ARDUINOJSON_USE_LONG_LONG 0 + #include -#include "defines.h" -#include "strings.h" -#include "CrashRecorder.h" #include #include #include #include + +#include "defines.h" +#include "strings.h" + #include #include +#include "CrashRecorder.h" +#include "Sensors.h" #include "Settings.h" #include "utils.h" @@ -31,10 +37,13 @@ using namespace NetworkUtils; // Vars -FileData fsNetworkSettings(&LittleFS, "/network.conf", 'n', &networkSettings, sizeof(networkSettings), 1000); -FileData fsSettings(&LittleFS, "/settings.conf", 's', &settings, sizeof(settings), 60000); ESPTelnetStream* telnetStream = nullptr; NetworkMgr* network = nullptr; +Sensors::Result sensorsResults[SENSORS_AMOUNT]; + +FileData fsNetworkSettings(&LittleFS, "/network.conf", 'n', &networkSettings, sizeof(networkSettings), 1000); +FileData fsSettings(&LittleFS, "/settings.conf", 's', &settings, sizeof(settings), 60000); +FileData fsSensorsSettings(&LittleFS, "/sensors.conf", 'e', &sensorsSettings, sizeof(sensorsSettings), 60000); // Tasks MqttTask* tMqtt; @@ -47,6 +56,9 @@ MainTask* tMain; void setup() { CrashRecorder::init(); + Sensors::setMaxSensors(SENSORS_AMOUNT); + Sensors::settings = sensorsSettings; + Sensors::results = sensorsResults; LittleFS.begin(); Log.setLevel(TinyLogger::Level::VERBOSE); @@ -64,10 +76,14 @@ void setup() { }); Serial.begin(115200); + #if ARDUINO_USB_MODE + Serial.setTxBufferSize(512); + #endif Log.addStream(&Serial); Log.print("\n\n\r"); - // network settings + // + // Network settings switch (fsNetworkSettings.read()) { case FD_FS_ERR: Log.swarningln(FPSTR(L_NETWORK_SETTINGS), F("Filesystem error, load default")); @@ -86,7 +102,27 @@ void setup() { break; } - // settings + network = (new NetworkMgr) + ->setHostname(networkSettings.hostname) + ->setStaCredentials( + strlen(networkSettings.sta.ssid) ? networkSettings.sta.ssid : nullptr, + strlen(networkSettings.sta.password) ? networkSettings.sta.password : nullptr, + networkSettings.sta.channel + )->setApCredentials( + strlen(networkSettings.ap.ssid) ? networkSettings.ap.ssid : nullptr, + strlen(networkSettings.ap.password) ? networkSettings.ap.password : nullptr, + networkSettings.ap.channel + ) + ->setUseDhcp(networkSettings.useDhcp) + ->setStaticConfig( + networkSettings.staticConfig.ip, + networkSettings.staticConfig.gateway, + networkSettings.staticConfig.subnet, + networkSettings.staticConfig.dns + ); + + // + // Settings switch (fsSettings.read()) { case FD_FS_ERR: Log.swarningln(FPSTR(L_SETTINGS), F("Filesystem error, load default")); @@ -112,8 +148,8 @@ void setup() { break; } - // logs - if (!settings.system.serial.enable) { + // Logs settings + if (!settings.system.serial.enabled) { Serial.end(); Log.clearStreams(); @@ -125,7 +161,7 @@ void setup() { Log.addStream(&Serial); } - if (settings.system.telnet.enable) { + if (settings.system.telnet.enabled) { telnetStream = new ESPTelnetStream; telnetStream->setKeepAliveInterval(500); Log.addStream(telnetStream); @@ -135,34 +171,34 @@ void setup() { Log.setLevel(static_cast(settings.system.logLevel)); } - // network - network = (new NetworkMgr) - ->setHostname(networkSettings.hostname) - ->setStaCredentials( - strlen(networkSettings.sta.ssid) ? networkSettings.sta.ssid : nullptr, - strlen(networkSettings.sta.password) ? networkSettings.sta.password : nullptr, - networkSettings.sta.channel - )->setApCredentials( - strlen(networkSettings.ap.ssid) ? networkSettings.ap.ssid : nullptr, - strlen(networkSettings.ap.password) ? networkSettings.ap.password : nullptr, - networkSettings.ap.channel - ) - ->setUseDhcp(networkSettings.useDhcp) - ->setStaticConfig( - networkSettings.staticConfig.ip, - networkSettings.staticConfig.gateway, - networkSettings.staticConfig.subnet, - networkSettings.staticConfig.dns - ); + // + // Sensors settings + switch (fsSensorsSettings.read()) { + case FD_FS_ERR: + Log.swarningln(FPSTR(L_SENSORS), F("Filesystem error, load default")); + break; + case FD_FILE_ERR: + Log.swarningln(FPSTR(L_SENSORS), F("Bad data, load default")); + break; + case FD_WRITE: + Log.sinfoln(FPSTR(L_SENSORS), F("Not found, load default")); + break; + case FD_ADD: + case FD_READ: + Log.sinfoln(FPSTR(L_SENSORS), F("Loaded")); + default: + break; + } - // tasks + // + // Make tasks tMqtt = new MqttTask(false, 500); Scheduler.start(tMqtt); tOt = new OpenThermTask(true, 750); Scheduler.start(tOt); - tSensors = new SensorsTask(true, EXT_SENSORS_INTERVAL); + tSensors = new SensorsTask(true, 1000); Scheduler.start(tSensors); tRegulator = new RegulatorTask(true, 10000); diff --git a/src/strings.h b/src/strings.h index ba5e06b..89cee3f 100644 --- a/src/strings.h +++ b/src/strings.h @@ -3,27 +3,192 @@ #define PROGMEM #endif -const char L_SETTINGS[] PROGMEM = "SETTINGS"; -const char L_SETTINGS_OT[] PROGMEM = "SETTINGS.OT"; -const char L_SETTINGS_DHW[] PROGMEM = "SETTINGS.DHW"; -const char L_SETTINGS_HEATING[] PROGMEM = "SETTINGS.HEATING"; -const char L_NETWORK[] PROGMEM = "NETWORK"; -const char L_NETWORK_SETTINGS[] PROGMEM = "NETWORK.SETTINGS"; -const char L_PORTAL_WEBSERVER[] PROGMEM = "PORTAL.WEBSERVER"; -const char L_PORTAL_DNSSERVER[] PROGMEM = "PORTAL.DNSSERVER"; -const char L_PORTAL_CAPTIVE[] PROGMEM = "PORTAL.CAPTIVE"; -const char L_PORTAL_OTA[] PROGMEM = "PORTAL.OTA"; -const char L_MAIN[] PROGMEM = "MAIN"; -const char L_MQTT[] PROGMEM = "MQTT"; -const char L_MQTT_MSG[] PROGMEM = "MQTT.MSG"; -const char L_OT[] PROGMEM = "OT"; -const char L_OT_DHW[] PROGMEM = "OT.DHW"; -const char L_OT_HEATING[] PROGMEM = "OT.HEATING"; -const char L_SENSORS_OUTDOOR[] PROGMEM = "SENSORS.OUTDOOR"; -const char L_SENSORS_INDOOR[] PROGMEM = "SENSORS.INDOOR"; -const char L_SENSORS_BLE[] PROGMEM = "SENSORS.BLE"; -const char L_REGULATOR[] PROGMEM = "REGULATOR"; -const char L_REGULATOR_PID[] PROGMEM = "REGULATOR.PID"; -const char L_REGULATOR_EQUITHERM[] PROGMEM = "REGULATOR.EQUITHERM"; -const char L_CASCADE_INPUT[] PROGMEM = "CASCADE.INPUT"; -const char L_CASCADE_OUTPUT[] PROGMEM = "CASCADE.OUTPUT"; \ No newline at end of file +const char L_SETTINGS[] PROGMEM = "SETTINGS"; +const char L_SETTINGS_OT[] PROGMEM = "SETTINGS.OT"; +const char L_SETTINGS_DHW[] PROGMEM = "SETTINGS.DHW"; +const char L_SETTINGS_HEATING[] PROGMEM = "SETTINGS.HEATING"; +const char L_NETWORK[] PROGMEM = "NETWORK"; +const char L_NETWORK_SETTINGS[] PROGMEM = "NETWORK.SETTINGS"; +const char L_PORTAL_WEBSERVER[] PROGMEM = "PORTAL.WEBSERVER"; +const char L_PORTAL_DNSSERVER[] PROGMEM = "PORTAL.DNSSERVER"; +const char L_PORTAL_CAPTIVE[] PROGMEM = "PORTAL.CAPTIVE"; +const char L_PORTAL_OTA[] PROGMEM = "PORTAL.OTA"; +const char L_MAIN[] PROGMEM = "MAIN"; +const char L_MQTT[] PROGMEM = "MQTT"; +const char L_MQTT_HA[] PROGMEM = "MQTT.HA"; +const char L_MQTT_MSG[] PROGMEM = "MQTT.MSG"; +const char L_OT[] PROGMEM = "OT"; +const char L_OT_DHW[] PROGMEM = "OT.DHW"; +const char L_OT_HEATING[] PROGMEM = "OT.HEATING"; +const char L_OT_CH2[] PROGMEM = "OT.CH2"; +const char L_SENSORS[] PROGMEM = "SENSORS"; +const char L_SENSORS_SETTINGS[] PROGMEM = "SENSORS.SETTINGS"; +const char L_SENSORS_DALLAS[] PROGMEM = "SENSORS.DALLAS"; +const char L_SENSORS_NTC[] PROGMEM = "SENSORS.NTC"; +const char L_SENSORS_BLE[] PROGMEM = "SENSORS.BLE"; +const char L_REGULATOR[] PROGMEM = "REGULATOR"; +const char L_REGULATOR_PID[] PROGMEM = "REGULATOR.PID"; +const char L_REGULATOR_EQUITHERM[] PROGMEM = "REGULATOR.EQUITHERM"; +const char L_CASCADE_INPUT[] PROGMEM = "CASCADE.INPUT"; +const char L_CASCADE_OUTPUT[] PROGMEM = "CASCADE.OUTPUT"; +const char L_EXTPUMP[] PROGMEM = "EXTPUMP"; + + +const char S_ACTIONS[] PROGMEM = "actions"; +const char S_ACTIVE[] PROGMEM = "active"; +const char S_ADDRESS[] PROGMEM = "address"; +const char S_ANTI_STUCK_INTERVAL[] PROGMEM = "antiStuckInterval"; +const char S_ANTI_STUCK_TIME[] PROGMEM = "antiStuckTime"; +const char S_AP[] PROGMEM = "ap"; +const char S_APP_VERSION[] PROGMEM = "appVersion"; +const char S_AUTH[] PROGMEM = "auth"; +const char S_BACKTRACE[] PROGMEM = "backtrace"; +const char S_BATTERY[] PROGMEM = "battery"; +const char S_BAUDRATE[] PROGMEM = "baudrate"; +const char S_BLOCKING[] PROGMEM = "blocking"; +const char S_BSSID[] PROGMEM = "bssid"; +const char S_BUILD[] PROGMEM = "build"; +const char S_CASCADE_CONTROL[] PROGMEM = "cascadeControl"; +const char S_CHANNEL[] PROGMEM = "channel"; +const char S_CHIP[] PROGMEM = "chip"; +const char S_CODE[] PROGMEM = "code"; +const char S_CONNECTED[] PROGMEM = "connected"; +const char S_CONTINUES[] PROGMEM = "continues"; +const char S_CORE[] PROGMEM = "core"; +const char S_CORES[] PROGMEM = "cores"; +const char S_CRASH[] PROGMEM = "crash"; +const char S_CURRENT_TEMP[] PROGMEM = "currentTemp"; +const char S_DATA[] PROGMEM = "data"; +const char S_DATE[] PROGMEM = "date"; +const char S_DHW[] PROGMEM = "dhw"; +const char S_DHW_BLOCKING[] PROGMEM = "dhwBlocking"; +const char S_DHW_PRESENT[] PROGMEM = "dhwPresent"; +const char S_DHW_TO_CH2[] PROGMEM = "dhwToCh2"; +const char S_DIAG[] PROGMEM = "diag"; +const char S_DNS[] PROGMEM = "dns"; +const char S_DT[] PROGMEM = "dt"; +const char S_D_FACTOR[] PROGMEM = "d_factor"; +const char S_EMERGENCY[] PROGMEM = "emergency"; +const char S_ENABLED[] PROGMEM = "enabled"; +const char S_ENV[] PROGMEM = "env"; +const char S_EPC[] PROGMEM = "epc"; +const char S_EQUITHERM[] PROGMEM = "equitherm"; +const char S_EXTERNAL_PUMP[] PROGMEM = "externalPump"; +const char S_FACTOR[] PROGMEM = "factor"; +const char S_FAULT[] PROGMEM = "fault"; +const char S_FILTERING[] PROGMEM = "filtering"; +const char S_FILTERING_FACTOR[] PROGMEM = "filteringFactor"; +const char S_FLAGS[] PROGMEM = "flags"; +const char S_FLAME[] PROGMEM = "flame"; +const char S_FLASH[] PROGMEM = "flash"; +const char S_FREE[] PROGMEM = "free"; +const char S_FREQ[] PROGMEM = "freq"; +const char S_GATEWAY[] PROGMEM = "gateway"; +const char S_GET_MIN_MAX_TEMP[] PROGMEM = "getMinMaxTemp"; +const char S_GPIO[] PROGMEM = "gpio"; +const char S_HEAP[] PROGMEM = "heap"; +const char S_HEATING[] PROGMEM = "heating"; +const char S_HEATING_CH1_TO_CH2[] PROGMEM = "heatingCh1ToCh2"; +const char S_HEATING_CH2_ENABLED[] PROGMEM = "heatingCh2Enabled"; +const char S_HIDDEN[] PROGMEM = "hidden"; +const char S_HOME_ASSISTANT_DISCOVERY[] PROGMEM = "homeAssistantDiscovery"; +const char S_HOSTNAME[] PROGMEM = "hostname"; +const char S_HUMIDITY[] PROGMEM = "humidity"; +const char S_HYSTERESIS[] PROGMEM = "hysteresis"; +const char S_ID[] PROGMEM = "id"; +const char S_IMMERGAS_FIX[] PROGMEM = "immergasFix"; +const char S_INDOOR_TEMP[] PROGMEM = "indoorTemp"; +const char S_INDOOR_TEMP_CONTROL[] PROGMEM = "indoorTempControl"; +const char S_IN_GPIO[] PROGMEM = "inGpio"; +const char S_INPUT[] PROGMEM = "input"; +const char S_INTERVAL[] PROGMEM = "interval"; +const char S_INVERT_STATE[] PROGMEM = "invertState"; +const char S_IP[] PROGMEM = "ip"; +const char S_I_FACTOR[] PROGMEM = "i_factor"; +const char S_K_FACTOR[] PROGMEM = "k_factor"; +const char S_LOGIN[] PROGMEM = "login"; +const char S_LOG_LEVEL[] PROGMEM = "logLevel"; +const char S_MAC[] PROGMEM = "mac"; +const char S_MASTER[] PROGMEM = "master"; +const char S_MAX[] PROGMEM = "max"; +const char S_MAX_FREE_BLOCK[] PROGMEM = "maxFreeBlock"; +const char S_MAX_MODULATION[] PROGMEM = "maxModulation"; +const char S_MAX_POWER[] PROGMEM = "maxPower"; +const char S_MAX_TEMP[] PROGMEM = "maxTemp"; +const char S_MEMBER_ID[] PROGMEM = "memberId"; +const char S_MIN[] PROGMEM = "min"; +const char S_MIN_FREE[] PROGMEM = "minFree"; +const char S_MIN_MAX_FREE_BLOCK[] PROGMEM = "minMaxFreeBlock"; +const char S_MIN_POWER[] PROGMEM = "minPower"; +const char S_MIN_TEMP[] PROGMEM = "minTemp"; +const char S_MODEL[] PROGMEM = "model"; +const char S_MODULATION[] PROGMEM = "modulation"; +const char S_MODULATION_SYNC_WITH_HEATING[] PROGMEM = "modulationSyncWithHeating"; +const char S_MQTT[] PROGMEM = "mqtt"; +const char S_NAME[] PROGMEM = "name"; +const char S_NATIVE_HEATING_CONTROL[] PROGMEM = "nativeHeatingControl"; +const char S_NETWORK[] PROGMEM = "network"; +const char S_N_FACTOR[] PROGMEM = "n_factor"; +const char S_OFFSET[] PROGMEM = "offset"; +const char S_ON_ENABLED_HEATING[] PROGMEM = "onEnabledHeating"; +const char S_ON_FAULT[] PROGMEM = "onFault"; +const char S_ON_LOSS_CONNECTION[] PROGMEM = "onLossConnection"; +const char S_OPENTHERM[] PROGMEM = "opentherm"; +const char S_OUTDOOR_TEMP[] PROGMEM = "outdoorTemp"; +const char S_OUT_GPIO[] PROGMEM = "outGpio"; +const char S_OUTPUT[] PROGMEM = "output"; +const char S_PASSWORD[] PROGMEM = "password"; +const char S_PID[] PROGMEM = "pid"; +const char S_PORT[] PROGMEM = "port"; +const char S_PORTAL[] PROGMEM = "portal"; +const char S_POST_CIRCULATION_TIME[] PROGMEM = "postCirculationTime"; +const char S_POWER[] PROGMEM = "power"; +const char S_PREFIX[] PROGMEM = "prefix"; +const char S_PROTOCOL_VERSION[] PROGMEM = "protocolVersion"; +const char S_PURPOSE[] PROGMEM = "purpose"; +const char S_P_FACTOR[] PROGMEM = "p_factor"; +const char S_REAL_SIZE[] PROGMEM = "realSize"; +const char S_REASON[] PROGMEM = "reason"; +const char S_RESET_DIAGNOSTIC[] PROGMEM = "resetDiagnostic"; +const char S_RESET_FAULT[] PROGMEM = "resetFault"; +const char S_RESET_REASON[] PROGMEM = "resetReason"; +const char S_RESTART[] PROGMEM = "restart"; +const char S_RETURN_TEMP[] PROGMEM = "returnTemp"; +const char S_REV[] PROGMEM = "rev"; +const char S_RSSI[] PROGMEM = "rssi"; +const char S_RX_LED_GPIO[] PROGMEM = "rxLedGpio"; +const char S_SDK[] PROGMEM = "sdk"; +const char S_SENSORS[] PROGMEM = "sensors"; +const char S_SERIAL[] PROGMEM = "serial"; +const char S_SERVER[] PROGMEM = "server"; +const char S_SETTINGS[] PROGMEM = "settings"; +const char S_SIGNAL_QUALITY[] PROGMEM = "signalQuality"; +const char S_SIZE[] PROGMEM = "size"; +const char S_SLAVE[] PROGMEM = "slave"; +const char S_SSID[] PROGMEM = "ssid"; +const char S_STA[] PROGMEM = "sta"; +const char S_STATE[] PROGMEM = "state"; +const char S_STATIC_CONFIG[] PROGMEM = "staticConfig"; +const char S_STATUS_LED_GPIO[] PROGMEM = "statusLedGpio"; +const char S_SETPOINT_TEMP[] PROGMEM = "setpointTemp"; +const char S_SUBNET[] PROGMEM = "subnet"; +const char S_SUMMER_WINTER_MODE[] PROGMEM = "summerWinterMode"; +const char S_SYSTEM[] PROGMEM = "system"; +const char S_TARGET[] PROGMEM = "target"; +const char S_TARGET_TEMP[] PROGMEM = "targetTemp"; +const char S_TELNET[] PROGMEM = "telnet"; +const char S_TEMPERATURE[] PROGMEM = "temperature"; +const char S_THRESHOLD_TIME[] PROGMEM = "thresholdTime"; +const char S_TOTAL[] PROGMEM = "total"; +const char S_TRESHOLD_TIME[] PROGMEM = "tresholdTime"; +const char S_TURBO[] PROGMEM = "turbo"; +const char S_TURBO_FACTOR[] PROGMEM = "turboFactor"; +const char S_TYPE[] PROGMEM = "type"; +const char S_T_FACTOR[] PROGMEM = "t_factor"; +const char S_UNIT_SYSTEM[] PROGMEM = "unitSystem"; +const char S_UPTIME[] PROGMEM = "uptime"; +const char S_USE[] PROGMEM = "use"; +const char S_USE_DHCP[] PROGMEM = "useDhcp"; +const char S_USER[] PROGMEM = "user"; +const char S_VALUE[] PROGMEM = "value"; +const char S_VERSION[] PROGMEM = "version"; diff --git a/src/utils.h b/src/utils.h index 092139b..a95929d 100644 --- a/src/utils.h +++ b/src/utils.h @@ -1,5 +1,11 @@ #include +inline bool isDigit(const char* ptr) { + char* endPtr; + strtol(ptr, &endPtr, 10); + return *endPtr == 0; +} + inline float liter2gallon(float value) { return value / 4.546091879f; } @@ -11,7 +17,7 @@ inline float gallon2liter(float value) { float convertVolume(float value, const UnitSystem unitFrom, const UnitSystem unitTo) { if (unitFrom == UnitSystem::METRIC && unitTo == UnitSystem::IMPERIAL) { value = liter2gallon(value); - + } else if (unitFrom == UnitSystem::IMPERIAL && unitTo == UnitSystem::METRIC) { value = gallon2liter(value); } @@ -30,7 +36,7 @@ inline float psi2bar(float value) { float convertPressure(float value, const UnitSystem unitFrom, const UnitSystem unitTo) { if (unitFrom == UnitSystem::METRIC && unitTo == UnitSystem::IMPERIAL) { value = bar2psi(value); - + } else if (unitFrom == UnitSystem::IMPERIAL && unitTo == UnitSystem::METRIC) { value = psi2bar(value); } @@ -49,7 +55,7 @@ inline float f2c(float value) { float convertTemp(float value, const UnitSystem unitFrom, const UnitSystem unitTo) { if (unitFrom == UnitSystem::METRIC && unitTo == UnitSystem::IMPERIAL) { value = c2f(value); - + } else if (unitFrom == UnitSystem::IMPERIAL && unitTo == UnitSystem::METRIC) { value = f2c(value); } @@ -61,16 +67,16 @@ inline bool isValidTemp(const float value, UnitSystem unit, const float min = 0. return value >= convertTemp(min, minMaxUnit, unit) && value <= convertTemp(max, minMaxUnit, unit); } -double roundd(double value, uint8_t decimals = 2) { +float roundf(float value, uint8_t decimals = 2) { if (decimals == 0) { - return (int)(value + 0.5); + return (int)(value + 0.5f); - } else if (abs(value) < 0.00000001) { - return 0.0; + } else if (abs(value) < 0.00000001f) { + return 0.0f; } - double multiplier = pow10(decimals); - value += 0.5 / multiplier * (value < 0 ? -1 : 1); + float multiplier = pow10(decimals); + value += 0.5f / multiplier * (value < 0.0f ? -1.0f : 1.0f); return (int)(value * multiplier) / multiplier; } @@ -86,26 +92,26 @@ inline size_t getTotalHeap() { size_t getFreeHeap(bool getMinValue = false) { #if defined(ARDUINO_ARCH_ESP32) - return getMinValue ? ESP.getMinFreeHeap() : ESP.getFreeHeap(); - + return getMinValue ? ESP.getMinFreeHeap() : ESP.getFreeHeap(); + #elif defined(ARDUINO_ARCH_ESP8266) - static size_t minValue = 0; - size_t value = ESP.getFreeHeap(); - - if (value < minValue || minValue == 0) { - minValue = value; - } + static size_t minValue = 0; + size_t value = ESP.getFreeHeap(); - return getMinValue ? minValue : value; + if (value < minValue || minValue == 0) { + minValue = value; + } + + return getMinValue ? minValue : value; #else - return 0; + return 0; #endif } size_t getMaxFreeBlockHeap(bool getMinValue = false) { static size_t minValue = 0; size_t value = 0; - + #if defined(ARDUINO_ARCH_ESP32) value = ESP.getMaxAllocHeap(); @@ -134,7 +140,7 @@ String getResetReason() { #if defined(ARDUINO_ARCH_ESP8266) value = ESP.getResetReason(); #elif defined(ARDUINO_ARCH_ESP32) - switch(esp_reset_reason()) { + switch (esp_reset_reason()) { case ESP_RST_POWERON: value = F("Reset due to power-on event"); break; @@ -189,14 +195,14 @@ String getResetReason() { } template -void arr2str(String &str, T arr[], size_t length) { +void arr2str(String& str, T arr[], size_t length) { char buffer[12]; for (size_t i = 0; i < length; i++) { auto addr = arr[i]; if (!addr) { continue; } - + sprintf(buffer, "0x%08X ", addr); str.concat(buffer); } @@ -205,75 +211,75 @@ void arr2str(String &str, T arr[], size_t length) { } void networkSettingsToJson(const NetworkSettings& src, JsonVariant dst) { - dst["hostname"] = src.hostname; + dst[FPSTR(S_HOSTNAME)] = src.hostname; - dst["useDhcp"] = src.useDhcp; - dst["staticConfig"]["ip"] = src.staticConfig.ip; - dst["staticConfig"]["gateway"] = src.staticConfig.gateway; - dst["staticConfig"]["subnet"] = src.staticConfig.subnet; - dst["staticConfig"]["dns"] = src.staticConfig.dns; + dst[FPSTR(S_USE_DHCP)] = src.useDhcp; + dst[FPSTR(S_STATIC_CONFIG)][FPSTR(S_IP)] = src.staticConfig.ip; + dst[FPSTR(S_STATIC_CONFIG)][FPSTR(S_GATEWAY)] = src.staticConfig.gateway; + dst[FPSTR(S_STATIC_CONFIG)][FPSTR(S_SUBNET)] = src.staticConfig.subnet; + dst[FPSTR(S_STATIC_CONFIG)][FPSTR(S_DNS)] = src.staticConfig.dns; - dst["ap"]["ssid"] = src.ap.ssid; - dst["ap"]["password"] = src.ap.password; - dst["ap"]["channel"] = src.ap.channel; + dst[FPSTR(S_AP)][FPSTR(S_SSID)] = src.ap.ssid; + dst[FPSTR(S_AP)][FPSTR(S_PASSWORD)] = src.ap.password; + dst[FPSTR(S_AP)][FPSTR(S_CHANNEL)] = src.ap.channel; - dst["sta"]["ssid"] = src.sta.ssid; - dst["sta"]["password"] = src.sta.password; - dst["sta"]["channel"] = src.sta.channel; + dst[FPSTR(S_STA)][FPSTR(S_SSID)] = src.sta.ssid; + dst[FPSTR(S_STA)][FPSTR(S_PASSWORD)] = src.sta.password; + dst[FPSTR(S_STA)][FPSTR(S_CHANNEL)] = src.sta.channel; } bool jsonToNetworkSettings(const JsonVariantConst src, NetworkSettings& dst) { bool changed = false; // hostname - if (!src["hostname"].isNull()) { - String value = src["hostname"].as(); + if (!src[FPSTR(S_HOSTNAME)].isNull()) { + String value = src[FPSTR(S_HOSTNAME)].as(); - if (value.length() < sizeof(dst.hostname)) { + if (value.length() < sizeof(dst.hostname) && !value.equals(dst.hostname)) { strcpy(dst.hostname, value.c_str()); changed = true; } } // use dhcp - if (src["useDhcp"].is()) { - dst.useDhcp = src["useDhcp"].as(); + if (src[FPSTR(S_USE_DHCP)].is()) { + dst.useDhcp = src[FPSTR(S_USE_DHCP)].as(); changed = true; } // static config - if (!src["staticConfig"]["ip"].isNull()) { - String value = src["staticConfig"]["ip"].as(); + if (!src[FPSTR(S_STATIC_CONFIG)][FPSTR(S_IP)].isNull()) { + String value = src[FPSTR(S_STATIC_CONFIG)][FPSTR(S_IP)].as(); - if (value.length() < sizeof(dst.staticConfig.ip)) { + if (value.length() < sizeof(dst.staticConfig.ip) && !value.equals(dst.staticConfig.ip)) { strcpy(dst.staticConfig.ip, value.c_str()); changed = true; } } - if (!src["staticConfig"]["gateway"].isNull()) { - String value = src["staticConfig"]["gateway"].as(); + if (!src[FPSTR(S_STATIC_CONFIG)][FPSTR(S_GATEWAY)].isNull()) { + String value = src[FPSTR(S_STATIC_CONFIG)][FPSTR(S_GATEWAY)].as(); - if (value.length() < sizeof(dst.staticConfig.gateway)) { + if (value.length() < sizeof(dst.staticConfig.gateway) && !value.equals(dst.staticConfig.gateway)) { strcpy(dst.staticConfig.gateway, value.c_str()); changed = true; } } - if (!src["staticConfig"]["subnet"].isNull()) { - String value = src["staticConfig"]["subnet"].as(); + if (!src[FPSTR(S_STATIC_CONFIG)][FPSTR(S_SUBNET)].isNull()) { + String value = src[FPSTR(S_STATIC_CONFIG)][FPSTR(S_SUBNET)].as(); - if (value.length() < sizeof(dst.staticConfig.subnet)) { + if (value.length() < sizeof(dst.staticConfig.subnet) && !value.equals(dst.staticConfig.subnet)) { strcpy(dst.staticConfig.subnet, value.c_str()); changed = true; } } - if (!src["staticConfig"]["dns"].isNull()) { - String value = src["staticConfig"]["dns"].as(); + if (!src[FPSTR(S_STATIC_CONFIG)][FPSTR(S_DNS)].isNull()) { + String value = src[FPSTR(S_STATIC_CONFIG)][FPSTR(S_DNS)].as(); - if (value.length() < sizeof(dst.staticConfig.dns)) { + if (value.length() < sizeof(dst.staticConfig.dns) && !value.equals(dst.staticConfig.dns)) { strcpy(dst.staticConfig.dns, value.c_str()); changed = true; } @@ -281,26 +287,26 @@ bool jsonToNetworkSettings(const JsonVariantConst src, NetworkSettings& dst) { // ap - if (!src["ap"]["ssid"].isNull()) { - String value = src["ap"]["ssid"].as(); + if (!src[FPSTR(S_AP)][FPSTR(S_SSID)].isNull()) { + String value = src[FPSTR(S_AP)][FPSTR(S_SSID)].as(); - if (value.length() < sizeof(dst.ap.ssid)) { + if (value.length() < sizeof(dst.ap.ssid) && !value.equals(dst.ap.ssid)) { strcpy(dst.ap.ssid, value.c_str()); changed = true; } } - if (!src["ap"]["password"].isNull()) { - String value = src["ap"]["password"].as(); + if (!src[FPSTR(S_AP)][FPSTR(S_PASSWORD)].isNull()) { + String value = src[FPSTR(S_AP)][FPSTR(S_PASSWORD)].as(); - if (value.length() < sizeof(dst.ap.password)) { + if (value.length() < sizeof(dst.ap.password) && !value.equals(dst.ap.password)) { strcpy(dst.ap.password, value.c_str()); changed = true; } } - if (!src["ap"]["channel"].isNull()) { - unsigned char value = src["ap"]["channel"].as(); + if (!src[FPSTR(S_AP)][FPSTR(S_CHANNEL)].isNull()) { + unsigned char value = src[FPSTR(S_AP)][FPSTR(S_CHANNEL)].as(); if (value >= 0 && value < 12) { dst.ap.channel = value; @@ -310,26 +316,26 @@ bool jsonToNetworkSettings(const JsonVariantConst src, NetworkSettings& dst) { // sta - if (!src["sta"]["ssid"].isNull()) { - String value = src["sta"]["ssid"].as(); + if (!src[FPSTR(S_STA)][FPSTR(S_SSID)].isNull()) { + String value = src[FPSTR(S_STA)][FPSTR(S_SSID)].as(); - if (value.length() < sizeof(dst.sta.ssid)) { + if (value.length() < sizeof(dst.sta.ssid) && !value.equals(dst.sta.ssid)) { strcpy(dst.sta.ssid, value.c_str()); changed = true; } } - if (!src["sta"]["password"].isNull()) { - String value = src["sta"]["password"].as(); + if (!src[FPSTR(S_STA)][FPSTR(S_PASSWORD)].isNull()) { + String value = src[FPSTR(S_STA)][FPSTR(S_PASSWORD)].as(); - if (value.length() < sizeof(dst.sta.password)) { + if (value.length() < sizeof(dst.sta.password) && !value.equals(dst.sta.password)) { strcpy(dst.sta.password, value.c_str()); changed = true; } } - if (!src["sta"]["channel"].isNull()) { - unsigned char value = src["sta"]["channel"].as(); + if (!src[FPSTR(S_STA)][FPSTR(S_CHANNEL)].isNull()) { + unsigned char value = src[FPSTR(S_STA)][FPSTR(S_CHANNEL)].as(); if (value >= 0 && value < 12) { dst.sta.channel = value; @@ -342,132 +348,113 @@ bool jsonToNetworkSettings(const JsonVariantConst src, NetworkSettings& dst) { void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) { if (!safe) { - dst["system"]["logLevel"] = static_cast(src.system.logLevel); - dst["system"]["serial"]["enable"] = src.system.serial.enable; - dst["system"]["serial"]["baudrate"] = src.system.serial.baudrate; - dst["system"]["telnet"]["enable"] = src.system.telnet.enable; - dst["system"]["telnet"]["port"] = src.system.telnet.port; - dst["system"]["unitSystem"] = static_cast(src.system.unitSystem); - dst["system"]["statusLedGpio"] = src.system.statusLedGpio; - - dst["portal"]["auth"] = src.portal.auth; - dst["portal"]["login"] = src.portal.login; - dst["portal"]["password"] = src.portal.password; - - dst["opentherm"]["unitSystem"] = static_cast(src.opentherm.unitSystem); - dst["opentherm"]["inGpio"] = src.opentherm.inGpio; - dst["opentherm"]["outGpio"] = src.opentherm.outGpio; - dst["opentherm"]["rxLedGpio"] = src.opentherm.rxLedGpio; - dst["opentherm"]["memberIdCode"] = src.opentherm.memberIdCode; - dst["opentherm"]["maxModulation"] = src.opentherm.maxModulation; - dst["opentherm"]["pressureFactor"] = roundd(src.opentherm.pressureFactor, 2); - dst["opentherm"]["dhwFlowRateFactor"] = roundd(src.opentherm.dhwFlowRateFactor, 2); - dst["opentherm"]["minPower"] = roundd(src.opentherm.minPower, 2); - dst["opentherm"]["maxPower"] = roundd(src.opentherm.maxPower, 2); - dst["opentherm"]["dhwPresent"] = src.opentherm.dhwPresent; - dst["opentherm"]["summerWinterMode"] = src.opentherm.summerWinterMode; - dst["opentherm"]["heatingCh2Enabled"] = src.opentherm.heatingCh2Enabled; - dst["opentherm"]["heatingCh1ToCh2"] = src.opentherm.heatingCh1ToCh2; - dst["opentherm"]["dhwToCh2"] = src.opentherm.dhwToCh2; - dst["opentherm"]["dhwBlocking"] = src.opentherm.dhwBlocking; - dst["opentherm"]["modulationSyncWithHeating"] = src.opentherm.modulationSyncWithHeating; - dst["opentherm"]["getMinMaxTemp"] = src.opentherm.getMinMaxTemp; - dst["opentherm"]["nativeHeatingControl"] = src.opentherm.nativeHeatingControl; - dst["opentherm"]["immergasFix"] = src.opentherm.immergasFix; - dst["opentherm"]["filterNumValues"]["enable"] = src.opentherm.filterNumValues.enable; - dst["opentherm"]["filterNumValues"]["factor"] = roundd(src.opentherm.filterNumValues.factor, 2); - - dst["mqtt"]["enable"] = src.mqtt.enable; - dst["mqtt"]["server"] = src.mqtt.server; - dst["mqtt"]["port"] = src.mqtt.port; - dst["mqtt"]["user"] = src.mqtt.user; - dst["mqtt"]["password"] = src.mqtt.password; - dst["mqtt"]["prefix"] = src.mqtt.prefix; - dst["mqtt"]["interval"] = src.mqtt.interval; - dst["mqtt"]["homeAssistantDiscovery"] = src.mqtt.homeAssistantDiscovery; - - dst["emergency"]["target"] = roundd(src.emergency.target, 2); - dst["emergency"]["tresholdTime"] = src.emergency.tresholdTime; - } - - dst["heating"]["enable"] = src.heating.enable; - dst["heating"]["turbo"] = src.heating.turbo; - dst["heating"]["target"] = roundd(src.heating.target, 2); - dst["heating"]["hysteresis"] = roundd(src.heating.hysteresis, 2); - dst["heating"]["turboFactor"] = roundd(src.heating.turboFactor, 2); - dst["heating"]["minTemp"] = src.heating.minTemp; - dst["heating"]["maxTemp"] = src.heating.maxTemp; - - dst["dhw"]["enable"] = src.dhw.enable; - dst["dhw"]["target"] = roundd(src.dhw.target, 1); - dst["dhw"]["minTemp"] = src.dhw.minTemp; - dst["dhw"]["maxTemp"] = src.dhw.maxTemp; - - dst["equitherm"]["enable"] = src.equitherm.enable; - dst["equitherm"]["n_factor"] = roundd(src.equitherm.n_factor, 3); - dst["equitherm"]["k_factor"] = roundd(src.equitherm.k_factor, 3); - dst["equitherm"]["t_factor"] = roundd(src.equitherm.t_factor, 3); - - dst["pid"]["enable"] = src.pid.enable; - dst["pid"]["p_factor"] = roundd(src.pid.p_factor, 3); - dst["pid"]["i_factor"] = roundd(src.pid.i_factor, 4); - dst["pid"]["d_factor"] = roundd(src.pid.d_factor, 1); - dst["pid"]["dt"] = src.pid.dt; - dst["pid"]["minTemp"] = src.pid.minTemp; - dst["pid"]["maxTemp"] = src.pid.maxTemp; - - dst["sensors"]["outdoor"]["type"] = static_cast(src.sensors.outdoor.type); - dst["sensors"]["outdoor"]["gpio"] = src.sensors.outdoor.gpio; - - char bleAddress[18]; - sprintf( - bleAddress, - "%02x:%02x:%02x:%02x:%02x:%02x", - src.sensors.outdoor.bleAddress[0], - src.sensors.outdoor.bleAddress[1], - src.sensors.outdoor.bleAddress[2], - src.sensors.outdoor.bleAddress[3], - src.sensors.outdoor.bleAddress[4], - src.sensors.outdoor.bleAddress[5] - ); - dst["sensors"]["outdoor"]["bleAddress"] = String(bleAddress); - dst["sensors"]["outdoor"]["offset"] = roundd(src.sensors.outdoor.offset, 2); - - dst["sensors"]["indoor"]["type"] = static_cast(src.sensors.indoor.type); - dst["sensors"]["indoor"]["gpio"] = src.sensors.indoor.gpio; - - sprintf( - bleAddress, - "%02x:%02x:%02x:%02x:%02x:%02x", - src.sensors.indoor.bleAddress[0], - src.sensors.indoor.bleAddress[1], - src.sensors.indoor.bleAddress[2], - src.sensors.indoor.bleAddress[3], - src.sensors.indoor.bleAddress[4], - src.sensors.indoor.bleAddress[5] - ); - dst["sensors"]["indoor"]["bleAddress"] = String(bleAddress); - dst["sensors"]["indoor"]["offset"] = roundd(src.sensors.indoor.offset, 2); + auto system = dst[FPSTR(S_SYSTEM)].to(); + system[FPSTR(S_LOG_LEVEL)] = static_cast(src.system.logLevel); + + auto serial = system[FPSTR(S_SERIAL)].to(); + serial[FPSTR(S_ENABLED)] = src.system.serial.enabled; + serial[FPSTR(S_BAUDRATE)] = src.system.serial.baudrate; + + auto telnet = system[FPSTR(S_TELNET)].to(); + telnet[FPSTR(S_ENABLED)] = src.system.telnet.enabled; + telnet[FPSTR(S_PORT)] = src.system.telnet.port; + + system[FPSTR(S_UNIT_SYSTEM)] = static_cast(src.system.unitSystem); + system[FPSTR(S_STATUS_LED_GPIO)] = src.system.statusLedGpio; + + auto portal = dst[FPSTR(S_PORTAL)].to(); + portal[FPSTR(S_AUTH)] = src.portal.auth; + portal[FPSTR(S_LOGIN)] = src.portal.login; + portal[FPSTR(S_PASSWORD)] = src.portal.password; + + auto opentherm = dst[FPSTR(S_OPENTHERM)].to(); + opentherm[FPSTR(S_UNIT_SYSTEM)] = static_cast(src.opentherm.unitSystem); + opentherm[FPSTR(S_IN_GPIO)] = src.opentherm.inGpio; + opentherm[FPSTR(S_OUT_GPIO)] = src.opentherm.outGpio; + opentherm[FPSTR(S_RX_LED_GPIO)] = src.opentherm.rxLedGpio; + opentherm[FPSTR(S_MEMBER_ID)] = src.opentherm.memberId; + opentherm[FPSTR(S_FLAGS)] = src.opentherm.flags; + opentherm[FPSTR(S_MAX_MODULATION)] = src.opentherm.maxModulation; + opentherm[FPSTR(S_MIN_POWER)] = roundf(src.opentherm.minPower, 2); + opentherm[FPSTR(S_MAX_POWER)] = roundf(src.opentherm.maxPower, 2); + opentherm[FPSTR(S_DHW_PRESENT)] = src.opentherm.dhwPresent; + opentherm[FPSTR(S_SUMMER_WINTER_MODE)] = src.opentherm.summerWinterMode; + opentherm[FPSTR(S_HEATING_CH2_ENABLED)] = src.opentherm.heatingCh2Enabled; + opentherm[FPSTR(S_HEATING_CH1_TO_CH2)] = src.opentherm.heatingCh1ToCh2; + opentherm[FPSTR(S_DHW_TO_CH2)] = src.opentherm.dhwToCh2; + opentherm[FPSTR(S_DHW_BLOCKING)] = src.opentherm.dhwBlocking; + opentherm[FPSTR(S_MODULATION_SYNC_WITH_HEATING)] = src.opentherm.modulationSyncWithHeating; + opentherm[FPSTR(S_GET_MIN_MAX_TEMP)] = src.opentherm.getMinMaxTemp; + opentherm[FPSTR(S_NATIVE_HEATING_CONTROL)] = src.opentherm.nativeHeatingControl; + opentherm[FPSTR(S_IMMERGAS_FIX)] = src.opentherm.immergasFix; + + auto mqtt = dst[FPSTR(S_MQTT)].to(); + mqtt[FPSTR(S_ENABLED)] = src.mqtt.enabled; + mqtt[FPSTR(S_SERVER)] = src.mqtt.server; + mqtt[FPSTR(S_PORT)] = src.mqtt.port; + mqtt[FPSTR(S_USER)] = src.mqtt.user; + mqtt[FPSTR(S_PASSWORD)] = src.mqtt.password; + mqtt[FPSTR(S_PREFIX)] = src.mqtt.prefix; + mqtt[FPSTR(S_INTERVAL)] = src.mqtt.interval; + mqtt[FPSTR(S_HOME_ASSISTANT_DISCOVERY)] = src.mqtt.homeAssistantDiscovery; + + auto emergency = dst[FPSTR(S_EMERGENCY)].to(); + emergency[FPSTR(S_TARGET)] = roundf(src.emergency.target, 2); + emergency[FPSTR(S_TRESHOLD_TIME)] = src.emergency.tresholdTime; + } + + auto heating = dst[FPSTR(S_HEATING)].to(); + heating[FPSTR(S_ENABLED)] = src.heating.enabled; + heating[FPSTR(S_TURBO)] = src.heating.turbo; + heating[FPSTR(S_TARGET)] = roundf(src.heating.target, 2); + heating[FPSTR(S_HYSTERESIS)] = roundf(src.heating.hysteresis, 3); + heating[FPSTR(S_TURBO_FACTOR)] = roundf(src.heating.turboFactor, 3); + heating[FPSTR(S_MIN_TEMP)] = src.heating.minTemp; + heating[FPSTR(S_MAX_TEMP)] = src.heating.maxTemp; + + auto dhw = dst[FPSTR(S_DHW)].to(); + dhw[FPSTR(S_ENABLED)] = src.dhw.enabled; + dhw[FPSTR(S_TARGET)] = roundf(src.dhw.target, 1); + dhw[FPSTR(S_MIN_TEMP)] = src.dhw.minTemp; + dhw[FPSTR(S_MAX_TEMP)] = src.dhw.maxTemp; + + auto equitherm = dst[FPSTR(S_EQUITHERM)].to(); + equitherm[FPSTR(S_ENABLED)] = src.equitherm.enabled; + equitherm[FPSTR(S_N_FACTOR)] = roundf(src.equitherm.n_factor, 3); + equitherm[FPSTR(S_K_FACTOR)] = roundf(src.equitherm.k_factor, 3); + equitherm[FPSTR(S_T_FACTOR)] = roundf(src.equitherm.t_factor, 3); + + auto pid = dst[FPSTR(S_PID)].to(); + pid[FPSTR(S_ENABLED)] = src.pid.enabled; + pid[FPSTR(S_P_FACTOR)] = roundf(src.pid.p_factor, 3); + pid[FPSTR(S_I_FACTOR)] = roundf(src.pid.i_factor, 4); + pid[FPSTR(S_D_FACTOR)] = roundf(src.pid.d_factor, 1); + pid[FPSTR(S_DT)] = src.pid.dt; + pid[FPSTR(S_MIN_TEMP)] = src.pid.minTemp; + pid[FPSTR(S_MAX_TEMP)] = src.pid.maxTemp; if (!safe) { - dst["externalPump"]["use"] = src.externalPump.use; - dst["externalPump"]["gpio"] = src.externalPump.gpio; - dst["externalPump"]["postCirculationTime"] = roundd(src.externalPump.postCirculationTime / 60, 0); - dst["externalPump"]["antiStuckInterval"] = roundd(src.externalPump.antiStuckInterval / 86400, 0); - dst["externalPump"]["antiStuckTime"] = roundd(src.externalPump.antiStuckTime / 60, 0); - - dst["cascadeControl"]["input"]["enable"] = src.cascadeControl.input.enable; - dst["cascadeControl"]["input"]["gpio"] = src.cascadeControl.input.gpio; - dst["cascadeControl"]["input"]["invertState"] = src.cascadeControl.input.invertState; - dst["cascadeControl"]["input"]["thresholdTime"] = src.cascadeControl.input.thresholdTime; - - dst["cascadeControl"]["output"]["enable"] = src.cascadeControl.output.enable; - dst["cascadeControl"]["output"]["gpio"] = src.cascadeControl.output.gpio; - dst["cascadeControl"]["output"]["invertState"] = src.cascadeControl.output.invertState; - dst["cascadeControl"]["output"]["thresholdTime"] = src.cascadeControl.output.thresholdTime; - dst["cascadeControl"]["output"]["onFault"] = src.cascadeControl.output.onFault; - dst["cascadeControl"]["output"]["onLossConnection"] = src.cascadeControl.output.onLossConnection; - dst["cascadeControl"]["output"]["onEnabledHeating"] = src.cascadeControl.output.onEnabledHeating; + auto externalPump = dst[FPSTR(S_EXTERNAL_PUMP)].to(); + externalPump[FPSTR(S_USE)] = src.externalPump.use; + externalPump[FPSTR(S_GPIO)] = src.externalPump.gpio; + externalPump[FPSTR(S_POST_CIRCULATION_TIME)] = roundf(src.externalPump.postCirculationTime / 60, 0); + externalPump[FPSTR(S_ANTI_STUCK_INTERVAL)] = roundf(src.externalPump.antiStuckInterval / 86400, 0); + externalPump[FPSTR(S_ANTI_STUCK_TIME)] = roundf(src.externalPump.antiStuckTime / 60, 0); + + auto cascadeControlInput = dst[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_INPUT)].to(); + cascadeControlInput[FPSTR(S_ENABLED)] = src.cascadeControl.input.enabled; + cascadeControlInput[FPSTR(S_GPIO)] = src.cascadeControl.input.gpio; + cascadeControlInput[FPSTR(S_INVERT_STATE)] = src.cascadeControl.input.invertState; + cascadeControlInput[FPSTR(S_THRESHOLD_TIME)] = src.cascadeControl.input.thresholdTime; + + auto cascadeControlOutput = dst[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)].to(); + cascadeControlOutput[FPSTR(S_ENABLED)] = src.cascadeControl.output.enabled; + cascadeControlOutput[FPSTR(S_GPIO)] = src.cascadeControl.output.gpio; + cascadeControlOutput[FPSTR(S_INVERT_STATE)] = src.cascadeControl.output.invertState; + cascadeControlOutput[FPSTR(S_THRESHOLD_TIME)] = src.cascadeControl.output.thresholdTime; + cascadeControlOutput[FPSTR(S_ON_FAULT)] = src.cascadeControl.output.onFault; + cascadeControlOutput[FPSTR(S_ON_LOSS_CONNECTION)] = src.cascadeControl.output.onLossConnection; + cascadeControlOutput[FPSTR(S_ON_ENABLED_HEATING)] = src.cascadeControl.output.onEnabledHeating; } } @@ -480,8 +467,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false if (!safe) { // system - if (!src["system"]["logLevel"].isNull()) { - uint8_t value = src["system"]["logLevel"].as(); + if (!src[FPSTR(S_SYSTEM)][FPSTR(S_LOG_LEVEL)].isNull()) { + uint8_t value = src[FPSTR(S_SYSTEM)][FPSTR(S_LOG_LEVEL)].as(); if (value != dst.system.logLevel && value >= TinyLogger::Level::SILENT && value <= TinyLogger::Level::VERBOSE) { dst.system.logLevel = value; @@ -489,17 +476,17 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["system"]["serial"]["enable"].is()) { - bool value = src["system"]["serial"]["enable"].as(); + if (src[FPSTR(S_SYSTEM)][FPSTR(S_SERIAL)][FPSTR(S_ENABLED)].is()) { + bool value = src[FPSTR(S_SYSTEM)][FPSTR(S_SERIAL)][FPSTR(S_ENABLED)].as(); - if (value != dst.system.serial.enable) { - dst.system.serial.enable = value; + if (value != dst.system.serial.enabled) { + dst.system.serial.enabled = value; changed = true; } } - if (!src["system"]["serial"]["baudrate"].isNull()) { - unsigned int value = src["system"]["serial"]["baudrate"].as(); + if (!src[FPSTR(S_SYSTEM)][FPSTR(S_SERIAL)][FPSTR(S_BAUDRATE)].isNull()) { + unsigned int value = src[FPSTR(S_SYSTEM)][FPSTR(S_SERIAL)][FPSTR(S_BAUDRATE)].as(); if (value == 9600 || value == 19200 || value == 38400 || value == 57600 || value == 74880 || value == 115200) { if (value != dst.system.serial.baudrate) { @@ -509,17 +496,17 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["system"]["telnet"]["enable"].is()) { - bool value = src["system"]["telnet"]["enable"].as(); - - if (value != dst.system.telnet.enable) { - dst.system.telnet.enable = value; + if (src[FPSTR(S_SYSTEM)][FPSTR(S_TELNET)][FPSTR(S_ENABLED)].is()) { + bool value = src[FPSTR(S_SYSTEM)][FPSTR(S_TELNET)][FPSTR(S_ENABLED)].as(); + + if (value != dst.system.telnet.enabled) { + dst.system.telnet.enabled = value; changed = true; } } - if (!src["system"]["telnet"]["port"].isNull()) { - unsigned short value = src["system"]["telnet"]["port"].as(); + if (!src[FPSTR(S_SYSTEM)][FPSTR(S_TELNET)][FPSTR(S_PORT)].isNull()) { + unsigned short value = src[FPSTR(S_SYSTEM)][FPSTR(S_TELNET)][FPSTR(S_PORT)].as(); if (value > 0 && value <= 65535 && value != dst.system.telnet.port) { dst.system.telnet.port = value; @@ -527,19 +514,19 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["system"]["unitSystem"].isNull()) { - byte value = src["system"]["unitSystem"].as(); + if (!src[FPSTR(S_SYSTEM)][FPSTR(S_UNIT_SYSTEM)].isNull()) { + uint8_t value = src[FPSTR(S_SYSTEM)][FPSTR(S_UNIT_SYSTEM)].as(); UnitSystem prevUnitSystem = dst.system.unitSystem; switch (value) { - case static_cast(UnitSystem::METRIC): + case static_cast(UnitSystem::METRIC): if (dst.system.unitSystem != UnitSystem::METRIC) { dst.system.unitSystem = UnitSystem::METRIC; changed = true; } break; - case static_cast(UnitSystem::IMPERIAL): + case static_cast(UnitSystem::IMPERIAL): if (dst.system.unitSystem != UnitSystem::IMPERIAL) { dst.system.unitSystem = UnitSystem::IMPERIAL; changed = true; @@ -564,15 +551,15 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["system"]["statusLedGpio"].isNull()) { - if (src["system"]["statusLedGpio"].is() && src["system"]["statusLedGpio"].as().size() == 0) { + if (!src[FPSTR(S_SYSTEM)][FPSTR(S_STATUS_LED_GPIO)].isNull()) { + if (src[FPSTR(S_SYSTEM)][FPSTR(S_STATUS_LED_GPIO)].is() && src[FPSTR(S_SYSTEM)][FPSTR(S_STATUS_LED_GPIO)].as().size() == 0) { if (dst.system.statusLedGpio != GPIO_IS_NOT_CONFIGURED) { dst.system.statusLedGpio = GPIO_IS_NOT_CONFIGURED; changed = true; } - + } else { - unsigned char value = src["system"]["statusLedGpio"].as(); + unsigned char value = src[FPSTR(S_SYSTEM)][FPSTR(S_STATUS_LED_GPIO)].as(); if (GPIO_IS_VALID(value) && value != dst.system.statusLedGpio) { dst.system.statusLedGpio = value; @@ -583,8 +570,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false // portal - if (src["portal"]["auth"].is()) { - bool value = src["portal"]["auth"].as(); + if (src[FPSTR(S_PORTAL)][FPSTR(S_AUTH)].is()) { + bool value = src[FPSTR(S_PORTAL)][FPSTR(S_AUTH)].as(); if (value != dst.portal.auth) { dst.portal.auth = value; @@ -592,19 +579,19 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["portal"]["login"].isNull()) { - String value = src["portal"]["login"].as(); + if (!src[FPSTR(S_PORTAL)][FPSTR(S_LOGIN)].isNull()) { + String value = src[FPSTR(S_PORTAL)][FPSTR(S_LOGIN)].as(); - if (value.length() < sizeof(dst.portal.login) && !String(dst.portal.login).equals(value)) { + if (value.length() < sizeof(dst.portal.login) && !value.equals(dst.portal.login)) { strcpy(dst.portal.login, value.c_str()); changed = true; } } - if (!src["portal"]["password"].isNull()) { - String value = src["portal"]["password"].as(); + if (!src[FPSTR(S_PORTAL)][FPSTR(S_PASSWORD)].isNull()) { + String value = src[FPSTR(S_PORTAL)][FPSTR(S_PASSWORD)].as(); - if (value.length() < sizeof(dst.portal.password) && !String(dst.portal.password).equals(value)) { + if (value.length() < sizeof(dst.portal.password) && !value.equals(dst.portal.password)) { strcpy(dst.portal.password, value.c_str()); changed = true; } @@ -612,18 +599,18 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false // opentherm - if (!src["opentherm"]["unitSystem"].isNull()) { - byte value = src["opentherm"]["unitSystem"].as(); + if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_UNIT_SYSTEM)].isNull()) { + uint8_t value = src[FPSTR(S_OPENTHERM)][FPSTR(S_UNIT_SYSTEM)].as(); switch (value) { - case static_cast(UnitSystem::METRIC): + case static_cast(UnitSystem::METRIC): if (dst.opentherm.unitSystem != UnitSystem::METRIC) { dst.opentherm.unitSystem = UnitSystem::METRIC; changed = true; } break; - case static_cast(UnitSystem::IMPERIAL): + case static_cast(UnitSystem::IMPERIAL): if (dst.opentherm.unitSystem != UnitSystem::IMPERIAL) { dst.opentherm.unitSystem = UnitSystem::IMPERIAL; changed = true; @@ -635,15 +622,15 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["opentherm"]["inGpio"].isNull()) { - if (src["opentherm"]["inGpio"].is() && src["opentherm"]["inGpio"].as().size() == 0) { + if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_IN_GPIO)].isNull()) { + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_IN_GPIO)].is() && src[FPSTR(S_OPENTHERM)][FPSTR(S_IN_GPIO)].as().size() == 0) { if (dst.opentherm.inGpio != GPIO_IS_NOT_CONFIGURED) { dst.opentherm.inGpio = GPIO_IS_NOT_CONFIGURED; changed = true; } - + } else { - unsigned char value = src["opentherm"]["inGpio"].as(); + unsigned char value = src[FPSTR(S_OPENTHERM)][FPSTR(S_IN_GPIO)].as(); if (GPIO_IS_VALID(value) && value != dst.opentherm.inGpio) { dst.opentherm.inGpio = value; @@ -651,16 +638,16 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } } - - if (!src["opentherm"]["outGpio"].isNull()) { - if (src["opentherm"]["outGpio"].is() && src["opentherm"]["outGpio"].as().size() == 0) { + + if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_OUT_GPIO)].isNull()) { + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_OUT_GPIO)].is() && src[FPSTR(S_OPENTHERM)][FPSTR(S_OUT_GPIO)].as().size() == 0) { if (dst.opentherm.outGpio != GPIO_IS_NOT_CONFIGURED) { dst.opentherm.outGpio = GPIO_IS_NOT_CONFIGURED; changed = true; } - + } else { - unsigned char value = src["opentherm"]["outGpio"].as(); + unsigned char value = src[FPSTR(S_OPENTHERM)][FPSTR(S_OUT_GPIO)].as(); if (GPIO_IS_VALID(value) && value != dst.opentherm.outGpio) { dst.opentherm.outGpio = value; @@ -669,15 +656,15 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["opentherm"]["rxLedGpio"].isNull()) { - if (src["opentherm"]["rxLedGpio"].is() && src["opentherm"]["rxLedGpio"].as().size() == 0) { + if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_RX_LED_GPIO)].isNull()) { + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_RX_LED_GPIO)].is() && src[FPSTR(S_OPENTHERM)][FPSTR(S_RX_LED_GPIO)].as().size() == 0) { if (dst.opentherm.rxLedGpio != GPIO_IS_NOT_CONFIGURED) { dst.opentherm.rxLedGpio = GPIO_IS_NOT_CONFIGURED; changed = true; } - + } else { - unsigned char value = src["opentherm"]["rxLedGpio"].as(); + unsigned char value = src[FPSTR(S_OPENTHERM)][FPSTR(S_RX_LED_GPIO)].as(); if (GPIO_IS_VALID(value) && value != dst.opentherm.rxLedGpio) { dst.opentherm.rxLedGpio = value; @@ -686,80 +673,53 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["opentherm"]["memberIdCode"].isNull()) { - unsigned int value = src["opentherm"]["memberIdCode"].as(); - - if (value >= 0 && value < 65536 && value != dst.opentherm.memberIdCode) { - dst.opentherm.memberIdCode = value; - changed = true; - } - } - - if (!src["opentherm"]["maxModulation"].isNull()) { - unsigned char value = src["opentherm"]["maxModulation"].as(); - - if (value > 0 && value <= 100 && value != dst.opentherm.maxModulation) { - dst.opentherm.maxModulation = value; - changed = true; - } - } - - if (!src["opentherm"]["pressureFactor"].isNull()) { - float value = src["opentherm"]["pressureFactor"].as(); - - if (value > 0 && value <= 100 && fabs(value - dst.opentherm.pressureFactor) > 0.0001f) { - dst.opentherm.pressureFactor = roundd(value, 2); - changed = true; - } - } - - if (!src["opentherm"]["dhwFlowRateFactor"].isNull()) { - float value = src["opentherm"]["dhwFlowRateFactor"].as(); + if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_MEMBER_ID)].isNull()) { + auto value = src[FPSTR(S_OPENTHERM)][FPSTR(S_MEMBER_ID)].as(); - if (value > 0 && value <= 100 && fabs(value - dst.opentherm.dhwFlowRateFactor) > 0.0001f) { - dst.opentherm.dhwFlowRateFactor = roundd(value, 2); + if (value != dst.opentherm.memberId) { + dst.opentherm.memberId = value; changed = true; } } - if (!src["opentherm"]["minPower"].isNull()) { - float value = src["opentherm"]["minPower"].as(); + if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_FLAGS)].isNull()) { + auto value = src[FPSTR(S_OPENTHERM)][FPSTR(S_FLAGS)].as(); - if (value >= 0 && value <= 1000 && fabs(value - dst.opentherm.minPower) > 0.0001f) { - dst.opentherm.minPower = roundd(value, 2); + if (value != dst.opentherm.flags) { + dst.opentherm.flags = value; changed = true; } } - if (!src["opentherm"]["maxPower"].isNull()) { - float value = src["opentherm"]["maxPower"].as(); + if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_MAX_MODULATION)].isNull()) { + unsigned char value = src[FPSTR(S_OPENTHERM)][FPSTR(S_MAX_MODULATION)].as(); - if (value >= 0 && value <= 1000 && fabs(value - dst.opentherm.maxPower) > 0.0001f) { - dst.opentherm.maxPower = roundd(value, 2); + if (value > 0 && value <= 100 && value != dst.opentherm.maxModulation) { + dst.opentherm.maxModulation = value; changed = true; } } - if (src["opentherm"]["filterNumValues"]["enable"].is()) { - bool value = src["opentherm"]["filterNumValues"]["enable"].as(); + if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_MIN_POWER)].isNull()) { + float value = src[FPSTR(S_OPENTHERM)][FPSTR(S_MIN_POWER)].as(); - if (value != dst.opentherm.filterNumValues.enable) { - dst.opentherm.filterNumValues.enable = value; + if (value >= 0 && value <= 1000 && fabsf(value - dst.opentherm.minPower) > 0.0001f) { + dst.opentherm.minPower = roundf(value, 2); changed = true; } } - if (!src["opentherm"]["filterNumValues"]["factor"].isNull()) { - float value = src["opentherm"]["filterNumValues"]["factor"].as(); + if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_MAX_POWER)].isNull()) { + float value = src[FPSTR(S_OPENTHERM)][FPSTR(S_MAX_POWER)].as(); - if (value > 0 && value <= 1 && fabs(value - dst.opentherm.filterNumValues.factor) > 0.0001f) { - dst.opentherm.filterNumValues.factor = roundd(value, 2); + if (value >= 0 && value <= 1000 && fabsf(value - dst.opentherm.maxPower) > 0.0001f) { + dst.opentherm.maxPower = roundf(value, 2); changed = true; } } - if (src["opentherm"]["dhwPresent"].is()) { - bool value = src["opentherm"]["dhwPresent"].as(); + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_DHW_PRESENT)].is()) { + bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_DHW_PRESENT)].as(); if (value != dst.opentherm.dhwPresent) { dst.opentherm.dhwPresent = value; @@ -767,8 +727,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["opentherm"]["summerWinterMode"].is()) { - bool value = src["opentherm"]["summerWinterMode"].as(); + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_SUMMER_WINTER_MODE)].is()) { + bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_SUMMER_WINTER_MODE)].as(); if (value != dst.opentherm.summerWinterMode) { dst.opentherm.summerWinterMode = value; @@ -776,8 +736,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["opentherm"]["heatingCh2Enabled"].is()) { - bool value = src["opentherm"]["heatingCh2Enabled"].as(); + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_HEATING_CH2_ENABLED)].is()) { + bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_HEATING_CH2_ENABLED)].as(); if (value != dst.opentherm.heatingCh2Enabled) { dst.opentherm.heatingCh2Enabled = value; @@ -791,8 +751,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["opentherm"]["heatingCh1ToCh2"].is()) { - bool value = src["opentherm"]["heatingCh1ToCh2"].as(); + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_HEATING_CH1_TO_CH2)].is()) { + bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_HEATING_CH1_TO_CH2)].as(); if (value != dst.opentherm.heatingCh1ToCh2) { dst.opentherm.heatingCh1ToCh2 = value; @@ -806,8 +766,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["opentherm"]["dhwToCh2"].is()) { - bool value = src["opentherm"]["dhwToCh2"].as(); + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_DHW_TO_CH2)].is()) { + bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_DHW_TO_CH2)].as(); if (value != dst.opentherm.dhwToCh2) { dst.opentherm.dhwToCh2 = value; @@ -821,8 +781,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["opentherm"]["dhwBlocking"].is()) { - bool value = src["opentherm"]["dhwBlocking"].as(); + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_DHW_BLOCKING)].is()) { + bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_DHW_BLOCKING)].as(); if (value != dst.opentherm.dhwBlocking) { dst.opentherm.dhwBlocking = value; @@ -830,8 +790,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["opentherm"]["modulationSyncWithHeating"].is()) { - bool value = src["opentherm"]["modulationSyncWithHeating"].as(); + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_MODULATION_SYNC_WITH_HEATING)].is()) { + bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_MODULATION_SYNC_WITH_HEATING)].as(); if (value != dst.opentherm.modulationSyncWithHeating) { dst.opentherm.modulationSyncWithHeating = value; @@ -839,8 +799,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["opentherm"]["getMinMaxTemp"].is()) { - bool value = src["opentherm"]["getMinMaxTemp"].as(); + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_GET_MIN_MAX_TEMP)].is()) { + bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_GET_MIN_MAX_TEMP)].as(); if (value != dst.opentherm.getMinMaxTemp) { dst.opentherm.getMinMaxTemp = value; @@ -848,23 +808,23 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["opentherm"]["nativeHeatingControl"].is()) { - bool value = src["opentherm"]["nativeHeatingControl"].as(); + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_NATIVE_HEATING_CONTROL)].is()) { + bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_NATIVE_HEATING_CONTROL)].as(); if (value != dst.opentherm.nativeHeatingControl) { dst.opentherm.nativeHeatingControl = value; if (value) { - dst.equitherm.enable = false; - dst.pid.enable = false; + dst.equitherm.enabled = false; + dst.pid.enabled = false; } changed = true; } } - if (src["opentherm"]["immergasFix"].is()) { - bool value = src["opentherm"]["immergasFix"].as(); + if (src[FPSTR(S_OPENTHERM)][FPSTR(S_IMMERGAS_FIX)].is()) { + bool value = src[FPSTR(S_OPENTHERM)][FPSTR(S_IMMERGAS_FIX)].as(); if (value != dst.opentherm.immergasFix) { dst.opentherm.immergasFix = value; @@ -874,26 +834,26 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false // mqtt - if (src["mqtt"]["enable"].is()) { - bool value = src["mqtt"]["enable"].as(); + if (src[FPSTR(S_MQTT)][FPSTR(S_ENABLED)].is()) { + bool value = src[FPSTR(S_MQTT)][FPSTR(S_ENABLED)].as(); - if (value != dst.mqtt.enable) { - dst.mqtt.enable = value; + if (value != dst.mqtt.enabled) { + dst.mqtt.enabled = value; changed = true; } } - - if (!src["mqtt"]["server"].isNull()) { - String value = src["mqtt"]["server"].as(); - if (value.length() < sizeof(dst.mqtt.server) && !String(dst.mqtt.server).equals(value)) { + if (!src[FPSTR(S_MQTT)][FPSTR(S_SERVER)].isNull()) { + String value = src[FPSTR(S_MQTT)][FPSTR(S_SERVER)].as(); + + if (value.length() < sizeof(dst.mqtt.server) && !value.equals(dst.mqtt.server)) { strcpy(dst.mqtt.server, value.c_str()); changed = true; } } - if (!src["mqtt"]["port"].isNull()) { - unsigned short value = src["mqtt"]["port"].as(); + if (!src[FPSTR(S_MQTT)][FPSTR(S_PORT)].isNull()) { + unsigned short value = src[FPSTR(S_MQTT)][FPSTR(S_PORT)].as(); if (value > 0 && value <= 65535 && value != dst.mqtt.port) { dst.mqtt.port = value; @@ -901,35 +861,35 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["mqtt"]["user"].isNull()) { - String value = src["mqtt"]["user"].as(); + if (!src[FPSTR(S_MQTT)][FPSTR(S_USER)].isNull()) { + String value = src[FPSTR(S_MQTT)][FPSTR(S_USER)].as(); - if (value.length() < sizeof(dst.mqtt.user) && !String(dst.mqtt.user).equals(value)) { + if (value.length() < sizeof(dst.mqtt.user) && !value.equals(dst.mqtt.user)) { strcpy(dst.mqtt.user, value.c_str()); changed = true; } } - if (!src["mqtt"]["password"].isNull()) { - String value = src["mqtt"]["password"].as(); + if (!src[FPSTR(S_MQTT)][FPSTR(S_PASSWORD)].isNull()) { + String value = src[FPSTR(S_MQTT)][FPSTR(S_PASSWORD)].as(); - if (value.length() < sizeof(dst.mqtt.password) && !String(dst.mqtt.password).equals(value)) { + if (value.length() < sizeof(dst.mqtt.password) && !value.equals(dst.mqtt.password)) { strcpy(dst.mqtt.password, value.c_str()); changed = true; } } - if (!src["mqtt"]["prefix"].isNull()) { - String value = src["mqtt"]["prefix"].as(); + if (!src[FPSTR(S_MQTT)][FPSTR(S_PREFIX)].isNull()) { + String value = src[FPSTR(S_MQTT)][FPSTR(S_PREFIX)].as(); - if (value.length() < sizeof(dst.mqtt.prefix) && !String(dst.mqtt.prefix).equals(value)) { + if (value.length() < sizeof(dst.mqtt.prefix) && !value.equals(dst.mqtt.prefix)) { strcpy(dst.mqtt.prefix, value.c_str()); changed = true; } } - if (!src["mqtt"]["interval"].isNull()) { - unsigned short value = src["mqtt"]["interval"].as(); + if (!src[FPSTR(S_MQTT)][FPSTR(S_INTERVAL)].isNull()) { + unsigned short value = src[FPSTR(S_MQTT)][FPSTR(S_INTERVAL)].as(); if (value >= 3 && value <= 60 && value != dst.mqtt.interval) { dst.mqtt.interval = value; @@ -937,8 +897,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["mqtt"]["homeAssistantDiscovery"].is()) { - bool value = src["mqtt"]["homeAssistantDiscovery"].as(); + if (src[FPSTR(S_MQTT)][FPSTR(S_HOME_ASSISTANT_DISCOVERY)].is()) { + bool value = src[FPSTR(S_MQTT)][FPSTR(S_HOME_ASSISTANT_DISCOVERY)].as(); if (value != dst.mqtt.homeAssistantDiscovery) { dst.mqtt.homeAssistantDiscovery = value; @@ -948,8 +908,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false // emergency - if (!src["emergency"]["tresholdTime"].isNull()) { - unsigned short value = src["emergency"]["tresholdTime"].as(); + if (!src[FPSTR(S_EMERGENCY)][FPSTR(S_TRESHOLD_TIME)].isNull()) { + unsigned short value = src[FPSTR(S_EMERGENCY)][FPSTR(S_TRESHOLD_TIME)].as(); if (value >= 60 && value <= 1800 && value != dst.emergency.tresholdTime) { dst.emergency.tresholdTime = value; @@ -960,94 +920,94 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false // equitherm - if (src["equitherm"]["enable"].is()) { - bool value = src["equitherm"]["enable"].as(); + if (src[FPSTR(S_EQUITHERM)][FPSTR(S_ENABLED)].is()) { + bool value = src[FPSTR(S_EQUITHERM)][FPSTR(S_ENABLED)].as(); if (!dst.opentherm.nativeHeatingControl) { - if (value != dst.equitherm.enable) { - dst.equitherm.enable = value; + if (value != dst.equitherm.enabled) { + dst.equitherm.enabled = value; changed = true; } - - } else if (dst.equitherm.enable) { - dst.equitherm.enable = false; + + } else if (dst.equitherm.enabled) { + dst.equitherm.enabled = false; changed = true; } } - if (!src["equitherm"]["n_factor"].isNull()) { - float value = src["equitherm"]["n_factor"].as(); + if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_N_FACTOR)].isNull()) { + float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_N_FACTOR)].as(); - if (value > 0 && value <= 10 && fabs(value - dst.equitherm.n_factor) > 0.0001f) { - dst.equitherm.n_factor = roundd(value, 3); + if (value > 0 && value <= 10 && fabsf(value - dst.equitherm.n_factor) > 0.0001f) { + dst.equitherm.n_factor = roundf(value, 3); changed = true; } } - if (!src["equitherm"]["k_factor"].isNull()) { - float value = src["equitherm"]["k_factor"].as(); + if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_K_FACTOR)].isNull()) { + float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_K_FACTOR)].as(); - if (value >= 0 && value <= 10 && fabs(value - dst.equitherm.k_factor) > 0.0001f) { - dst.equitherm.k_factor = roundd(value, 3); + if (value >= 0 && value <= 10 && fabsf(value - dst.equitherm.k_factor) > 0.0001f) { + dst.equitherm.k_factor = roundf(value, 3); changed = true; } } - if (!src["equitherm"]["t_factor"].isNull()) { - float value = src["equitherm"]["t_factor"].as(); + if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_T_FACTOR)].isNull()) { + float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_T_FACTOR)].as(); - if (value >= 0 && value <= 10 && fabs(value - dst.equitherm.t_factor) > 0.0001f) { - dst.equitherm.t_factor = roundd(value, 3); + if (value >= 0 && value <= 10 && fabsf(value - dst.equitherm.t_factor) > 0.0001f) { + dst.equitherm.t_factor = roundf(value, 3); changed = true; } } // pid - if (src["pid"]["enable"].is()) { - bool value = src["pid"]["enable"].as(); - + if (src[FPSTR(S_PID)][FPSTR(S_ENABLED)].is()) { + bool value = src[FPSTR(S_PID)][FPSTR(S_ENABLED)].as(); + if (!dst.opentherm.nativeHeatingControl) { - if (value != dst.pid.enable) { - dst.pid.enable = value; + if (value != dst.pid.enabled) { + dst.pid.enabled = value; changed = true; } - } else if (dst.pid.enable) { - dst.pid.enable = false; + } else if (dst.pid.enabled) { + dst.pid.enabled = false; changed = true; } } - if (!src["pid"]["p_factor"].isNull()) { - float value = src["pid"]["p_factor"].as(); + if (!src[FPSTR(S_PID)][FPSTR(S_P_FACTOR)].isNull()) { + float value = src[FPSTR(S_PID)][FPSTR(S_P_FACTOR)].as(); - if (value > 0 && value <= 1000 && fabs(value - dst.pid.p_factor) > 0.0001f) { - dst.pid.p_factor = roundd(value, 3); + if (value > 0 && value <= 1000 && fabsf(value - dst.pid.p_factor) > 0.0001f) { + dst.pid.p_factor = roundf(value, 3); changed = true; } } - if (!src["pid"]["i_factor"].isNull()) { - float value = src["pid"]["i_factor"].as(); + if (!src[FPSTR(S_PID)][FPSTR(S_I_FACTOR)].isNull()) { + float value = src[FPSTR(S_PID)][FPSTR(S_I_FACTOR)].as(); - if (value >= 0 && value <= 100 && fabs(value - dst.pid.i_factor) > 0.0001f) { - dst.pid.i_factor = roundd(value, 4); + if (value >= 0 && value <= 100 && fabsf(value - dst.pid.i_factor) > 0.0001f) { + dst.pid.i_factor = roundf(value, 4); changed = true; } } - if (!src["pid"]["d_factor"].isNull()) { - float value = src["pid"]["d_factor"].as(); + if (!src[FPSTR(S_PID)][FPSTR(S_D_FACTOR)].isNull()) { + float value = src[FPSTR(S_PID)][FPSTR(S_D_FACTOR)].as(); - if (value >= 0 && value <= 100000 && fabs(value - dst.pid.d_factor) > 0.0001f) { - dst.pid.d_factor = roundd(value, 1); + if (value >= 0 && value <= 100000 && fabsf(value - dst.pid.d_factor) > 0.0001f) { + dst.pid.d_factor = roundf(value, 1); changed = true; } } - if (!src["pid"]["dt"].isNull()) { - unsigned short value = src["pid"]["dt"].as(); + if (!src[FPSTR(S_PID)][FPSTR(S_DT)].isNull()) { + unsigned short value = src[FPSTR(S_PID)][FPSTR(S_DT)].as(); if (value >= 30 && value <= 1800 && value != dst.pid.dt) { dst.pid.dt = value; @@ -1055,17 +1015,17 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["pid"]["minTemp"].isNull()) { - short value = src["pid"]["minTemp"].as(); + if (!src[FPSTR(S_PID)][FPSTR(S_MIN_TEMP)].isNull()) { + short value = src[FPSTR(S_PID)][FPSTR(S_MIN_TEMP)].as(); - if (isValidTemp(value, dst.system.unitSystem, dst.equitherm.enable ? -99.9f : 0.0f) && value != dst.pid.minTemp) { + if (isValidTemp(value, dst.system.unitSystem, dst.equitherm.enabled ? -99.9f : 0.0f) && value != dst.pid.minTemp) { dst.pid.minTemp = value; changed = true; } } - if (!src["pid"]["maxTemp"].isNull()) { - short value = src["pid"]["maxTemp"].as(); + if (!src[FPSTR(S_PID)][FPSTR(S_MAX_TEMP)].isNull()) { + short value = src[FPSTR(S_PID)][FPSTR(S_MAX_TEMP)].as(); if (isValidTemp(value, dst.system.unitSystem) && value != dst.pid.maxTemp) { dst.pid.maxTemp = value; @@ -1080,17 +1040,17 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false // heating - if (src["heating"]["enable"].is()) { - bool value = src["heating"]["enable"].as(); + if (src[FPSTR(S_HEATING)][FPSTR(S_ENABLED)].is()) { + bool value = src[FPSTR(S_HEATING)][FPSTR(S_ENABLED)].as(); - if (value != dst.heating.enable) { - dst.heating.enable = value; + if (value != dst.heating.enabled) { + dst.heating.enabled = value; changed = true; } } - if (src["heating"]["turbo"].is()) { - bool value = src["heating"]["turbo"].as(); + if (src[FPSTR(S_HEATING)][FPSTR(S_TURBO)].is()) { + bool value = src[FPSTR(S_HEATING)][FPSTR(S_TURBO)].as(); if (value != dst.heating.turbo) { dst.heating.turbo = value; @@ -1098,37 +1058,37 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["heating"]["hysteresis"].isNull()) { - float value = src["heating"]["hysteresis"].as(); + if (!src[FPSTR(S_HEATING)][FPSTR(S_HYSTERESIS)].isNull()) { + float value = src[FPSTR(S_HEATING)][FPSTR(S_HYSTERESIS)].as(); - if (value >= 0.0f && value <= 15.0f && fabs(value - dst.heating.hysteresis) > 0.0001f) { - dst.heating.hysteresis = roundd(value, 2); + if (value >= 0.0f && value <= 15.0f && fabsf(value - dst.heating.hysteresis) > 0.0001f) { + dst.heating.hysteresis = roundf(value, 2); changed = true; } } - if (!src["heating"]["turboFactor"].isNull()) { - float value = src["heating"]["turboFactor"].as(); + if (!src[FPSTR(S_HEATING)][FPSTR(S_TURBO_FACTOR)].isNull()) { + float value = src[FPSTR(S_HEATING)][FPSTR(S_TURBO_FACTOR)].as(); - if (value >= 1.5f && value <= 10.0f && fabs(value - dst.heating.turboFactor) > 0.0001f) { - dst.heating.turboFactor = roundd(value, 2); + if (value >= 1.5f && value <= 10.0f && fabsf(value - dst.heating.turboFactor) > 0.0001f) { + dst.heating.turboFactor = roundf(value, 3); changed = true; } } - if (!src["heating"]["minTemp"].isNull()) { - unsigned char value = src["heating"]["minTemp"].as(); + if (!src[FPSTR(S_HEATING)][FPSTR(S_MIN_TEMP)].isNull()) { + unsigned char value = src[FPSTR(S_HEATING)][FPSTR(S_MIN_TEMP)].as(); - if (value != dst.heating.minTemp && value >= vars.parameters.heatingMinTemp && value < vars.parameters.heatingMaxTemp && value != dst.heating.minTemp) { + if (value != dst.heating.minTemp && value >= vars.slave.heating.minTemp && value < vars.slave.heating.maxTemp && value != dst.heating.minTemp) { dst.heating.minTemp = value; changed = true; } } - if (!src["heating"]["maxTemp"].isNull()) { - unsigned char value = src["heating"]["maxTemp"].as(); + if (!src[FPSTR(S_HEATING)][FPSTR(S_MAX_TEMP)].isNull()) { + unsigned char value = src[FPSTR(S_HEATING)][FPSTR(S_MAX_TEMP)].as(); - if (value != dst.heating.maxTemp && value > vars.parameters.heatingMinTemp && value <= vars.parameters.heatingMaxTemp && value != dst.heating.maxTemp) { + if (value != dst.heating.maxTemp && value > vars.slave.heating.minTemp && value <= vars.slave.heating.maxTemp && value != dst.heating.maxTemp) { dst.heating.maxTemp = value; changed = true; } @@ -1141,28 +1101,28 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false // dhw - if (src["dhw"]["enable"].is()) { - bool value = src["dhw"]["enable"].as(); + if (src[FPSTR(S_DHW)][FPSTR(S_ENABLED)].is()) { + bool value = src[FPSTR(S_DHW)][FPSTR(S_ENABLED)].as(); - if (value != dst.dhw.enable) { - dst.dhw.enable = value; + if (value != dst.dhw.enabled) { + dst.dhw.enabled = value; changed = true; } } - if (!src["dhw"]["minTemp"].isNull()) { - unsigned char value = src["dhw"]["minTemp"].as(); + if (!src[FPSTR(S_DHW)][FPSTR(S_MIN_TEMP)].isNull()) { + unsigned char value = src[FPSTR(S_DHW)][FPSTR(S_MIN_TEMP)].as(); - if (value >= vars.parameters.dhwMinTemp && value != dst.dhw.minTemp) { + if (value >= vars.slave.dhw.minTemp && value < vars.slave.dhw.maxTemp && value != dst.dhw.minTemp) { dst.dhw.minTemp = value; changed = true; } } - if (!src["dhw"]["maxTemp"].isNull()) { - unsigned char value = src["dhw"]["maxTemp"].as(); + if (!src[FPSTR(S_DHW)][FPSTR(S_MAX_TEMP)].isNull()) { + unsigned char value = src[FPSTR(S_DHW)][FPSTR(S_MAX_TEMP)].as(); - if (value > vars.parameters.dhwMinTemp && value != dst.dhw.maxTemp) { + if (value > vars.slave.dhw.minTemp && value <= vars.slave.dhw.maxTemp && value != dst.dhw.maxTemp) { dst.dhw.maxTemp = value; changed = true; } @@ -1173,172 +1133,11 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false changed = true; } - // sensors - if (!src["sensors"]["outdoor"]["type"].isNull()) { - byte value = src["sensors"]["outdoor"]["type"].as(); - - switch (value) { - case static_cast(SensorType::BOILER_OUTDOOR): - if (dst.sensors.outdoor.type != SensorType::BOILER_OUTDOOR) { - dst.sensors.outdoor.type = SensorType::BOILER_OUTDOOR; - changed = true; - } - break; - - case static_cast(SensorType::MANUAL): - if (dst.sensors.outdoor.type != SensorType::MANUAL) { - dst.sensors.outdoor.type = SensorType::MANUAL; - changed = true; - } - break; - - case static_cast(SensorType::DS18B20): - if (dst.sensors.outdoor.type != SensorType::DS18B20) { - dst.sensors.outdoor.type = SensorType::DS18B20; - changed = true; - } - break; - - #if USE_BLE - case static_cast(SensorType::BLUETOOTH): - if (dst.sensors.outdoor.type != SensorType::BLUETOOTH) { - dst.sensors.outdoor.type = SensorType::BLUETOOTH; - changed = true; - } - break; - #endif - - default: - break; - } - } - - if (!src["sensors"]["outdoor"]["gpio"].isNull()) { - if (src["sensors"]["outdoor"]["gpio"].is() && src["sensors"]["outdoor"]["gpio"].as().size() == 0) { - if (dst.sensors.outdoor.gpio != GPIO_IS_NOT_CONFIGURED) { - dst.sensors.outdoor.gpio = GPIO_IS_NOT_CONFIGURED; - changed = true; - } - - } else { - unsigned char value = src["sensors"]["outdoor"]["gpio"].as(); - - if (GPIO_IS_VALID(value) && value != dst.sensors.outdoor.gpio) { - dst.sensors.outdoor.gpio = value; - changed = true; - } - } - } - - #if USE_BLE - if (!src["sensors"]["outdoor"]["bleAddress"].isNull()) { - String value = src["sensors"]["outdoor"]["bleAddress"].as(); - int tmp[6]; - if(sscanf(value.c_str(), "%02x:%02x:%02x:%02x:%02x:%02x", &tmp[0], &tmp[1], &tmp[2], &tmp[3], &tmp[4], &tmp[5]) == 6) { - for(uint8_t i = 0; i < 6; i++) { - if (dst.sensors.outdoor.bleAddress[i] != (uint8_t) tmp[i]) { - dst.sensors.outdoor.bleAddress[i] = (uint8_t) tmp[i]; - changed = true; - } - } - } - } - #endif - - if (!src["sensors"]["outdoor"]["offset"].isNull()) { - float value = src["sensors"]["outdoor"]["offset"].as(); - - if (value >= -20.0f && value <= 20.0f && fabs(value - dst.sensors.outdoor.offset) > 0.0001f) { - dst.sensors.outdoor.offset = roundd(value, 2); - changed = true; - } - } - - if (!src["sensors"]["indoor"]["type"].isNull()) { - byte value = src["sensors"]["indoor"]["type"].as(); - - switch (value) { - case static_cast(SensorType::BOILER_RETURN): - if (dst.sensors.indoor.type != SensorType::BOILER_RETURN) { - dst.sensors.indoor.type = SensorType::BOILER_RETURN; - changed = true; - } - break; - - case static_cast(SensorType::MANUAL): - if (dst.sensors.indoor.type != SensorType::MANUAL) { - dst.sensors.indoor.type = SensorType::MANUAL; - changed = true; - } - break; - - case static_cast(SensorType::DS18B20): - if (dst.sensors.indoor.type != SensorType::DS18B20) { - dst.sensors.indoor.type = SensorType::DS18B20; - changed = true; - } - break; - - #if USE_BLE - case static_cast(SensorType::BLUETOOTH): - if (dst.sensors.indoor.type != SensorType::BLUETOOTH) { - dst.sensors.indoor.type = SensorType::BLUETOOTH; - changed = true; - } - break; - #endif - - default: - break; - } - } - - if (!src["sensors"]["indoor"]["gpio"].isNull()) { - if (src["sensors"]["indoor"]["gpio"].is() && src["sensors"]["indoor"]["gpio"].as().size() == 0) { - if (dst.sensors.indoor.gpio != GPIO_IS_NOT_CONFIGURED) { - dst.sensors.indoor.gpio = GPIO_IS_NOT_CONFIGURED; - changed = true; - } - - } else { - unsigned char value = src["sensors"]["indoor"]["gpio"].as(); - - if (GPIO_IS_VALID(value) && value != dst.sensors.indoor.gpio) { - dst.sensors.indoor.gpio = value; - changed = true; - } - } - } - - #if USE_BLE - if (!src["sensors"]["indoor"]["bleAddress"].isNull()) { - String value = src["sensors"]["indoor"]["bleAddress"].as(); - int tmp[6]; - if(sscanf(value.c_str(), "%02x:%02x:%02x:%02x:%02x:%02x", &tmp[0], &tmp[1], &tmp[2], &tmp[3], &tmp[4], &tmp[5]) == 6) { - for(uint8_t i = 0; i < 6; i++) { - if (dst.sensors.indoor.bleAddress[i] != (uint8_t) tmp[i]) { - dst.sensors.indoor.bleAddress[i] = (uint8_t) tmp[i]; - changed = true; - } - } - } - } - #endif - - if (!src["sensors"]["indoor"]["offset"].isNull()) { - float value = src["sensors"]["indoor"]["offset"].as(); - - if (value >= -20.0f && value <= 20.0f && fabs(value - dst.sensors.indoor.offset) > 0.0001f) { - dst.sensors.indoor.offset = roundd(value, 2); - changed = true; - } - } - if (!safe) { // external pump - if (src["externalPump"]["use"].is()) { - bool value = src["externalPump"]["use"].as(); + if (src[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_USE)].is()) { + bool value = src[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_USE)].as(); if (value != dst.externalPump.use) { dst.externalPump.use = value; @@ -1346,15 +1145,15 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["externalPump"]["gpio"].isNull()) { - if (src["externalPump"]["gpio"].is() && src["externalPump"]["gpio"].as().size() == 0) { + if (!src[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_GPIO)].isNull()) { + if (src[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_GPIO)].is() && src[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_GPIO)].as().size() == 0) { if (dst.externalPump.gpio != GPIO_IS_NOT_CONFIGURED) { dst.externalPump.gpio = GPIO_IS_NOT_CONFIGURED; changed = true; } - + } else { - unsigned char value = src["externalPump"]["gpio"].as(); + unsigned char value = src[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_GPIO)].as(); if (GPIO_IS_VALID(value) && value != dst.externalPump.gpio) { dst.externalPump.gpio = value; @@ -1363,8 +1162,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["externalPump"]["postCirculationTime"].isNull()) { - unsigned short value = src["externalPump"]["postCirculationTime"].as(); + if (!src[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_POST_CIRCULATION_TIME)].isNull()) { + unsigned short value = src[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_POST_CIRCULATION_TIME)].as(); if (value >= 0 && value <= 120) { value = value * 60; @@ -1376,8 +1175,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["externalPump"]["antiStuckInterval"].isNull()) { - unsigned int value = src["externalPump"]["antiStuckInterval"].as(); + if (!src[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_ANTI_STUCK_INTERVAL)].isNull()) { + unsigned int value = src[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_ANTI_STUCK_INTERVAL)].as(); if (value >= 0 && value <= 366) { value = value * 86400; @@ -1389,8 +1188,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["externalPump"]["antiStuckTime"].isNull()) { - unsigned short value = src["externalPump"]["antiStuckTime"].as(); + if (!src[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_ANTI_STUCK_TIME)].isNull()) { + unsigned short value = src[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_ANTI_STUCK_TIME)].as(); if (value >= 0 && value <= 20) { value = value * 60; @@ -1404,24 +1203,24 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false // cascade control - if (src["cascadeControl"]["input"]["enable"].is()) { - bool value = src["cascadeControl"]["input"]["enable"].as(); + if (src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_INPUT)][FPSTR(S_ENABLED)].is()) { + bool value = src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_INPUT)][FPSTR(S_ENABLED)].as(); - if (value != dst.cascadeControl.input.enable) { - dst.cascadeControl.input.enable = value; + if (value != dst.cascadeControl.input.enabled) { + dst.cascadeControl.input.enabled = value; changed = true; } } - if (!src["cascadeControl"]["input"]["gpio"].isNull()) { - if (src["cascadeControl"]["input"]["gpio"].is() && src["cascadeControl"]["input"]["gpio"].as().size() == 0) { + if (!src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_INPUT)][FPSTR(S_GPIO)].isNull()) { + if (src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_INPUT)][FPSTR(S_GPIO)].is() && src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_INPUT)][FPSTR(S_GPIO)].as().size() == 0) { if (dst.cascadeControl.input.gpio != GPIO_IS_NOT_CONFIGURED) { dst.cascadeControl.input.gpio = GPIO_IS_NOT_CONFIGURED; changed = true; } - + } else { - unsigned char value = src["cascadeControl"]["input"]["gpio"].as(); + unsigned char value = src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_INPUT)][FPSTR(S_GPIO)].as(); if (GPIO_IS_VALID(value) && value != dst.cascadeControl.input.gpio) { dst.cascadeControl.input.gpio = value; @@ -1430,8 +1229,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["cascadeControl"]["input"]["invertState"].is()) { - bool value = src["cascadeControl"]["input"]["invertState"].as(); + if (src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_INPUT)][FPSTR(S_INVERT_STATE)].is()) { + bool value = src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_INPUT)][FPSTR(S_INVERT_STATE)].as(); if (value != dst.cascadeControl.input.invertState) { dst.cascadeControl.input.invertState = value; @@ -1439,8 +1238,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["cascadeControl"]["input"]["thresholdTime"].isNull()) { - unsigned short value = src["cascadeControl"]["input"]["thresholdTime"].as(); + if (!src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_INPUT)][FPSTR(S_THRESHOLD_TIME)].isNull()) { + unsigned short value = src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_INPUT)][FPSTR(S_THRESHOLD_TIME)].as(); if (value >= 5 && value <= 600) { if (value != dst.cascadeControl.input.thresholdTime) { @@ -1450,24 +1249,24 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["cascadeControl"]["output"]["enable"].is()) { - bool value = src["cascadeControl"]["output"]["enable"].as(); + if (src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_ENABLED)].is()) { + bool value = src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_ENABLED)].as(); - if (value != dst.cascadeControl.output.enable) { - dst.cascadeControl.output.enable = value; + if (value != dst.cascadeControl.output.enabled) { + dst.cascadeControl.output.enabled = value; changed = true; } } - if (!src["cascadeControl"]["output"]["gpio"].isNull()) { - if (src["cascadeControl"]["output"]["gpio"].is() && src["cascadeControl"]["output"]["gpio"].as().size() == 0) { + if (!src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_GPIO)].isNull()) { + if (src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_GPIO)].is() && src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_GPIO)].as().size() == 0) { if (dst.cascadeControl.output.gpio != GPIO_IS_NOT_CONFIGURED) { dst.cascadeControl.output.gpio = GPIO_IS_NOT_CONFIGURED; changed = true; } - + } else { - unsigned char value = src["cascadeControl"]["output"]["gpio"].as(); + unsigned char value = src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_GPIO)].as(); if (GPIO_IS_VALID(value) && value != dst.cascadeControl.output.gpio) { dst.cascadeControl.output.gpio = value; @@ -1476,8 +1275,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["cascadeControl"]["output"]["invertState"].is()) { - bool value = src["cascadeControl"]["output"]["invertState"].as(); + if (src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_INVERT_STATE)].is()) { + bool value = src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_INVERT_STATE)].as(); if (value != dst.cascadeControl.output.invertState) { dst.cascadeControl.output.invertState = value; @@ -1485,8 +1284,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (!src["cascadeControl"]["output"]["thresholdTime"].isNull()) { - unsigned short value = src["cascadeControl"]["output"]["thresholdTime"].as(); + if (!src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_THRESHOLD_TIME)].isNull()) { + unsigned short value = src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_THRESHOLD_TIME)].as(); if (value >= 5 && value <= 600) { if (value != dst.cascadeControl.output.thresholdTime) { @@ -1496,8 +1295,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["cascadeControl"]["output"]["onFault"].is()) { - bool value = src["cascadeControl"]["output"]["onFault"].as(); + if (src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_ON_FAULT)].is()) { + bool value = src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_ON_FAULT)].as(); if (value != dst.cascadeControl.output.onFault) { dst.cascadeControl.output.onFault = value; @@ -1505,8 +1304,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["cascadeControl"]["output"]["onLossConnection"].is()) { - bool value = src["cascadeControl"]["output"]["onLossConnection"].as(); + if (src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_ON_LOSS_CONNECTION)].is()) { + bool value = src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_ON_LOSS_CONNECTION)].as(); if (value != dst.cascadeControl.output.onLossConnection) { dst.cascadeControl.output.onLossConnection = value; @@ -1514,8 +1313,8 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false } } - if (src["cascadeControl"]["output"]["onEnabledHeating"].is()) { - bool value = src["cascadeControl"]["output"]["onEnabledHeating"].as(); + if (src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_ON_ENABLED_HEATING)].is()) { + bool value = src[FPSTR(S_CASCADE_CONTROL)][FPSTR(S_OUTPUT)][FPSTR(S_ON_ENABLED_HEATING)].as(); if (value != dst.cascadeControl.output.onEnabledHeating) { dst.cascadeControl.output.onEnabledHeating = value; @@ -1526,7 +1325,7 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false // force check emergency target { - float value = !src["emergency"]["target"].isNull() ? src["emergency"]["target"].as() : dst.emergency.target; + float value = !src[FPSTR(S_EMERGENCY)][FPSTR(S_TARGET)].isNull() ? src[FPSTR(S_EMERGENCY)][FPSTR(S_TARGET)].as() : dst.emergency.target; bool noRegulators = !dst.opentherm.nativeHeatingControl; bool valid = isValidTemp( value, @@ -1544,41 +1343,48 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false ); } - if (fabs(dst.emergency.target - value) > 0.0001f) { - dst.emergency.target = roundd(value, 2); + if (fabsf(dst.emergency.target - value) > 0.0001f) { + dst.emergency.target = roundf(value, 2); changed = true; } } // force check heating target { - float value = !src["heating"]["target"].isNull() ? src["heating"]["target"].as() : dst.heating.target; - bool noRegulators = !dst.opentherm.nativeHeatingControl && !dst.equitherm.enable && !dst.pid.enable; + bool indoorTempControl = dst.equitherm.enabled || dst.pid.enabled || dst.opentherm.nativeHeatingControl; + float minTemp = indoorTempControl ? THERMOSTAT_INDOOR_MIN_TEMP : dst.heating.minTemp; + float maxTemp = indoorTempControl ? THERMOSTAT_INDOOR_MAX_TEMP : dst.heating.maxTemp; + + float value = !src[FPSTR(S_HEATING)][FPSTR(S_TARGET)].isNull() + ? src[FPSTR(S_HEATING)][FPSTR(S_TARGET)].as() + : dst.heating.target; bool valid = isValidTemp( value, dst.system.unitSystem, - noRegulators ? dst.heating.minTemp : THERMOSTAT_INDOOR_MIN_TEMP, - noRegulators ? dst.heating.maxTemp : THERMOSTAT_INDOOR_MAX_TEMP, - noRegulators ? dst.system.unitSystem : UnitSystem::METRIC + minTemp, + maxTemp, + dst.system.unitSystem ); if (!valid) { value = convertTemp( - noRegulators ? DEFAULT_HEATING_TARGET_TEMP : THERMOSTAT_INDOOR_DEFAULT_TEMP, + indoorTempControl ? THERMOSTAT_INDOOR_DEFAULT_TEMP : DEFAULT_HEATING_TARGET_TEMP, UnitSystem::METRIC, dst.system.unitSystem ); } - if (fabs(dst.heating.target - value) > 0.0001f) { - dst.heating.target = roundd(value, 2); + if (fabsf(dst.heating.target - value) > 0.0001f) { + dst.heating.target = roundf(value, 2); changed = true; } } // force check dhw target { - float value = !src["dhw"]["target"].isNull() ? src["dhw"]["target"].as() : dst.dhw.target; + float value = !src[FPSTR(S_DHW)][FPSTR(S_TARGET)].isNull() + ? src[FPSTR(S_DHW)][FPSTR(S_TARGET)].as() + : dst.dhw.target; bool valid = isValidTemp( value, dst.system.unitSystem, @@ -1591,7 +1397,7 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false value = convertTemp(DEFAULT_DHW_TARGET_TEMP, UnitSystem::METRIC, dst.system.unitSystem); } - if (fabs(dst.dhw.target - value) > 0.0001f) { + if (fabsf(dst.dhw.target - value) > 0.0001f) { dst.dhw.target = value; changed = true; } @@ -1604,94 +1410,392 @@ inline bool safeJsonToSettings(const JsonVariantConst src, Settings& dst) { return jsonToSettings(src, dst, true); } -void varsToJson(const Variables& src, JsonVariant dst) { - dst["states"]["otStatus"] = src.states.otStatus; - dst["states"]["emergency"] = src.states.emergency; - dst["states"]["heating"] = src.states.heating; - dst["states"]["dhw"] = src.states.dhw; - dst["states"]["flame"] = src.states.flame; - dst["states"]["fault"] = src.states.fault; - dst["states"]["diagnostic"] = src.states.diagnostic; - dst["states"]["externalPump"] = src.states.externalPump; - dst["states"]["mqtt"] = src.states.mqtt; - - dst["sensors"]["modulation"] = roundd(src.sensors.modulation, 2); - dst["sensors"]["pressure"] = roundd(src.sensors.pressure, 2); - dst["sensors"]["dhwFlowRate"] = roundd(src.sensors.dhwFlowRate, 2); - dst["sensors"]["power"] = roundd(src.sensors.power, 2); - dst["sensors"]["faultCode"] = src.sensors.faultCode; - dst["sensors"]["diagnosticCode"] = src.sensors.diagnosticCode; - dst["sensors"]["rssi"] = src.sensors.rssi; - dst["sensors"]["uptime"] = millis() / 1000ul; - dst["sensors"]["outdoor"]["connected"] = src.sensors.outdoor.connected; - dst["sensors"]["outdoor"]["rssi"] = src.sensors.outdoor.rssi; - dst["sensors"]["outdoor"]["battery"] = roundd(src.sensors.outdoor.battery, 2); - dst["sensors"]["outdoor"]["humidity"] = roundd(src.sensors.outdoor.humidity, 2); - dst["sensors"]["indoor"]["connected"] = src.sensors.indoor.connected; - dst["sensors"]["indoor"]["rssi"] = src.sensors.indoor.rssi; - dst["sensors"]["indoor"]["battery"] = roundd(src.sensors.indoor.battery, 2); - dst["sensors"]["indoor"]["humidity"] = roundd(src.sensors.indoor.humidity, 2); - - dst["temperatures"]["indoor"] = roundd(src.temperatures.indoor, 2); - dst["temperatures"]["outdoor"] = roundd(src.temperatures.outdoor, 2); - dst["temperatures"]["heating"] = roundd(src.temperatures.heating, 2); - dst["temperatures"]["heatingReturn"] = roundd(src.temperatures.heatingReturn, 2); - dst["temperatures"]["dhw"] = roundd(src.temperatures.dhw, 2); - dst["temperatures"]["exhaust"] = roundd(src.temperatures.exhaust, 2); - - dst["cascadeControl"]["input"] = src.cascadeControl.input; - dst["cascadeControl"]["output"] = src.cascadeControl.output; - - dst["parameters"]["heatingEnabled"] = src.parameters.heatingEnabled; - dst["parameters"]["heatingMinTemp"] = src.parameters.heatingMinTemp; - dst["parameters"]["heatingMaxTemp"] = src.parameters.heatingMaxTemp; - dst["parameters"]["heatingSetpoint"] = roundd(src.parameters.heatingSetpoint, 2); - dst["parameters"]["dhwMinTemp"] = src.parameters.dhwMinTemp; - dst["parameters"]["dhwMaxTemp"] = src.parameters.dhwMaxTemp; - - dst["parameters"]["slaveMemberId"] = src.parameters.slaveMemberId; - dst["parameters"]["slaveFlags"] = src.parameters.slaveFlags; - dst["parameters"]["slaveType"] = src.parameters.slaveType; - dst["parameters"]["slaveVersion"] = src.parameters.slaveVersion; - dst["parameters"]["slaveOtVersion"] = src.parameters.slaveOtVersion; +void sensorSettingsToJson(const uint8_t sensorId, const Sensors::Settings& src, JsonVariant dst) { + dst[FPSTR(S_ID)] = sensorId; + dst[FPSTR(S_ENABLED)] = src.enabled; + dst[FPSTR(S_NAME)] = src.name; + dst[FPSTR(S_PURPOSE)] = static_cast(src.purpose); + dst[FPSTR(S_TYPE)] = static_cast(src.type); + dst[FPSTR(S_GPIO)] = src.gpio; + + if (src.type == Sensors::Type::DALLAS_TEMP) { + char addr[24]; + sprintf_P( + addr, + PSTR("%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx"), + src.address[0], src.address[1], src.address[2], src.address[3], + src.address[4], src.address[5], src.address[6], src.address[7] + ); + dst[FPSTR(S_ADDRESS)] = String(addr); + + } else if (src.type == Sensors::Type::BLUETOOTH) { + char addr[18]; + sprintf_P( + addr, + PSTR("%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx"), + src.address[0], src.address[1], src.address[2], + src.address[3], src.address[4], src.address[5] + ); + dst[FPSTR(S_ADDRESS)] = String(addr); + + } else { + dst[FPSTR(S_ADDRESS)] = ""; + } + + dst[FPSTR(S_OFFSET)] = roundf(src.offset, 3); + dst[FPSTR(S_FACTOR)] = roundf(src.factor, 3); + dst[FPSTR(S_FILTERING)] = src.filtering; + dst[FPSTR(S_FILTERING_FACTOR)] = roundf(src.filteringFactor, 3); } -bool jsonToVars(const JsonVariantConst src, Variables& dst) { +bool jsonToSensorSettings(const uint8_t sensorId, const JsonVariantConst src, Sensors::Settings& dst) { + if (sensorId > Sensors::getMaxSensorId()) { + return false; + } + bool changed = false; - // temperatures - if (!src["temperatures"]["indoor"].isNull()) { - float value = src["temperatures"]["indoor"].as(); + // enabled + if (src[FPSTR(S_ENABLED)].is()) { + auto value = src[FPSTR(S_ENABLED)].as(); + + if (value != dst.enabled) { + dst.enabled = value; + changed = true; + } + } + + // name + if (!src[FPSTR(S_NAME)].isNull()) { + auto value = src[FPSTR(S_NAME)].as(); + Sensors::cleanName(value); + + if (value.length() < sizeof(dst.name) && !value.equals(dst.name)) { + strcpy(dst.name, value.c_str()); + changed = true; + } + } + + // purpose + if (!src[FPSTR(S_PURPOSE)].isNull()) { + uint8_t value = src[FPSTR(S_PURPOSE)].as(); + + switch (value) { + case static_cast(Sensors::Purpose::OUTDOOR_TEMP): + case static_cast(Sensors::Purpose::INDOOR_TEMP): + case static_cast(Sensors::Purpose::HEATING_TEMP): + case static_cast(Sensors::Purpose::HEATING_RETURN_TEMP): + case static_cast(Sensors::Purpose::DHW_TEMP): + case static_cast(Sensors::Purpose::DHW_RETURN_TEMP): + case static_cast(Sensors::Purpose::DHW_FLOW_RATE): + case static_cast(Sensors::Purpose::EXHAUST_TEMP): + case static_cast(Sensors::Purpose::MODULATION_LEVEL): + + case static_cast(Sensors::Purpose::POWER_FACTOR): + case static_cast(Sensors::Purpose::POWER): + case static_cast(Sensors::Purpose::FAN_SPEED): + case static_cast(Sensors::Purpose::CO2): + case static_cast(Sensors::Purpose::PRESSURE): + case static_cast(Sensors::Purpose::HUMIDITY): + case static_cast(Sensors::Purpose::TEMPERATURE): + case static_cast(Sensors::Purpose::NOT_CONFIGURED): + if (static_cast(dst.purpose) != value) { + dst.purpose = static_cast(value); + changed = true; + } + break; + + default: + break; + } + } + + // type + if (!src[FPSTR(S_TYPE)].isNull()) { + uint8_t value = src[FPSTR(S_TYPE)].as(); + + switch (value) { + case static_cast(Sensors::Type::OT_OUTDOOR_TEMP): + case static_cast(Sensors::Type::OT_HEATING_TEMP): + case static_cast(Sensors::Type::OT_HEATING_RETURN_TEMP): + case static_cast(Sensors::Type::OT_DHW_TEMP): + case static_cast(Sensors::Type::OT_DHW_TEMP2): + case static_cast(Sensors::Type::OT_DHW_FLOW_RATE): + case static_cast(Sensors::Type::OT_CH2_TEMP): + case static_cast(Sensors::Type::OT_EXHAUST_TEMP): + case static_cast(Sensors::Type::OT_HEAT_EXCHANGER_TEMP): + case static_cast(Sensors::Type::OT_PRESSURE): + case static_cast(Sensors::Type::OT_MODULATION_LEVEL): + case static_cast(Sensors::Type::OT_CURRENT_POWER): + case static_cast(Sensors::Type::OT_EXHAUST_CO2): + case static_cast(Sensors::Type::OT_EXHAUST_FAN_SPEED): + case static_cast(Sensors::Type::OT_SUPPLY_FAN_SPEED): + case static_cast(Sensors::Type::OT_SOLAR_STORAGE_TEMP): + case static_cast(Sensors::Type::OT_SOLAR_COLLECTOR_TEMP): + case static_cast(Sensors::Type::OT_FAN_SPEED_SETPOINT): + case static_cast(Sensors::Type::OT_FAN_SPEED_CURRENT): + + case static_cast(Sensors::Type::NTC_10K_TEMP): + case static_cast(Sensors::Type::DALLAS_TEMP): + case static_cast(Sensors::Type::BLUETOOTH): + case static_cast(Sensors::Type::HEATING_SETPOINT_TEMP): + case static_cast(Sensors::Type::MANUAL): + case static_cast(Sensors::Type::NOT_CONFIGURED): + if (static_cast(dst.type) != value) { + dst.type = static_cast(value); + changed = true; + } + break; + + default: + break; + } + } + + // gpio + if (!src[FPSTR(S_GPIO)].isNull()) { + if (dst.type != Sensors::Type::DALLAS_TEMP && dst.type == Sensors::Type::BLUETOOTH && dst.type == Sensors::Type::NTC_10K_TEMP) { + if (dst.gpio != GPIO_IS_NOT_CONFIGURED) { + dst.gpio = GPIO_IS_NOT_CONFIGURED; + changed = true; + } + + } else if (src[FPSTR(S_GPIO)].is() && src[FPSTR(S_GPIO)].as().size() == 0) { + if (dst.gpio != GPIO_IS_NOT_CONFIGURED) { + dst.gpio = GPIO_IS_NOT_CONFIGURED; + changed = true; + } + + } else { + unsigned char value = src[FPSTR(S_GPIO)].as(); - if (settings.sensors.indoor.type == SensorType::MANUAL && isValidTemp(value, settings.system.unitSystem, -99.9f, 99.9f)) { - if (fabs(value - dst.temperatures.indoor) > 0.0001f) { - dst.temperatures.indoor = roundd(value, 2); + if (GPIO_IS_VALID(value) && value != dst.gpio) { + dst.gpio = value; changed = true; } } } - if (!src["temperatures"]["outdoor"].isNull()) { - float value = src["temperatures"]["outdoor"].as(); + // address + if (!src[FPSTR(S_ADDRESS)].isNull()) { + String value = src[FPSTR(S_ADDRESS)].as(); + + if (dst.type == Sensors::Type::DALLAS_TEMP) { + uint8_t tmp[8]; + int parsed = sscanf( + value.c_str(), + "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx", + &tmp[0], &tmp[1], &tmp[2], &tmp[3], + &tmp[4], &tmp[5], &tmp[6], &tmp[7] + ); - if (settings.sensors.outdoor.type == SensorType::MANUAL && isValidTemp(value, settings.system.unitSystem, -99.9f, 99.9f)) { - if (fabs(value - dst.temperatures.outdoor) > 0.0001f) { - dst.temperatures.outdoor = roundd(value, 2); - changed = true; + if (parsed == 8) { + for (uint8_t i = 0; i < 8; i++) { + if (dst.address[i] != tmp[i]) { + dst.address[i] = tmp[i]; + changed = true; + } + } + } + + } else if (dst.type == Sensors::Type::BLUETOOTH) { + uint8_t tmp[6]; + int parsed = sscanf( + value.c_str(), + "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx", + &tmp[0], &tmp[1], &tmp[2], + &tmp[3], &tmp[4], &tmp[5] + ); + + if (parsed == 6) { + for (uint8_t i = 0; i < 6; i++) { + if (dst.address[i] != tmp[i]) { + dst.address[i] = tmp[i]; + changed = true; + } + } } } } + // offset + if (!src[FPSTR(S_OFFSET)].isNull()) { + float value = src[FPSTR(S_OFFSET)].as(); + + if (value >= -20.0f && value <= 20.0f && fabsf(value - dst.offset) > 0.0001f) { + dst.offset = roundf(value, 2); + changed = true; + } + } + + // factor + if (!src[FPSTR(S_FACTOR)].isNull()) { + float value = src[FPSTR(S_FACTOR)].as(); + + if (value > 0.09f && value <= 10.0f && fabsf(value - dst.factor) > 0.0001f) { + dst.factor = roundf(value, 3); + changed = true; + } + } + + // filtering + if (src[FPSTR(S_FILTERING)].is()) { + auto value = src[FPSTR(S_FILTERING)].as(); + + if (value != dst.filtering) { + dst.filtering = value; + changed = true; + } + } + + // filtering factor + if (!src[FPSTR(S_FILTERING_FACTOR)].isNull()) { + float value = src[FPSTR(S_FILTERING_FACTOR)].as(); + + if (value > 0 && value <= 1 && fabsf(value - dst.filteringFactor) > 0.0001f) { + dst.filteringFactor = roundf(value, 3); + changed = true; + } + } + + return changed; +} + +void sensorResultToJson(const uint8_t sensorId, JsonVariant dst) { + if (!Sensors::isValidSensorId(sensorId)) { + return; + } + + auto& sSensor = Sensors::settings[sensorId]; + auto& rSensor = Sensors::results[sensorId]; + + //dst[FPSTR(S_ID)] = sensorId; + dst[FPSTR(S_CONNECTED)] = rSensor.connected; + dst[FPSTR(S_SIGNAL_QUALITY)] = rSensor.signalQuality; + + if (sSensor.type == Sensors::Type::BLUETOOTH) { + dst[FPSTR(S_TEMPERATURE)] = roundf(rSensor.values[static_cast(Sensors::ValueType::TEMPERATURE)], 3); + dst[FPSTR(S_HUMIDITY)] = roundf(rSensor.values[static_cast(Sensors::ValueType::HUMIDITY)], 3); + dst[FPSTR(S_BATTERY)] = roundf(rSensor.values[static_cast(Sensors::ValueType::BATTERY)], 1); + dst[FPSTR(S_RSSI)] = roundf(rSensor.values[static_cast(Sensors::ValueType::RSSI)], 0); + + } else { + dst[FPSTR(S_VALUE)] = roundf(rSensor.values[static_cast(Sensors::ValueType::PRIMARY)], 3); + } +} + +bool jsonToSensorResult(const uint8_t sensorId, const JsonVariantConst src) { + if (!Sensors::isValidSensorId(sensorId)) { + return false; + } + + auto& sSensor = Sensors::settings[sensorId]; + if (!sSensor.enabled || sSensor.type != Sensors::Type::MANUAL) { + return false; + } + + auto& dst = Sensors::results[sensorId]; + bool changed = false; + + // value + if (!src[FPSTR(S_VALUE)].isNull()) { + float value = src[FPSTR(S_VALUE)].as(); + + uint8_t vType = static_cast(Sensors::ValueType::PRIMARY); + if (fabsf(value - dst.values[vType]) > 0.0001f) { + dst.values[vType] = roundf(value, 2); + changed = true; + } + } + + return changed; +} + +void varsToJson(const Variables& src, JsonVariant dst) { + auto slave = dst[FPSTR(S_SLAVE)].to(); + slave[FPSTR(S_MEMBER_ID)] = src.slave.memberId; + slave[FPSTR(S_FLAGS)] = src.slave.flags; + slave[FPSTR(S_TYPE)] = src.slave.type; + slave[FPSTR(S_APP_VERSION)] = src.slave.appVersion; + slave[FPSTR(S_PROTOCOL_VERSION)] = src.slave.appVersion; + slave[FPSTR(S_CONNECTED)] = src.slave.connected; + slave[FPSTR(S_FLAME)] = src.slave.flame; + + auto sModulation = slave[FPSTR(S_MODULATION)].to(); + sModulation[FPSTR(S_MIN)] = src.slave.modulation.min; + sModulation[FPSTR(S_MAX)] = src.slave.modulation.max; + + auto sPower = slave[FPSTR(S_POWER)].to(); + sPower[FPSTR(S_MIN)] = roundf(src.slave.power.min, 2); + sPower[FPSTR(S_MAX)] = roundf(src.slave.power.max, 2); + + auto sHeating = slave[FPSTR(S_HEATING)].to(); + sHeating[FPSTR(S_ACTIVE)] = src.slave.heating.active; + sHeating[FPSTR(S_MIN_TEMP)] = src.slave.heating.minTemp; + sHeating[FPSTR(S_MAX_TEMP)] = src.slave.heating.maxTemp; + + auto sDhw = slave[FPSTR(S_DHW)].to(); + sDhw[FPSTR(S_ACTIVE)] = src.slave.dhw.active; + sDhw[FPSTR(S_MIN_TEMP)] = src.slave.dhw.minTemp; + sDhw[FPSTR(S_MAX_TEMP)] = src.slave.dhw.maxTemp; + + auto sFault = slave[FPSTR(S_FAULT)].to(); + sFault[FPSTR(S_ACTIVE)] = src.slave.fault.active; + sFault[FPSTR(S_CODE)] = src.slave.fault.code; + + auto sDiag = slave[FPSTR(S_DIAG)].to(); + sDiag[FPSTR(S_ACTIVE)] = src.slave.diag.active; + sDiag[FPSTR(S_CODE)] = src.slave.diag.code; + + + auto master = dst[FPSTR(S_MASTER)].to(); + auto mHeating = master[FPSTR(S_HEATING)].to(); + mHeating[FPSTR(S_ENABLED)] = src.master.heating.enabled; + mHeating[FPSTR(S_BLOCKING)] = src.master.heating.blocking; + mHeating[FPSTR(S_INDOOR_TEMP_CONTROL)] = src.master.heating.indoorTempControl; + mHeating[FPSTR(S_SETPOINT_TEMP)] = roundf(src.master.heating.setpointTemp, 2); + mHeating[FPSTR(S_TARGET_TEMP)] = roundf(src.master.heating.targetTemp, 2); + mHeating[FPSTR(S_CURRENT_TEMP)] = roundf(src.master.heating.currentTemp, 2); + mHeating[FPSTR(S_RETURN_TEMP)] = roundf(src.master.heating.returnTemp, 2); + mHeating[FPSTR(S_INDOOR_TEMP)] = roundf(src.master.heating.indoorTemp, 2); + mHeating[FPSTR(S_OUTDOOR_TEMP)] = roundf(src.master.heating.outdoorTemp, 2); + mHeating[FPSTR(S_MIN_TEMP)] = roundf(src.master.heating.minTemp, 2); + mHeating[FPSTR(S_MAX_TEMP)] = roundf(src.master.heating.maxTemp, 2); + + auto mDhw = master[FPSTR(S_DHW)].to(); + mDhw[FPSTR(S_ENABLED)] = src.master.dhw.enabled; + mDhw[FPSTR(S_TARGET_TEMP)] = roundf(src.master.dhw.targetTemp, 2); + mDhw[FPSTR(S_CURRENT_TEMP)] = roundf(src.master.dhw.currentTemp, 2); + mDhw[FPSTR(S_RETURN_TEMP)] = roundf(src.master.dhw.returnTemp, 2); + mDhw[FPSTR(S_MIN_TEMP)] = settings.dhw.minTemp; + mDhw[FPSTR(S_MAX_TEMP)] = settings.dhw.maxTemp; + + master[FPSTR(S_NETWORK)][FPSTR(S_CONNECTED)] = src.network.connected; + master[FPSTR(S_NETWORK)][FPSTR(S_RSSI)] = src.network.rssi; + master[FPSTR(S_MQTT)][FPSTR(S_CONNECTED)] = src.mqtt.connected; + master[FPSTR(S_EMERGENCY)][FPSTR(S_STATE)] = src.emergency.state; + master[FPSTR(S_EXTERNAL_PUMP)][FPSTR(S_STATE)] = src.externalPump.state; + + auto mCascadeControl = master[FPSTR(S_CASCADE_CONTROL)].to(); + mCascadeControl[FPSTR(S_INPUT)] = src.cascadeControl.input; + mCascadeControl[FPSTR(S_OUTPUT)] = src.cascadeControl.output; + + master[FPSTR(S_UPTIME)] = millis() / 1000; +} + +bool jsonToVars(const JsonVariantConst src, Variables& dst) { + bool changed = false; + // actions - if (src["actions"]["restart"].is() && src["actions"]["restart"].as()) { + if (src[FPSTR(S_ACTIONS)][FPSTR(S_RESTART)].is() && src[FPSTR(S_ACTIONS)][FPSTR(S_RESTART)].as()) { dst.actions.restart = true; } - if (src["actions"]["resetFault"].is() && src["actions"]["resetFault"].as()) { + if (src[FPSTR(S_ACTIONS)][FPSTR(S_RESET_FAULT)].is() && src[FPSTR(S_ACTIONS)][FPSTR(S_RESET_FAULT)].as()) { dst.actions.resetFault = true; } - if (src["actions"]["resetDiagnostic"].is() && src["actions"]["resetDiagnostic"].as()) { + if (src[FPSTR(S_ACTIONS)][FPSTR(S_RESET_DIAGNOSTIC)].is() && src[FPSTR(S_ACTIONS)][FPSTR(S_RESET_DIAGNOSTIC)].as()) { dst.actions.resetDiagnostic = true; } diff --git a/src_data/fonts/iconly.eot b/src_data/fonts/iconly.eot index 955eb43..e5bf31f 100644 Binary files a/src_data/fonts/iconly.eot and b/src_data/fonts/iconly.eot differ diff --git a/src_data/fonts/iconly.svg b/src_data/fonts/iconly.svg index 8eed6c2..5768718 100644 --- a/src_data/fonts/iconly.svg +++ b/src_data/fonts/iconly.svg @@ -49,6 +49,12 @@ + + @@ -67,12 +73,24 @@ + + + + diff --git a/src_data/fonts/iconly.ttf b/src_data/fonts/iconly.ttf index 967cc78..736915b 100644 Binary files a/src_data/fonts/iconly.ttf and b/src_data/fonts/iconly.ttf differ diff --git a/src_data/fonts/iconly.woff b/src_data/fonts/iconly.woff index 412a1ce..37e595d 100644 Binary files a/src_data/fonts/iconly.woff and b/src_data/fonts/iconly.woff differ diff --git a/src_data/fonts/iconly.woff2 b/src_data/fonts/iconly.woff2 index 724540a..45ae36e 100644 Binary files a/src_data/fonts/iconly.woff2 and b/src_data/fonts/iconly.woff2 differ diff --git a/src_data/locales/en.json b/src_data/locales/en.json index f7b48d6..876b156 100644 --- a/src_data/locales/en.json +++ b/src_data/locales/en.json @@ -74,8 +74,9 @@ "section": { "control": "Control", - "states": "States and sensors", - "otDiag": "OpenTherm diagnostic" + "states": "States", + "sensors": "Sensors", + "diag": "OpenTherm diagnostic" }, "thermostat": { @@ -86,39 +87,45 @@ "turbo": "Turbo mode" }, - "state": { - "ot": "OpenTherm connected", - "mqtt": "MQTT connected", - "emergency": "Emergency", - "heating": "Heating", - "dhw": "DHW", - "flame": "Flame", - "fault": "Fault", - "diag": "Diagnostic", - "extpump": "External pump", - "outdoorSensorConnected": "Outdoor sensor connected", - "outdoorSensorRssi": "Outdoor sensor RSSI", - "outdoorSensorHumidity": "Outdoor sensor humidity", - "outdoorSensorBattery": "Outdoor sensor battery", - "indoorSensorConnected": "Indoor sensor connected", - "cascadeControlInput": "Cascade control (input)", - "cascadeControlOutput": "Cascade control (output)", - "indoorSensorRssi": "Indoor sensor RSSI", - "indoorSensorHumidity": "Indoor sensor humidity", - "indoorSensorBattery": "Indoor sensor battery", - "modulation": "Modulation", - "pressure": "Pressure", - "dhwFlowRate": "DHW flow rate", - "power": "Current power", - "faultCode": "Fault code", - "diagCode": "Diagnostic code", - "indoorTemp": "Indoor temp", - "outdoorTemp": "Outdoor temp", - "heatingTemp": "Heating temp", - "heatingSetpointTemp": "Heating setpoint temp", - "heatingReturnTemp": "Heating return temp", - "dhwTemp": "DHW temp", - "exhaustTemp": "Exhaust temp" + "states": { + "mNetworkConnected": "Network connection", + "mMqttConnected": "MQTT connection", + "mEmergencyState": "Emergency mode", + "mExtPumpState": "External pump", + "mCascadeControlInput": "Cascade control (input)", + "mCascadeControlOutput": "Cascade control (output)", + + "sConnected": "OpenTherm connection", + "sFlame": "Flame", + "sFaultActive": "Fault", + "sFaultCode": "Faul code", + "sDiagActive": "Diagnostic", + "sDiagCode": "Diagnostic code", + + "mHeatEnabled": "Heating enabled", + "mHeatBlocking": "Heating blocked", + "sHeatActive": "Heating active", + "mHeatSetpointTemp": "Heating setpoint temp", + "mHeatTargetTemp": "Heating target temp", + "mHeatCurrTemp": "Heating current temp", + "mHeatRetTemp": "Heating return temp", + "mHeatIndoorTemp": "Heating, indoor temp", + "mHeatOutdoorTemp": "Heating, outdoor temp", + + "mDhwEnabled": "DHW enabled", + "sDhwActive": "DHW active", + "mDhwTargetTemp": "DHW target temp", + "mDhwCurrTemp": "DHW current temp", + "mDhwRetTemp": "DHW return temp" + }, + + "sensors": { + "values": { + "temp": "Temperature", + "humidity": "Humidity", + "battery": "Battery", + "rssi": "RSSI" + } } }, @@ -161,6 +168,87 @@ } }, + "sensors": { + "title": "Sensors settings - OpenTherm Gateway", + "name": "Sensors settings", + + "enabled": "Enabled", + "sensorName": { + "title": "Sensor name", + "note": "May only contain: a-z, A-Z, 0-9, _ and space" + }, + "purpose": "Purpose", + "purposes": { + "outdoorTemp": "Outdoor temperature", + "indoorTemp": "Indoor temperature", + "heatTemp": "Heating, temperature", + "heatRetTemp": "Heating, return temperature", + "dhwTemp": "DHW, temperature", + "dhwRetTemp": "DHW, return temperature", + "dhwFlowRate": "DHW, flow rate", + "exhaustTemp": "Exhaust temperature", + "modLevel": "Modulation level (in percents)", + "powerFactor": "Power (in percent)", + "power": "Power (in kWt)", + "fanSpeed": "Fan speed", + "co2": "CO2", + "pressure": "Pressure", + "humidity": "Humidity", + "temperature": "Temperature", + "notConfigured": "Not configured" + }, + "type": "Type/source", + "types": { + "otOutdoorTemp": "OpenTherm, outdoor temp", + "otHeatTemp": "OpenTherm, heating, temp", + "otHeatRetTemp": "OpenTherm, heating, return temp", + "otDhwTemp": "OpenTherm, DHW, temperature", + "otDhwTemp2": "OpenTherm, DHW, temperature 2", + "otDhwFlowRate": "OpenTherm, DHW, flow rate", + "otCh2Temp": "OpenTherm, channel 2, temp", + "otExhaustTemp": "OpenTherm, exhaust temp", + "otHeatExchangerTemp": "OpenTherm, heat exchanger temp", + "otPressure": "OpenTherm, pressure", + "otModLevel": "OpenTherm, modulation level", + "otCurrentPower": "OpenTherm, current power", + "otExhaustCo2": "OpenTherm, exhaust CO2", + "otExhaustFanSpeed": "OpenTherm, exhaust fan speed", + "otSupplyFanSpeed": "OpenTherm, supply fan speed", + "otSolarStorageTemp": "OpenTherm, solar storage temp", + "otSolarCollectorTemp": "OpenTherm, solar collector temp", + "otFanSpeedSetpoint": "OpenTherm, setpoint fan speed", + "otFanSpeedCurrent": "OpenTherm, current fan speed", + + "ntcTemp": "NTC sensor", + "dallasTemp": "DALLAS sensor", + "bluetooth": "BLE sensor", + "heatSetpointTemp": "Heating, setpoint temp", + "manual": "Manual via MQTT/API", + "notConfigured": "Not configured" + }, + "gpio": "GPIO", + "address": { + "title": "Sensor address", + "note": "For auto detection of DALLAS sensors leave it at default, for BLE devices need a MAC address" + }, + "correction": { + "desc": "Correction of values", + "offset": "Compensation (offset)", + "factor": "Multiplier" + }, + "filtering": { + "desc": "Filtering values", + "enabled": { + "title": "Enabled filtering", + "note": "It can be useful if there is a lot of sharp noise on the charts. The filter used is \"Running Average\"." + }, + "factor": { + "title": "Filtration factor", + "note": "The lower the value, the smoother and longer the change in numeric values." + } + } + }, + "settings": { "title": "Settings - OpenTherm Gateway", "name": "Settings", @@ -176,8 +264,6 @@ "pid": "PID settings", "ot": "OpenTherm settings", "mqtt": "MQTT settings", - "outdorSensor": "Outdoor sensor settings", - "indoorSensor": "Indoor sensor settings", "extPump": "External pump settings", "cascadeControl": "Cascade control settings" }, @@ -207,11 +293,11 @@ "statusLedGpio": "Status LED GPIO", "logLevel": "Log level", "serial": { - "enable": "Enable Serial port", + "enable": "Enabled Serial port", "baud": "Serial port baud rate" }, "telnet": { - "enable": "Enable Telnet", + "enable": "Enabled Telnet", "port": { "title": "Telnet port", "note": "Default: 23" @@ -256,16 +342,9 @@ "inGpio": "In GPIO", "outGpio": "Out GPIO", "ledGpio": "RX LED GPIO", - "memberIdCode": "Master MemberID code", + "memberId": "Master member ID", + "flags": "Master flags", "maxMod": "Max modulation level", - "pressureFactor": { - "title": "Coeff. pressure correction", - "note": "If the pressure displayed is X10 from the real one, set the 0.1." - }, - "dhwFlowRateFactor": { - "title": "Coeff. DHW flow rate correction", - "note": "If the DHW flow rate displayed is X10 from the real one, set the 0.1." - }, "minPower": { "title": "Min boiler power (kW)", "note": "This value is at 0-1% boiler modulation level. Typically found in the boiler specification as \"minimum useful heat output\"." @@ -274,17 +353,6 @@ "title": "Max boiler power (kW)", "note": "0 - try detect automatically. Typically found in the boiler specification as \"maximum useful heat output\"." }, - "fnv": { - "desc": "Filtering numeric values", - "enable": { - "title": "Enable filtering", - "note": "It can be useful if there is a lot of sharp noise on the charts. The filter used is \"Running Average\"." - }, - "factor": { - "title": "Filtration coeff.", - "note": "The lower the value, the smoother and longer the change in numeric values." - } - }, "options": { "desc": "Options", @@ -315,20 +383,6 @@ "interval": "Publish interval (sec)" }, - "tempSensor": { - "source": { - "type": "Source type", - "boilerOutdoor": "From boiler via OpenTherm", - "boilerReturn": "Return heat carrier temp via OpenTherm", - "manual": "Manual via MQTT/API", - "ext": "External (DS18B20)", - "ble": "BLE device" - }, - "gpio": "GPIO", - "offset": "Temp offset (calibration)", - "bleAddress": "BLE device MAC address" - }, - "extPump": { "use": "Use external pump", "gpio": "Relay GPIO", @@ -340,14 +394,14 @@ "cascadeControl": { "input": { "desc": "Can be used to turn on the heating only if another boiler is faulty. The other boiler controller must change the state of the GPIO input in the event of a fault.", - "enable": "Enable input", + "enable": "Enabled input", "gpio": "GPIO", "invertState": "Invert GPIO state", "thresholdTime": "State change threshold time (sec)" }, "output": { "desc": "Can be used to switch on another boiler via relay.", - "enable": "Enable output", + "enable": "Enabled output", "gpio": "GPIO", "invertState": "Invert GPIO state", "thresholdTime": "State change threshold time (sec)", @@ -374,7 +428,7 @@ "note": { "disclaimer1": "After a successful upgrade the filesystem, ALL settings will be reset to default values! Save backup before upgrading.", - "disclaimer2": "After a successful upgrade, the device will automatically reboot after 10 seconds." + "disclaimer2": "After a successful upgrade, the device will automatically reboot after 15 seconds." }, "settingsFile": "Settings file", diff --git a/src_data/locales/ru.json b/src_data/locales/ru.json index 174f8b6..64c1ec8 100644 --- a/src_data/locales/ru.json +++ b/src_data/locales/ru.json @@ -74,8 +74,9 @@ "section": { "control": "Управление", - "states": "Состояние и сенсоры", - "otDiag": "Диагностика OpenTherm" + "states": "Состояние", + "sensors": "Сенсоры", + "diag": "Диагностика OpenTherm" }, "thermostat": { @@ -86,39 +87,45 @@ "turbo": "Турбо" }, - "state": { - "ot": "OpenTherm подключение", - "mqtt": "MQTT подключение", - "emergency": "Аварийный режим", - "heating": "Отопление", - "dhw": "ГВС", - "flame": "Пламя", - "fault": "Ошибка", - "diag": "Диагностика", - "extpump": "Внешний насос", - "outdoorSensorConnected": "Датчик наруж. темп.", - "outdoorSensorRssi": "RSSI датчика наруж. темп.", - "outdoorSensorHumidity": "Влажность с наруж. датчика темп.", - "outdoorSensorBattery": "Заряд наруж. датчика темп.", - "indoorSensorConnected": "Датчик внутр. темп.", - "cascadeControlInput": "Каскадное управление (вход)", - "cascadeControlOutput": "Каскадное управление (выход)", - "indoorSensorRssi": "RSSI датчика внутр. темп.", - "indoorSensorHumidity": "Влажность с внутр. датчика темп.", - "indoorSensorBattery": "Заряд внутр. датчика темп.", - "modulation": "Уровень модуляции", - "pressure": "Давление", - "dhwFlowRate": "Расход ГВС", - "power": "Текущая мощность", - "faultCode": "Код ошибки", - "diagCode": "Диагностический код", - "indoorTemp": "Внутренняя темп.", - "outdoorTemp": "Наружная темп.", - "heatingTemp": "Темп. отопления", - "heatingSetpointTemp": "Уставка темп. отопления", - "heatingReturnTemp": "Темп. обратки отопления", - "dhwTemp": "Темп. ГВС", - "exhaustTemp": "Темп. выхлопных газов" + "states": { + "mNetworkConnected": "Подключение к сети", + "mMqttConnected": "Подключение к MQTT", + "mEmergencyState": "Аварийный режим", + "mExtPumpState": "Внешний насос", + "mCascadeControlInput": "Каскадное управление (вход)", + "mCascadeControlOutput": "Каскадное управление (выход)", + + "sConnected": "Подключение к OpenTherm", + "sFlame": "Пламя", + "sFaultActive": "Ошибка", + "sFaultCode": "Код ошибки", + "sDiagActive": "Диагностика", + "sDiagCode": "Диагностический код", + + "mHeatEnabled": "Отопление", + "mHeatBlocking": "Блокировка отопления", + "sHeatActive": "Активность отопления", + "mHeatSetpointTemp": "Отопление, уставка", + "mHeatTargetTemp": "Отопление, целевая температура", + "mHeatCurrTemp": "Отопление, текущая температура", + "mHeatRetTemp": "Отопление, температура обратки", + "mHeatIndoorTemp": "Отопление, внутренняя темп.", + "mHeatOutdoorTemp": "Отопление, наружная темп.", + + "mDhwEnabled": "ГВС", + "sDhwActive": "Активность ГВС", + "mDhwTargetTemp": "ГВС, целевая температура", + "mDhwCurrTemp": "ГВС, текущая температура", + "mDhwRetTemp": "ГВС, температура обратки" + }, + + "sensors": { + "values": { + "temp": "Температура", + "humidity": "Влажность", + "battery": "Уровень заряда", + "rssi": "RSSI" + } } }, @@ -161,6 +168,87 @@ } }, + "sensors": { + "title": "Настройки сенсоров - OpenTherm Gateway", + "name": "Настройки сенсоров", + + "enabled": "Включить и использовать", + "sensorName": { + "title": "Имя сенсора", + "note": "Может содержать только: a-z, A-Z, 0-9, _ и пробел" + }, + "purpose": "Назначение", + "purposes": { + "outdoorTemp": "Внешняя температура", + "indoorTemp": "Внутреняя температура", + "heatTemp": "Отопление, температура", + "heatRetTemp": "Отопление, температура обратки", + "dhwTemp": "ГВС, температура", + "dhwRetTemp": "ГВС, температура обратки", + "dhwFlowRate": "ГВС, расход/скорость потока", + "exhaustTemp": "Температура выхлопных газов", + "modLevel": "Уровень модуляции (в процентах)", + "powerFactor": "Мощность (в процентах)", + "power": "Мощность (в кВт)", + "fanSpeed": "Скорость вентилятора", + "co2": "CO2", + "pressure": "Давление", + "humidity": "Влажность", + "temperature": "Температура", + "notConfigured": "Не сконфигурировано" + }, + "type": "Тип/источник", + "types": { + "otOutdoorTemp": "OpenTherm, внешняя температура", + "otHeatTemp": "OpenTherm, отопление, температура", + "otHeatRetTemp": "OpenTherm, отопление, температура обратки", + "otDhwTemp": "OpenTherm, ГВС, температура", + "otDhwTemp2": "OpenTherm, ГВС, температура 2", + "otDhwFlowRate": "OpenTherm, ГВС, расход/скорость потока", + "otCh2Temp": "OpenTherm, канал 2, температура", + "otExhaustTemp": "OpenTherm, температура выхлопных газов", + "otHeatExchangerTemp": "OpenTherm, температура теплообменника", + "otPressure": "OpenTherm, давление", + "otModLevel": "OpenTherm, уровень модуляции", + "otCurrentPower": "OpenTherm, текущая мощность", + "otExhaustCo2": "OpenTherm, CO2 вытяжного воздуха", + "otExhaustFanSpeed": "OpenTherm, скорость вытяжного вентилятора", + "otSupplyFanSpeed": "OpenTherm, скорость приточного вентилятора", + "otSolarStorageTemp": "OpenTherm, темп. бойлера солн. коллектора", + "otSolarCollectorTemp": "OpenTherm, темп. солн. коллектора", + "otFanSpeedSetpoint": "OpenTherm, установленная мощн. вентилятора", + "otFanSpeedCurrent": "OpenTherm, текущая мощн. вентилятора", + + "ntcTemp": "NTC датчик", + "dallasTemp": "DALLAS датчик", + "bluetooth": "BLE датчик", + "heatSetpointTemp": "Отопление, температура уставки", + "manual": "Вручную через MQTT/API", + "notConfigured": "Не сконфигурировано" + }, + "gpio": "GPIO датчика", + "address": { + "title": "Адрес датчика", + "note": "Для DALLAS датчиков оставьте по умолчанию для автоопределения, для BLE устройств необходимо указать MAC адрес" + }, + "correction": { + "desc": "Коррекция показаний", + "offset": "Компенсация (смещение)", + "factor": "Множитель" + }, + "filtering": { + "desc": "Фильтрация показаний", + "enabled": { + "title": "Включить фильтрацию", + "note": "Может быть полезно, если на графиках много резкого шума. В качестве фильтра используется \"бегущее среднее\"." + }, + "factor": { + "title": "Коэфф. фильтрации", + "note": "Чем меньше коэф., тем плавнее и дольше изменение числовых значений." + } + } + }, + "settings": { "title": "Настройки - OpenTherm Gateway", "name": "Настройки", @@ -176,8 +264,6 @@ "pid": "Настройки ПИД", "ot": "Настройки OpenTherm", "mqtt": "Настройки MQTT", - "outdorSensor": "Настройки наружного датчика температуры", - "indoorSensor": "Настройки внутреннего датчика температуры", "extPump": "Настройки дополнительного насоса", "cascadeControl": "Настройки каскадного управления" }, @@ -231,21 +317,7 @@ "title": "Целевая температура", "note": "Важно: Целевая температура в помещении, если включена ОТ опция «Передать управление отоплением котлу».
Во всех остальных случаях целевая температура теплоносителя." }, - "treshold": "Пороговое время включения (сек)", - - "events": { - "desc": "События", - "network": "При отключении сети", - "mqtt": "При отключении MQTT", - "indoorSensorDisconnect": "При потере связи с датчиком внутренней темп.", - "outdoorSensorDisconnect": "При потере связи с датчиком наружной темп." - }, - - "regulators": { - "desc": "Используемые регуляторы", - "equitherm": "ПЗА (требуется внешний (DS18B20) или подключенный к котлу датчик наружной температуры)", - "pid": "ПИД (требуется внешний (DS18B20) датчик внутренней температуры)" - } + "treshold": "Пороговое время включения (сек)" }, "equitherm": { @@ -270,16 +342,9 @@ "inGpio": "Вход GPIO", "outGpio": "Выход GPIO", "ledGpio": "RX LED GPIO", - "memberIdCode": "Master MemberID код", + "memberId": "Master member ID", + "flags": "Master flags", "maxMod": "Макс. уровень модуляции", - "pressureFactor": { - "title": "Коэфф. коррекции давления", - "note": "Если давление отображается Х10 от реального, установите значение 0.1." - }, - "dhwFlowRateFactor": { - "title": "Коэфф. коррекции потока ГВС", - "note": "Если поток ГВС отображается Х10 от реального, установите значение 0.1." - }, "minPower": { "title": "Мин. мощность котла (кВт)", "note": "Это значение соответствует уровню модуляции котла 0–1%. Обычно можно найти в спецификации котла как \"минимальная полезная тепловая мощность\"." @@ -288,17 +353,6 @@ "title": "Макс. мощность котла (кВт)", "note": "0 - попробовать определить автоматически. Обычно можно найти в спецификации котла как \"максимальная полезная тепловая мощность\"." }, - "fnv": { - "desc": "Фильтрация числовых значений", - "enable": { - "title": "Включить фильтрацию", - "note": "Может быть полезно, если на графиках много резкого шума. В качестве фильтра используется \"бегущее среднее\"." - }, - "factor": { - "title": "Коэфф. фильтрации", - "note": "Чем меньше коэф., тем плавнее и дольше изменение числовых значений." - } - }, "options": { "desc": "Опции", @@ -329,20 +383,6 @@ "interval": "Интервал публикации (сек)" }, - "tempSensor": { - "source": { - "type": "Источник данных", - "boilerOutdoor": "От котла через OpenTherm", - "boilerReturn": "Температура обратки через OpenTherm", - "manual": "Вручную через MQTT/API", - "ext": "Внешний датчик (DS18B20)", - "ble": "BLE устройство" - }, - "gpio": "GPIO", - "offset": "Смещение температуры (калибровка)", - "bleAddress": "MAC адрес BLE устройства" - }, - "extPump": { "use": "Использовать доп. насос", "gpio": "GPIO реле", @@ -366,7 +406,7 @@ "invertState": "Инвертировать состояние GPIO", "thresholdTime": "Пороговое время изменения состояния (сек)", "events": { - "title": "События", + "desc": "События", "onFault": "Если состояние fault (ошибки) активно", "onLossConnection": "Если соединение по OpenTherm потеряно", "onEnabledHeating": "Если отопление включено" @@ -388,7 +428,7 @@ "note": { "disclaimer1": "После успешного обновления файловой системы ВСЕ настройки будут сброшены на стандартные! Создайте резервную копию ПЕРЕД обновлением.", - "disclaimer2": "После успешного обновления устройство автоматически перезагрузится через 10 секунд." + "disclaimer2": "После успешного обновления устройство автоматически перезагрузится через 15 секунд." }, "settingsFile": "Файл настроек", diff --git a/src_data/pages/dashboard.html b/src_data/pages/dashboard.html index 554327e..d8e9faf 100644 --- a/src_data/pages/dashboard.html +++ b/src_data/pages/dashboard.html @@ -4,19 +4,20 @@ dashboard.title - +