22 Commits

Author SHA1 Message Date
Yurii
9dd5ee8da5 chore: bump pioarduino/platform-espressif32 from 3.1.3 to 3.2.0 2025-05-08 09:39:30 +03:00
Yurii
7dbd503e1e fix: restarting on critically low heap #151 2025-05-07 06:12:39 +03:00
Yurii
85932fdc1d refactor: rounding `heating.setpointTemp` 2025-03-26 16:14:32 +03:00
Yurii
77d80225ad refactor: small changes 2025-03-18 09:26:53 +03:00
Yurii
dc68315166 refactor: added `Msg type` output to opentherm log 2025-03-18 09:21:00 +03:00
Yurii
5d0ca68dc0 fix: `OpenThermTask::setMaxHeatingTemp(const uint8_t temperature)` changed to OpenThermTask::setMaxHeatingTemp(const float temperature) 2025-03-18 09:19:24 +03:00
Yurii
d50b70c211 refactor: improved work with opentherm on esp32 2025-03-14 05:12:20 +03:00
Yurii
dd53d1ef3e chore: bump version to 1.5.4 2025-03-06 19:15:56 +03:00
Yurii
3bd8010b74 refactor: BLE device support for ESP32 C6 (#147)
Building on ``Arduino``+``ESP-IDF`` with ``h2zero/esp-nimble-cpp`` for ESP32 C6
2025-03-06 04:45:13 +03:00
Yurii
6a26e27d39 refactor: heating temperature step changed
* step changed to 0.1
* added processing of long presses on thermostats in the dashboard
2025-03-06 04:29:01 +03:00
Yurii
8fa440810c refactor: status BLE sensors 2025-03-05 02:28:17 +03:00
Yurii
95b18385ba chore: gitignore update 2025-03-04 17:50:59 +03:00
Yurii
4457e16a8f refactor: increased opentherm disconnect timeout 2025-02-27 12:48:27 +03:00
Yurii
1965ca671e chore: bump pioarduino/platform-espressif32 from 3.1.2 to 3.1.3 2025-02-20 16:50:06 +03:00
Yurii
0d1873ec77 fix: set SSID on click in table of available networks fixed 2025-02-17 19:37:51 +03:00
Yurii
38ec56fb33 fix: working with `Sensors::Type::MANUAL` sensors fixed 2025-02-17 18:58:01 +03:00
Yurii
bb7c3eeba3 feat: added mDNS settings 2025-02-15 00:05:10 +03:00
Yurii
0c778d4c7f refactor: added `robots.txt` to disallow indexing 2025-02-14 23:43:25 +03:00
Yurii
2e5e5e59a8 feat: added mDNS 2025-02-14 07:43:52 +03:00
Yurii
e1623e7b63 chore: bump pioarduino/platform-espressif32 from 3.1.1 to 3.1.2 2025-02-14 06:34:46 +03:00
Yurii
80b91d9a01 feat: generate `network.hostname and settings.mqtt.prefix` if they are empty 2025-02-03 06:38:36 +03:00
Roman Andriadi
25b70e4db5 refactor: allow up to 100x correction of sensor values (#137) 2025-02-03 04:56:00 +03:00
26 changed files with 420 additions and 223 deletions

9
.gitignore vendored
View File

@@ -1,8 +1,13 @@
.pio .pio
.vscode .vscode
build/*.bin build/*
data/* data/*
managed_components/*
node_modules/*
secrets.ini secrets.ini
node_modules
package-lock.json package-lock.json
*.lock
sdkconfig.*
CMakeLists.txt
!sdkconfig.defaults
!.gitkeep !.gitkeep

View File

@@ -48,6 +48,10 @@ let paths = {
{ {
src: 'src_data/images/*.*', src: 'src_data/images/*.*',
dest: 'data/static/images/' dest: 'data/static/images/'
},
{
src: 'src_data/*.txt',
dest: 'data/static/'
} }
], ],
pages: { pages: {

View File

@@ -7,7 +7,7 @@ public:
typedef std::function<void(unsigned long, byte)> BeforeSendRequestCallback; typedef std::function<void(unsigned long, byte)> BeforeSendRequestCallback;
typedef std::function<void(unsigned long, unsigned long, OpenThermResponseStatus, byte)> AfterSendRequestCallback; typedef std::function<void(unsigned long, unsigned long, OpenThermResponseStatus, byte)> AfterSendRequestCallback;
CustomOpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false) : OpenTherm(inPin, outPin, isSlave) {} CustomOpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false, bool alwaysReceive = false) : OpenTherm(inPin, outPin, isSlave, alwaysReceive) {}
~CustomOpenTherm() {} ~CustomOpenTherm() {}
CustomOpenTherm* setDelayCallback(DelayCallback callback = nullptr) { CustomOpenTherm* setDelayCallback(DelayCallback callback = nullptr) {
@@ -28,8 +28,8 @@ public:
return this; return this;
} }
unsigned long sendRequest(unsigned long request, byte attempts = 5, byte _attempt = 0) { unsigned long sendRequest(unsigned long request) override {
_attempt++; this->sendRequestAttempt++;
while (!this->isReady()) { while (!this->isReady()) {
if (this->delayCallback) { if (this->delayCallback) {
@@ -40,15 +40,10 @@ public:
} }
if (this->beforeSendRequestCallback) { if (this->beforeSendRequestCallback) {
this->beforeSendRequestCallback(request, _attempt); this->beforeSendRequestCallback(request, this->sendRequestAttempt);
} }
unsigned long _response; if (this->sendRequestAsync(request)) {
OpenThermResponseStatus _responseStatus = OpenThermResponseStatus::NONE;
if (!this->sendRequestAsync(request)) {
_response = 0;
} else {
do { do {
if (this->delayCallback) { if (this->delayCallback) {
this->delayCallback(150); this->delayCallback(150);
@@ -56,42 +51,25 @@ public:
this->process(); this->process();
} while (this->status != OpenThermStatus::READY && this->status != OpenThermStatus::DELAY); } while (this->status != OpenThermStatus::READY && this->status != OpenThermStatus::DELAY);
_response = this->getLastResponse();
_responseStatus = this->getLastResponseStatus();
} }
if (this->afterSendRequestCallback) { if (this->afterSendRequestCallback) {
this->afterSendRequestCallback(request, _response, _responseStatus, _attempt); this->afterSendRequestCallback(request, this->response, this->responseStatus, this->sendRequestAttempt);
} }
if (_responseStatus == OpenThermResponseStatus::SUCCESS || _responseStatus == OpenThermResponseStatus::INVALID || _attempt >= attempts) { if (this->responseStatus == OpenThermResponseStatus::SUCCESS || this->responseStatus == OpenThermResponseStatus::INVALID) {
return _response; this->sendRequestAttempt = 0;
return this->response;
} else if (this->sendRequestAttempt >= this->sendRequestMaxAttempts) {
this->sendRequestAttempt = 0;
return this->response;
} else { } else {
return this->sendRequest(request, attempts, _attempt); return this->sendRequest(request);
} }
} }
unsigned long setBoilerStatus(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2, bool summerWinterMode, bool dhwBlocking, uint8_t lb = 0) {
unsigned int data = enableCentralHeating
| (enableHotWater << 1)
| (enableCooling << 2)
| (enableOutsideTemperatureCompensation << 3)
| (enableCentralHeating2 << 4)
| (summerWinterMode << 5)
| (dhwBlocking << 6);
data <<= 8;
data |= lb;
return this->sendRequest(buildRequest(
OpenThermMessageType::READ_DATA,
OpenThermMessageID::Status,
data
));
}
bool sendBoilerReset() { bool sendBoilerReset() {
unsigned int data = 1; unsigned int data = 1;
data <<= 8; data <<= 8;
@@ -130,10 +108,31 @@ public:
static bool isValidResponseId(unsigned long response, OpenThermMessageID id) { static bool isValidResponseId(unsigned long response, OpenThermMessageID id) {
byte responseId = (response >> 16) & 0xFF; byte responseId = (response >> 16) & 0xFF;
return (byte)id == responseId; return (byte)id == responseId;
} }
static uint8_t getResponseMessageTypeId(unsigned long response) {
return (response << 1) >> 29;
}
static const char* getResponseMessageTypeString(unsigned long response) {
uint8_t msgType = getResponseMessageTypeId(response);
switch (msgType) {
case (uint8_t) OpenThermMessageType::READ_ACK:
case (uint8_t) OpenThermMessageType::WRITE_ACK:
case (uint8_t) OpenThermMessageType::DATA_INVALID:
case (uint8_t) OpenThermMessageType::UNKNOWN_DATA_ID:
return CustomOpenTherm::messageTypeToString(
static_cast<OpenThermMessageType>(msgType)
);
default:
return "UNKNOWN";
}
}
// converters // converters
template <class T> template <class T>
static unsigned int toFloat(const T val) { static unsigned int toFloat(const T val) {
@@ -145,6 +144,8 @@ public:
} }
protected: protected:
const uint8_t sendRequestMaxAttempts = 5;
uint8_t sendRequestAttempt = 0;
DelayCallback delayCallback; DelayCallback delayCallback;
BeforeSendRequestCallback beforeSendRequestCallback; BeforeSendRequestCallback beforeSendRequestCallback;
AfterSendRequestCallback afterSendRequestCallback; AfterSendRequestCallback afterSendRequestCallback;

View File

@@ -14,12 +14,12 @@ extra_configs = secrets.default.ini
core_dir = .pio core_dir = .pio
[env] [env]
version = 1.5.3 version = 1.5.4
framework = arduino framework = arduino
lib_deps = lib_deps =
bblanchon/ArduinoJson@^7.3.0 bblanchon/ArduinoJson@^7.3.0
;ihormelnyk/OpenTherm Library@^1.1.5 ;ihormelnyk/OpenTherm Library@^1.1.5
https://github.com/ihormelnyk/opentherm_library#master https://github.com/Laxilef/opentherm_library#esp32_timer
arduino-libraries/ArduinoMqttClient@^0.1.8 arduino-libraries/ArduinoMqttClient@^0.1.8
lennarthennigs/ESP Telnet@^2.2 lennarthennigs/ESP Telnet@^2.2
gyverlibs/FileData@^1.0.2 gyverlibs/FileData@^1.0.2
@@ -84,7 +84,7 @@ board_build.ldscript = eagle.flash.4m1m.ld
;platform_packages = ;platform_packages =
; framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#3.0.5 ; 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 ; 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.11/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20/platform-espressif32.zip
platform_packages = platform_packages =
board_build.partitions = esp32_partitions.csv board_build.partitions = esp32_partitions.csv
lib_deps = lib_deps =
@@ -287,21 +287,26 @@ build_flags =
[env:esp32_c6] [env:esp32_c6]
platform = ${esp32_defaults.platform} platform = ${esp32_defaults.platform}
framework = arduino, espidf
platform_packages = ${esp32_defaults.platform_packages} platform_packages = ${esp32_defaults.platform_packages}
board = esp32-c6-devkitm-1 board = esp32-c6-devkitm-1
board_build.partitions = ${esp32_defaults.board_build.partitions} board_build.partitions = ${esp32_defaults.board_build.partitions}
lib_deps = lib_deps = ${esp32_defaults.lib_deps}
${esp32_defaults.lib_deps} lib_ignore =
;${esp32_defaults.nimble_lib} ${esp32_defaults.lib_ignore}
lib_ignore = ${esp32_defaults.lib_ignore}
extra_scripts = ${esp32_defaults.extra_scripts} extra_scripts = ${esp32_defaults.extra_scripts}
build_unflags = build_unflags =
-mtext-section-literals -mtext-section-literals
build_type = ${esp32_defaults.build_type} build_type = ${esp32_defaults.build_type}
build_flags = build_flags =
${esp32_defaults.build_flags} ${esp32_defaults.build_flags}
; Currently the NimBLE library is incompatible with ESP32 C6 -D USE_BLE=1
;-D USE_BLE=1 -D DEFAULT_OT_IN_GPIO=15
-D DEFAULT_OT_OUT_GPIO=23
-D DEFAULT_SENSOR_OUTDOOR_GPIO=0
-D DEFAULT_SENSOR_INDOOR_GPIO=0
-D DEFAULT_STATUS_LED_GPIO=11
-D DEFAULT_OT_RX_LED_GPIO=10
[env:otthing] [env:otthing]
platform = ${esp32_defaults.platform} platform = ${esp32_defaults.platform}

33
sdkconfig.defaults Normal file
View File

@@ -0,0 +1,33 @@
# Source:
# https://github.com/pioarduino/platform-espressif32/tree/main/examples/espidf-arduino-h2zero-BLE_scan
CONFIG_FREERTOS_HZ=1000
CONFIG_MBEDTLS_PSK_MODES=y
CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y
CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE=y
#
# BT config
CONFIG_BT_ENABLED=y
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n
CONFIG_BTDM_CTRL_MODE_BTDM=n
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y
#
# Arduino Configuration
CONFIG_AUTOSTART_ARDUINO=y
CONFIG_ARDUINO_SELECTIVE_COMPILATION=y
CONFIG_ARDUINO_SELECTIVE_Zigbee=n
CONFIG_ARDUINO_SELECTIVE_Matter=n
CONFIG_ARDUINO_SELECTIVE_WiFiProv=n
CONFIG_ARDUINO_SELECTIVE_BLE=n
CONFIG_ARDUINO_SELECTIVE_BluetoothSerial=n
CONFIG_ARDUINO_SELECTIVE_SimpleBLE=n
CONFIG_ARDUINO_SELECTIVE_RainMaker=n
CONFIG_ARDUINO_SELECTIVE_OpenThread=n
CONFIG_ARDUINO_SELECTIVE_Insights=n

View File

@@ -1215,7 +1215,7 @@ public:
doc[FPSTR(HA_MIN_TEMP)] = minTemp; doc[FPSTR(HA_MIN_TEMP)] = minTemp;
doc[FPSTR(HA_MAX_TEMP)] = maxTemp; doc[FPSTR(HA_MAX_TEMP)] = maxTemp;
doc[FPSTR(HA_TEMP_STEP)] = 0.5f; doc[FPSTR(HA_TEMP_STEP)] = 0.1f;
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit(); doc.shrinkToFit();

View File

@@ -196,6 +196,7 @@ protected:
// critical heap // critical heap
if (!vars.states.restarting && (freeHeap < 2048 || maxFreeBlockHeap < 2048)) { if (!vars.states.restarting && (freeHeap < 2048 || maxFreeBlockHeap < 2048)) {
this->restartSignalReceivedTime = millis(); this->restartSignalReceivedTime = millis();
this->restartSignalReceived = true;
vars.states.restarting = true; vars.states.restarting = true;
} }

View File

@@ -70,8 +70,8 @@ public:
this->prevPubVarsTime = 0; this->prevPubVarsTime = 0;
} }
inline void rebuildHaEntity(uint8_t sensorId, Sensors::Settings& prevSettings) { inline void reconfigureSensor(uint8_t sensorId, Sensors::Settings& prevSettings) {
this->queueRebuildingHaEntities[sensorId] = prevSettings; this->queueReconfigureSensors[sensorId] = prevSettings;
} }
protected: protected:
@@ -81,7 +81,7 @@ protected:
MqttWriter* writer = nullptr; MqttWriter* writer = nullptr;
UnitSystem currentUnitSystem = UnitSystem::METRIC; UnitSystem currentUnitSystem = UnitSystem::METRIC;
bool currentHomeAssistantDiscovery = false; bool currentHomeAssistantDiscovery = false;
std::unordered_map<uint8_t, Sensors::Settings> queueRebuildingHaEntities; std::unordered_map<uint8_t, Sensors::Settings> queueReconfigureSensors;
unsigned short readyForSendTime = 30000; unsigned short readyForSendTime = 30000;
unsigned long lastReconnectTime = 0; unsigned long lastReconnectTime = 0;
unsigned long connectedTime = 0; unsigned long connectedTime = 0;
@@ -276,8 +276,8 @@ protected:
this->publishNonStaticHaEntities(); this->publishNonStaticHaEntities();
} }
// rebuilding ha configs
for (auto& [sensorId, prevSettings] : this->queueRebuildingHaEntities) { for (auto& [sensorId, prevSettings] : this->queueReconfigureSensors) {
Log.sinfoln(FPSTR(L_MQTT_HA), F("Rebuilding config for sensor #%hhu '%s'"), sensorId, prevSettings.name); Log.sinfoln(FPSTR(L_MQTT_HA), F("Rebuilding config for sensor #%hhu '%s'"), sensorId, prevSettings.name);
// delete old config // delete old config
@@ -297,15 +297,6 @@ protected:
this->haHelper->deleteSignalQualityDynamicSensor(prevSettings); this->haHelper->deleteSignalQualityDynamicSensor(prevSettings);
this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::TEMPERATURE); this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::TEMPERATURE);
break; break;
case Sensors::Type::MANUAL:
this->client->unsubscribe(
this->haHelper->getDeviceTopic(
F("sensors"),
Sensors::makeObjectId(prevSettings.name).c_str(),
F("set")
).c_str()
);
default: default:
this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::PRIMARY); this->haHelper->deleteDynamicSensor(prevSettings, Sensors::ValueType::PRIMARY);
@@ -333,26 +324,51 @@ protected:
this->haHelper->publishSignalQualityDynamicSensor(sSettings, false); this->haHelper->publishSignalQualityDynamicSensor(sSettings, false);
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem); this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem);
break; break;
case Sensors::Type::MANUAL:
this->client->subscribe(
this->haHelper->getDeviceTopic(
F("sensors"),
Sensors::makeObjectId(prevSettings.name).c_str(),
F("set")
).c_str()
);
default: default:
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::PRIMARY, settings.system.unitSystem); this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::PRIMARY, settings.system.unitSystem);
} }
} }
this->queueRebuildingHaEntities.clear();
} else if (this->currentHomeAssistantDiscovery) { } else if (this->currentHomeAssistantDiscovery) {
this->currentHomeAssistantDiscovery = false; this->currentHomeAssistantDiscovery = false;
} }
// reconfigure manual sensors
for (auto& [sensorId, prevSettings] : this->queueReconfigureSensors) {
// unsubscribe from old topic
if (strlen(prevSettings.name) && prevSettings.enabled) {
if (prevSettings.type == Sensors::Type::MANUAL) {
this->client->unsubscribe(
this->haHelper->getDeviceTopic(
F("sensors"),
Sensors::makeObjectId(prevSettings.name).c_str(),
F("set")
).c_str()
);
}
}
if (!Sensors::hasEnabledAndValid(sensorId)) {
continue;
}
// subscribe to new topic
auto& sSettings = Sensors::settings[sensorId];
if (sSettings.type == Sensors::Type::MANUAL) {
this->client->subscribe(
this->haHelper->getDeviceTopic(
F("sensors"),
Sensors::makeObjectId(sSettings.name).c_str(),
F("set")
).c_str()
);
}
}
// clear queue
this->queueReconfigureSensors.clear();
if (this->newConnection) { if (this->newConnection) {
this->newConnection = false; this->newConnection = false;
} }
@@ -366,6 +382,26 @@ protected:
this->client->subscribe(this->haHelper->getDeviceTopic(F("settings/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()); this->client->subscribe(this->haHelper->getDeviceTopic(F("state/set")).c_str());
// subscribe to manual sensors
for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) {
if (!Sensors::hasEnabledAndValid(sensorId)) {
continue;
}
auto& sSettings = Sensors::settings[sensorId];
if (sSettings.type != Sensors::Type::MANUAL) {
continue;
}
this->client->subscribe(
this->haHelper->getDeviceTopic(
F("sensors"),
Sensors::makeObjectId(sSettings.name).c_str(),
F("set")
).c_str()
);
}
} }
void onDisconnect() { void onDisconnect() {
@@ -513,15 +549,6 @@ protected:
this->haHelper->publishSignalQualityDynamicSensor(sSettings, false); this->haHelper->publishSignalQualityDynamicSensor(sSettings, false);
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem); this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem);
break; break;
case Sensors::Type::MANUAL:
this->client->subscribe(
this->haHelper->getDeviceTopic(
F("sensors"),
Sensors::makeObjectId(sSettings.name).c_str(),
F("set")
).c_str()
);
default: default:
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::PRIMARY, settings.system.unitSystem); this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::PRIMARY, settings.system.unitSystem);

View File

@@ -93,8 +93,13 @@ protected:
this->instance->setAfterSendRequestCallback([this](unsigned long request, unsigned long response, OpenThermResponseStatus status, byte attempt) { this->instance->setAfterSendRequestCallback([this](unsigned long request, unsigned long response, OpenThermResponseStatus status, byte attempt) {
Log.sverboseln( Log.sverboseln(
FPSTR(L_OT), FPSTR(L_OT),
F("ID: %4d Request: %8lx Response: %8lx Attempt: %2d Status: %s"), F("ID: %4d Request: %8lx Response: %8lx Msg type: %s Attempt: %2d Status: %s"),
CustomOpenTherm::getDataID(request), request, response, attempt, CustomOpenTherm::statusToString(status) CustomOpenTherm::getDataID(request),
request,
response,
CustomOpenTherm::getResponseMessageTypeString(response),
attempt,
CustomOpenTherm::statusToString(status)
); );
if (status == OpenThermResponseStatus::SUCCESS) { if (status == OpenThermResponseStatus::SUCCESS) {
@@ -138,7 +143,12 @@ protected:
return; return;
} else if (this->instance->status == OpenThermStatus::NOT_INITIALIZED) { } else if (this->instance->status == OpenThermStatus::NOT_INITIALIZED) {
this->instance->begin(); if (!this->instance->begin()) {
Log.swarningln(FPSTR(L_OT), F("Failed begin"));
this->delay(5000);
return;
}
} }
// RX LED GPIO setup // RX LED GPIO setup
@@ -212,9 +222,28 @@ protected:
F("Failed receive boiler status: %s"), F("Failed receive boiler status: %s"),
CustomOpenTherm::statusToString(this->instance->getLastResponseStatus()) CustomOpenTherm::statusToString(this->instance->getLastResponseStatus())
); );
} else {
vars.slave.heating.active = CustomOpenTherm::isCentralHeatingActive(response);
vars.slave.dhw.active = settings.opentherm.options.dhwSupport ? CustomOpenTherm::isHotWaterActive(response) : false;
vars.slave.flame = CustomOpenTherm::isFlameOn(response);
vars.slave.cooling = CustomOpenTherm::isCoolingActive(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; cooling: %hhu; fault: %hhu; diag: %hhu"),
vars.slave.heating.active, vars.slave.dhw.active,
vars.slave.flame, vars.slave.cooling, vars.slave.fault.active, vars.slave.diag.active
);
} }
if (!vars.slave.connected && millis() - this->lastSuccessResponse < 1325) { // 5 request retries
// 1000ms maximum response waiting time
// 100ms delay between requests
// +15%
// 5 * (1000 + 100) * 1.15 = 6325 ms
if (!vars.slave.connected && millis() - this->lastSuccessResponse < 6325) {
Log.sinfoln( Log.sinfoln(
FPSTR(L_OT), FPSTR(L_OT),
F("Connected, downtime: %lu s."), F("Connected, downtime: %lu s."),
@@ -224,7 +253,7 @@ protected:
this->connectedTime = millis(); this->connectedTime = millis();
vars.slave.connected = true; vars.slave.connected = true;
} else if (vars.slave.connected && millis() - this->lastSuccessResponse > 1325) { } else if (vars.slave.connected && millis() - this->lastSuccessResponse > 6325) {
Log.swarningln( Log.swarningln(
FPSTR(L_OT), FPSTR(L_OT),
F("Disconnected, uptime: %lu s."), F("Disconnected, uptime: %lu s."),
@@ -305,19 +334,6 @@ protected:
Log.sinfoln(FPSTR(L_OT_DHW), vars.master.dhw.enabled ? F("Enabled") : F("Disabled")); Log.sinfoln(FPSTR(L_OT_DHW), vars.master.dhw.enabled ? F("Enabled") : F("Disabled"));
} }
vars.slave.heating.active = CustomOpenTherm::isCentralHeatingActive(response);
vars.slave.dhw.active = settings.opentherm.options.dhwSupport ? CustomOpenTherm::isHotWaterActive(response) : false;
vars.slave.flame = CustomOpenTherm::isFlameOn(response);
vars.slave.cooling = CustomOpenTherm::isCoolingActive(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; cooling: %hhu; fault: %hhu; diag: %hhu"),
vars.slave.heating.active, vars.slave.dhw.active,
vars.slave.flame, vars.slave.cooling, vars.slave.fault.active, vars.slave.diag.active
);
// These parameters will be updated every minute // These parameters will be updated every minute
if (millis() - this->prevUpdateNonEssentialVars > 60000) { if (millis() - this->prevUpdateNonEssentialVars > 60000) {
if (this->updateMinModulationLevel()) { if (this->updateMinModulationLevel()) {
@@ -1174,17 +1190,17 @@ protected:
bool needSetDhwTemp(const float target) { bool needSetDhwTemp(const float target) {
return millis() - this->dhwSetTempTime > this->dhwSetTempInterval return millis() - this->dhwSetTempTime > this->dhwSetTempInterval
|| fabsf(target - vars.slave.dhw.targetTemp) > 0.001f; || fabsf(target - vars.slave.dhw.targetTemp) > 0.05f;
} }
bool needSetHeatingTemp(const float target) { bool needSetHeatingTemp(const float target) {
return millis() - this->heatingSetTempTime > this->heatingSetTempInterval return millis() - this->heatingSetTempTime > this->heatingSetTempInterval
|| fabsf(target - vars.slave.heating.targetTemp) > 0.001f; || fabsf(target - vars.slave.heating.targetTemp) > 0.05f;
} }
bool needSetCh2Temp(const float target) { bool needSetCh2Temp(const float target) {
return millis() - this->ch2SetTempTime > this->ch2SetTempInterval return millis() - this->ch2SetTempTime > this->ch2SetTempInterval
|| fabsf(target - vars.slave.ch2.targetTemp) > 0.001f; || fabsf(target - vars.slave.ch2.targetTemp) > 0.05f;
} }
bool updateSlaveConfig() { bool updateSlaveConfig() {
@@ -1345,7 +1361,7 @@ protected:
return CustomOpenTherm::getUInt(response) == request; return CustomOpenTherm::getUInt(response) == request;
} }
bool setMaxHeatingTemp(const uint8_t temperature) { bool setMaxHeatingTemp(const float temperature) {
const unsigned int request = CustomOpenTherm::temperatureToData(temperature); const unsigned int request = CustomOpenTherm::temperatureToData(temperature);
const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest( const unsigned long response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermMessageType::WRITE_DATA, OpenThermMessageType::WRITE_DATA,

View File

@@ -1,10 +1,12 @@
//#define PORTAL_CACHE "max-age=86400" //#define PORTAL_CACHE "max-age=86400"
#define PORTAL_CACHE nullptr #define PORTAL_CACHE nullptr
#ifdef ARDUINO_ARCH_ESP8266 #ifdef ARDUINO_ARCH_ESP8266
#include <ESP8266mDNS.h>
#include <ESP8266WebServer.h> #include <ESP8266WebServer.h>
#include <Updater.h> #include <Updater.h>
using WebServer = ESP8266WebServer; using WebServer = ESP8266WebServer;
#else #else
#include <ESPmDNS.h>
#include <WebServer.h> #include <WebServer.h>
#include <Update.h> #include <Update.h>
#endif #endif
@@ -53,7 +55,7 @@ protected:
bool webServerEnabled = false; bool webServerEnabled = false;
bool dnsServerEnabled = false; bool dnsServerEnabled = false;
unsigned long webServerChangeState = 0; unsigned long webServerChangeState = 0;
unsigned long dnsServerChangeState = 0; bool mDnsState = false;
#if defined(ARDUINO_ARCH_ESP32) #if defined(ARDUINO_ARCH_ESP32)
const char* getTaskName() override { const char* getTaskName() override {
@@ -616,7 +618,7 @@ protected:
} }
if (changed) { if (changed) {
tMqtt->rebuildHaEntity(sensorId, prevSettings); tMqtt->reconfigureSensor(sensorId, prevSettings);
fsSensorsSettings.update(); fsSensorsSettings.update();
} }
}); });
@@ -858,6 +860,7 @@ protected:
} }
}); });
this->webServer->serveStatic("/robots.txt", LittleFS, "/static/robots.txt", PORTAL_CACHE);
this->webServer->serveStatic("/favicon.ico", LittleFS, "/static/images/favicon.ico", PORTAL_CACHE); this->webServer->serveStatic("/favicon.ico", LittleFS, "/static/images/favicon.ico", PORTAL_CACHE);
this->webServer->serveStatic("/static", LittleFS, "/static", PORTAL_CACHE); this->webServer->serveStatic("/static", LittleFS, "/static", PORTAL_CACHE);
} }
@@ -872,6 +875,16 @@ protected:
this->startWebServer(); this->startWebServer();
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Started: AP up or STA connected")); Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Started: AP up or STA connected"));
// Enabling mDNS
if (!this->mDnsState && settings.portal.mdns) {
if (MDNS.begin(networkSettings.hostname)) {
MDNS.addService("http", "tcp", 80);
this->mDnsState = true;
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("mDNS enabled and service added"));
}
}
#ifdef ARDUINO_ARCH_ESP8266 #ifdef ARDUINO_ARCH_ESP8266
::optimistic_yield(1000); ::optimistic_yield(1000);
#endif #endif
@@ -880,13 +893,29 @@ protected:
this->stopWebServer(); this->stopWebServer();
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Stopped: AP and STA down")); Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Stopped: AP and STA down"));
// Disabling mDNS
if (this->mDnsState) {
MDNS.end();
this->mDnsState = false;
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("mDNS disabled"));
}
#ifdef ARDUINO_ARCH_ESP8266 #ifdef ARDUINO_ARCH_ESP8266
::optimistic_yield(1000); ::optimistic_yield(1000);
#endif #endif
} }
// Disabling mDNS if disabled in settings
if (this->mDnsState && !settings.portal.mdns) {
MDNS.end();
this->mDnsState = false;
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("mDNS disabled"));
}
// dns server // dns server
if (!this->stateDnsServer() && this->stateWebServer() && network->isApEnabled() && network->hasApClients() && millis() - this->dnsServerChangeState >= this->changeStateInterval) { if (!this->stateDnsServer() && !network->isConnected() && network->isApEnabled() && this->stateWebServer()) {
this->startDnsServer(); this->startDnsServer();
Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Started: AP up")); Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Started: AP up"));
@@ -894,9 +923,9 @@ protected:
::optimistic_yield(1000); ::optimistic_yield(1000);
#endif #endif
} else if (this->stateDnsServer() && (!network->isApEnabled() || !this->stateWebServer())) { } else if (this->stateDnsServer() && (network->isConnected() || !network->isApEnabled() || !this->stateWebServer())) {
this->stopDnsServer(); this->stopDnsServer();
Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Stopped: AP down")); Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Stopped: AP down/STA connected"));
#ifdef ARDUINO_ARCH_ESP8266 #ifdef ARDUINO_ARCH_ESP8266
::optimistic_yield(1000); ::optimistic_yield(1000);
@@ -1006,7 +1035,6 @@ protected:
this->dnsServer->start(53, "*", network->getApIp()); this->dnsServer->start(53, "*", network->getApIp());
this->dnsServerEnabled = true; this->dnsServerEnabled = true;
this->dnsServerChangeState = millis();
} }
void stopDnsServer() { void stopDnsServer() {
@@ -1017,6 +1045,5 @@ protected:
//this->dnsServer->processNextRequest(); //this->dnsServer->processNextRequest();
this->dnsServer->stop(); this->dnsServer->stop();
this->dnsServerEnabled = false; this->dnsServerEnabled = false;
this->dnsServerChangeState = millis();
} }
}; };

View File

@@ -60,11 +60,11 @@ protected:
this->hysteresis(); this->hysteresis();
vars.master.heating.targetTemp = settings.heating.target; vars.master.heating.targetTemp = settings.heating.target;
vars.master.heating.setpointTemp = constrain( vars.master.heating.setpointTemp = roundf(constrain(
this->getHeatingSetpointTemp(), this->getHeatingSetpointTemp(),
this->getHeatingMinSetpointTemp(), this->getHeatingMinSetpointTemp(),
this->getHeatingMaxSetpointTemp() this->getHeatingMaxSetpointTemp()
); ), 0);
Sensors::setValueByType( Sensors::setValueByType(
Sensors::Type::HEATING_SETPOINT_TEMP, vars.master.heating.setpointTemp, Sensors::Type::HEATING_SETPOINT_TEMP, vars.master.heating.setpointTemp,

View File

@@ -496,9 +496,8 @@ protected:
} }
} }
if (!rSensor.connected) { // Mark connected
rSensor.connected = true; Sensors::setConnectionStatusById(sensorId, true, true);
}
if (!this->bleLastSetDtTime[sensorId] || millis() - this->bleLastSetDtTime[sensorId] > this->bleSetDtInterval) { if (!this->bleLastSetDtTime[sensorId] || millis() - this->bleLastSetDtTime[sensorId] > this->bleSetDtInterval) {
struct tm ti; struct tm ti;
@@ -521,7 +520,6 @@ protected:
this->bleLastSetDtTime[sensorId] = millis(); this->bleLastSetDtTime[sensorId] = millis();
} }
} }
} }
} }
@@ -981,16 +979,16 @@ protected:
auto& rSensor = Sensors::results[sensorId]; auto& rSensor = Sensors::results[sensorId];
if (rSensor.connected && !sSensor.enabled) { if (rSensor.connected && !sSensor.enabled) {
rSensor.connected = false; Sensors::setConnectionStatusById(sensorId, false, false);
} else if (rSensor.connected && sSensor.type == Sensors::Type::NOT_CONFIGURED) { } else if (rSensor.connected && sSensor.type == Sensors::Type::NOT_CONFIGURED) {
rSensor.connected = false; Sensors::setConnectionStatusById(sensorId, false, false);
} else if (rSensor.connected && sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) { } else if (rSensor.connected && sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) {
rSensor.connected = false; Sensors::setConnectionStatusById(sensorId, false, false);
} else if (sSensor.type != Sensors::Type::MANUAL && rSensor.connected && (millis() - rSensor.activityTime) > this->disconnectedTimeout) { } else if (sSensor.type != Sensors::Type::MANUAL && rSensor.connected && (millis() - rSensor.activityTime) > this->disconnectedTimeout) {
rSensor.connected = false; Sensors::setConnectionStatusById(sensorId, false, false);
}/* else if (!rSensor.connected) { }/* else if (!rSensor.connected) {
rSensor.connected = true; rSensor.connected = true;

View File

@@ -49,6 +49,7 @@ struct Settings {
bool auth = false; bool auth = false;
char login[13] = DEFAULT_PORTAL_LOGIN; char login[13] = DEFAULT_PORTAL_LOGIN;
char password[33] = DEFAULT_PORTAL_PASSWORD; char password[33] = DEFAULT_PORTAL_PASSWORD;
bool mdns = true;
} portal; } portal;
struct { struct {

View File

@@ -55,7 +55,7 @@
#endif #endif
#ifndef DEFAULT_HOSTNAME #ifndef DEFAULT_HOSTNAME
#define DEFAULT_HOSTNAME "opentherm" #define DEFAULT_HOSTNAME ""
#endif #endif
#ifndef DEFAULT_AP_SSID #ifndef DEFAULT_AP_SSID
@@ -111,7 +111,7 @@
#endif #endif
#ifndef DEFAULT_MQTT_PREFIX #ifndef DEFAULT_MQTT_PREFIX
#define DEFAULT_MQTT_PREFIX "opentherm" #define DEFAULT_MQTT_PREFIX ""
#endif #endif
#ifndef DEFAULT_OT_IN_GPIO #ifndef DEFAULT_OT_IN_GPIO

3
src/idf_component.yml Normal file
View File

@@ -0,0 +1,3 @@
dependencies:
idf: ">=5.3.2"
h2zero/esp-nimble-cpp: ">=2.2.1"

View File

@@ -102,6 +102,12 @@ void setup() {
break; break;
} }
// generate hostname if it is empty
if (!strlen(networkSettings.hostname)) {
strcpy(networkSettings.hostname, getChipId("otgateway-").c_str());
fsNetworkSettings.update();
}
network = (new NetworkMgr) network = (new NetworkMgr)
->setHostname(networkSettings.hostname) ->setHostname(networkSettings.hostname)
->setStaCredentials( ->setStaCredentials(
@@ -148,6 +154,12 @@ void setup() {
break; break;
} }
// generate mqtt prefix if it is empty
if (!strlen(settings.mqtt.prefix)) {
strcpy(settings.mqtt.prefix, getChipId("otgateway_").c_str());
fsSettings.update();
}
// Logs settings // Logs settings
if (!settings.system.serial.enabled) { if (!settings.system.serial.enabled) {
Serial.end(); Serial.end();

View File

@@ -122,6 +122,7 @@ const char S_MAX_MODULATION[] PROGMEM = "maxModulation";
const char S_MAX_POWER[] PROGMEM = "maxPower"; const char S_MAX_POWER[] PROGMEM = "maxPower";
const char S_MAX_TEMP[] PROGMEM = "maxTemp"; const char S_MAX_TEMP[] PROGMEM = "maxTemp";
const char S_MAX_TEMP_SYNC_WITH_TARGET_TEMP[] PROGMEM = "maxTempSyncWithTargetTemp"; const char S_MAX_TEMP_SYNC_WITH_TARGET_TEMP[] PROGMEM = "maxTempSyncWithTargetTemp";
const char S_MDNS[] PROGMEM = "mdns";
const char S_MEMBER_ID[] PROGMEM = "memberId"; const char S_MEMBER_ID[] PROGMEM = "memberId";
const char S_MIN[] PROGMEM = "min"; const char S_MIN[] PROGMEM = "min";
const char S_MIN_FREE[] PROGMEM = "minFree"; const char S_MIN_FREE[] PROGMEM = "minFree";

View File

@@ -1,5 +1,37 @@
#include <Arduino.h> #include <Arduino.h>
String getChipId(const char* prefix = nullptr, const char* suffix = nullptr) {
String chipId;
chipId.reserve(
6
+ (prefix != nullptr ? strlen(prefix) : 0)
+ (suffix != nullptr ? strlen(suffix) : 0)
);
if (prefix != nullptr) {
chipId.concat(prefix);
}
uint32_t cid = 0;
#if defined(ARDUINO_ARCH_ESP8266)
cid = ESP.getChipId();
#elif defined(ARDUINO_ARCH_ESP32)
// https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/ChipID/GetChipID/GetChipID.ino
for (uint8_t i = 0; i < 17; i = i + 8) {
cid |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
}
#endif
chipId += String(cid, HEX);
if (suffix != nullptr) {
chipId.concat(suffix);
}
chipId.trim();
return chipId;
}
bool isLeapYear(short year) { bool isLeapYear(short year) {
if (year % 4 != 0) { if (year % 4 != 0) {
return false; return false;
@@ -408,6 +440,7 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
portal[FPSTR(S_AUTH)] = src.portal.auth; portal[FPSTR(S_AUTH)] = src.portal.auth;
portal[FPSTR(S_LOGIN)] = src.portal.login; portal[FPSTR(S_LOGIN)] = src.portal.login;
portal[FPSTR(S_PASSWORD)] = src.portal.password; portal[FPSTR(S_PASSWORD)] = src.portal.password;
portal[FPSTR(S_MDNS)] = src.portal.mdns;
auto opentherm = dst[FPSTR(S_OPENTHERM)].to<JsonObject>(); auto opentherm = dst[FPSTR(S_OPENTHERM)].to<JsonObject>();
opentherm[FPSTR(S_UNIT_SYSTEM)] = static_cast<uint8_t>(src.opentherm.unitSystem); opentherm[FPSTR(S_UNIT_SYSTEM)] = static_cast<uint8_t>(src.opentherm.unitSystem);
@@ -675,6 +708,15 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
changed = true; changed = true;
} }
if (src[FPSTR(S_PORTAL)][FPSTR(S_MDNS)].is<bool>()) {
bool value = src[FPSTR(S_PORTAL)][FPSTR(S_MDNS)].as<bool>();
if (value != dst.portal.mdns) {
dst.portal.mdns = value;
changed = true;
}
}
// opentherm // opentherm
if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_UNIT_SYSTEM)].isNull()) { if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_UNIT_SYSTEM)].isNull()) {
@@ -1791,7 +1833,7 @@ bool jsonToSensorSettings(const uint8_t sensorId, const JsonVariantConst src, Se
if (!src[FPSTR(S_FACTOR)].isNull()) { if (!src[FPSTR(S_FACTOR)].isNull()) {
float value = src[FPSTR(S_FACTOR)].as<float>(); float value = src[FPSTR(S_FACTOR)].as<float>();
if (value > 0.09f && value <= 10.0f && fabsf(value - dst.factor) > 0.0001f) { if (value > 0.09f && value <= 100.0f && fabsf(value - dst.factor) > 0.0001f) {
dst.factor = roundf(value, 3); dst.factor = roundf(value, 3);
changed = true; changed = true;
} }

View File

@@ -296,7 +296,8 @@
"portal": { "portal": {
"login": "Login", "login": "Login",
"password": "Password", "password": "Password",
"auth": "Require authentication" "auth": "Require authentication",
"mdns": "Use mDNS"
}, },
"system": { "system": {

View File

@@ -296,7 +296,8 @@
"portal": { "portal": {
"login": "Login", "login": "Login",
"password": "Password", "password": "Password",
"auth": "Richiede autenticazione" "auth": "Richiede autenticazione",
"mdns": "Usa mDNS"
}, },
"system": { "system": {

View File

@@ -296,7 +296,8 @@
"portal": { "portal": {
"login": "Логин", "login": "Логин",
"password": "Пароль", "password": "Пароль",
"auth": "Требовать аутентификацию" "auth": "Требовать аутентификацию",
"mdns": "Использовать mDNS"
}, },
"system": { "system": {

View File

@@ -41,14 +41,18 @@
<details open> <details open>
<summary><b data-i18n>dashboard.section.control</b></summary> <summary><b data-i18n>dashboard.section.control</b></summary>
<div class="grid"> <div class="grid">
<div class="thermostat" id="thermostat-heating"> <div class="thermostat tHeat" data-purpose="heating" data-min="0" data-max="100" data-step="0.1" data-big-step="1">
<div class="thermostat-header" data-i18n>dashboard.thermostat.heating</div> <div class="thermostat-header" data-i18n>dashboard.thermostat.heating</div>
<div class="thermostat-temp"> <div class="thermostat-temp">
<div class="thermostat-temp-target"><span id="tHeatTargetTemp"></span> <span class="tempUnit"></span></div> <div class="thermostat-temp-target"><span class="targetTemp"></span> <span class="tempUnit"></span></div>
<div class="thermostat-temp-current"><span data-i18n>dashboard.thermostat.temp.current</span>: <span id="tHeatCurrentTemp"></span> <span class="tempUnit"></span></div> <div class="thermostat-temp-current"><span data-i18n>dashboard.thermostat.temp.current</span>: <span id="tHeatCurrentTemp"></span> <span class="tempUnit"></span></div>
</div> </div>
<div class="thermostat-minus"><button id="tHeatActionMinus" class="outline"><i class="icons-down"></i></button></div> <div class="thermostat-minus">
<div class="thermostat-plus"><button id="tHeatActionPlus" class="outline"><i class="icons-up"></i></button></div> <button class="tAction outline" data-action="decrement"><i class="icons-down"></i></button>
</div>
<div class="thermostat-plus">
<button class="tAction outline" data-action="increment"><i class="icons-up"></i></button>
</div>
<div class="thermostat-control"> <div class="thermostat-control">
<input type="checkbox" role="switch" id="tHeatEnabled" value="true"> <input type="checkbox" role="switch" id="tHeatEnabled" value="true">
<label htmlFor="tHeatEnabled" data-i18n>dashboard.thermostat.enable</label> <label htmlFor="tHeatEnabled" data-i18n>dashboard.thermostat.enable</label>
@@ -58,21 +62,25 @@
</div> </div>
</div> </div>
<div class="thermostat" id="thermostat-dhw"> <div class="thermostat tDhw" data-purpose="dhw" data-min="0" data-max="100" data-step="1" data-big-step="5">
<div class="thermostat-header" data-i18n>dashboard.thermostat.dhw</div> <div class="thermostat-header" data-i18n>dashboard.thermostat.dhw</div>
<div class="thermostat-temp"> <div class="thermostat-temp">
<div class="thermostat-temp-target"><span id="tDhwTargetTemp"></span> <span class="tempUnit"></span></div> <div class="thermostat-temp-target"><span class="targetTemp"></span> <span class="tempUnit"></span></div>
<div class="thermostat-temp-current"><span data-i18n>dashboard.thermostat.temp.current</span>: <span id="tDhwCurrentTemp"></span> <span class="tempUnit"></span></div> <div class="thermostat-temp-current"><span data-i18n>dashboard.thermostat.temp.current</span>: <span id="tDhwCurrentTemp"></span> <span class="tempUnit"></span></div>
</div> </div>
<div class="thermostat-minus"><button class="outline" id="tDhwActionMinus"><i class="icons-down"></i></button></div> <div class="thermostat-minus">
<div class="thermostat-plus"><button class="outline" id="tDhwActionPlus"><i class="icons-up"></i></button></div> <button class="tAction outline" data-action="decrement"><i class="icons-down"></i></button>
</div>
<div class="thermostat-plus">
<button class="tAction outline" data-action="increment"><i class="icons-up"></i></button>
</div>
<div class="thermostat-control"> <div class="thermostat-control">
<input type="checkbox" role="switch" id="tDhwEnabled" value="true"> <input type="checkbox" role="switch" id="tDhwEnabled" value="true">
<label htmlFor="tDhwEnabled" data-i18n>dashboard.thermostat.enable</label> <label htmlFor="tDhwEnabled" data-i18n>dashboard.thermostat.enable</label>
</div> </div>
</div> </div>
</div> </div>
<div class="notify notify-error notify-fault hidden"> <div class="notify notify-error notify-fault hidden">
<div class="notify-icon"> <div class="notify-icon">
<i class="icons-error"></i> <i class="icons-error"></i>
@@ -282,7 +290,6 @@
<script src="/static/app.js?{BUILD_TIME}"></script> <script src="/static/app.js?{BUILD_TIME}"></script>
<script> <script>
let modifiedTime = null; let modifiedTime = null;
let noRegulators;
let prevSettings; let prevSettings;
let newSettings = { let newSettings = {
heating: { heating: {
@@ -300,78 +307,69 @@
const lang = new Lang(document.getElementById('lang')); const lang = new Lang(document.getElementById('lang'));
lang.build(); lang.build();
document.querySelector('#tHeatActionMinus').addEventListener('click', (event) => { let actionTimer = null;
if (!prevSettings) { let actionLongPress = false;
return; document.querySelectorAll('.tAction').forEach((item) => {
const action = item.dataset.action;
const tContainer = item.parentNode.parentNode;
for (const eName of ['pointerup', 'pointercancel']) {
item.addEventListener(eName, (event) => {
clearInterval(actionTimer);
const purpose = tContainer.dataset.purpose;
const minTemp = parseFloat(tContainer.dataset.min);
const maxTemp = parseFloat(tContainer.dataset.max);
const step = parseFloat(tContainer.dataset.step);
const bigStep = parseFloat(tContainer.dataset.bigStep);
if (!actionLongPress && prevSettings) {
let value = 0;
if (action == 'increment') {
value = step;
} else if (action == 'decrement') {
value = -(step);
}
newSettings[purpose].target = parseFloat(constrain(newSettings[purpose].target + value, minTemp, maxTemp).toFixed(2));
modifiedTime = Date.now();
setValue('.targetTemp', newSettings[purpose].target, tContainer);
}
});
} }
newSettings.heating.target -= 0.5; item.addEventListener('pointerdown', (event) => {
modifiedTime = Date.now(); if (!prevSettings) {
return;
}
let minTemp; const purpose = tContainer.dataset.purpose;
if (noRegulators) { const minTemp = parseFloat(tContainer.dataset.min);
minTemp = prevSettings.heating.minTemp; const maxTemp = parseFloat(tContainer.dataset.max);
} else { const step = parseFloat(tContainer.dataset.step);
minTemp = prevSettings.system.unitSystem == 0 ? 5 : 41; const bigStep = parseFloat(tContainer.dataset.bigStep);
}
if (prevSettings && newSettings.heating.target < minTemp) { actionLongPress = false;
newSettings.heating.target = minTemp; actionTimer = setInterval(() => {
} if (!actionLongPress) {
actionLongPress = true;
}
setValue('#tHeatTargetTemp', newSettings.heating.target); let value = 0;
}); if (action == 'increment') {
value = bigStep;
document.querySelector('#tHeatActionPlus').addEventListener('click', (event) => { } else if (action == 'decrement') {
if (!prevSettings) { value = -(bigStep);
return; }
}
newSettings.heating.target += 0.5; newSettings[purpose].target = parseFloat(constrain(newSettings[purpose].target + value, minTemp, maxTemp).toFixed(2));
modifiedTime = Date.now(); modifiedTime = Date.now();
let maxTemp; setValue('.targetTemp', newSettings[purpose].target, tContainer);
if (noRegulators) { }, 500);
maxTemp = prevSettings.heating.maxTemp; });
} else {
maxTemp = prevSettings.system.unitSystem == 0 ? 30 : 86;
}
if (prevSettings && newSettings.heating.target > maxTemp) {
newSettings.heating.target = maxTemp;
}
setValue('#tHeatTargetTemp', newSettings.heating.target);
});
document.querySelector('#tDhwActionMinus').addEventListener('click', (event) => {
if (!prevSettings) {
return;
}
newSettings.dhw.target -= 1.0;
modifiedTime = Date.now();
if (newSettings.dhw.target < prevSettings.dhw.minTemp) {
newSettings.dhw.target = prevSettings.dhw.minTemp;
}
setValue('#tDhwTargetTemp', newSettings.dhw.target);
});
document.querySelector('#tDhwActionPlus').addEventListener('click', (event) => {
if (!prevSettings) {
return;
}
newSettings.dhw.target += 1.0;
modifiedTime = Date.now();
if (newSettings.dhw.target > prevSettings.dhw.maxTemp) {
newSettings.dhw.target = prevSettings.dhw.maxTemp;
}
setValue('#tDhwTargetTemp', newSettings.dhw.target);
}); });
document.querySelector('#tHeatEnabled').addEventListener('change', (event) => { document.querySelector('#tHeatEnabled').addEventListener('change', (event) => {
@@ -486,7 +484,6 @@
} }
const result = await response.json(); const result = await response.json();
noRegulators = !result.opentherm.options.nativeHeatingControl && !result.equitherm.enabled && !result.pid.enabled;
prevSettings = result; prevSettings = result;
unitSystem = result.system.unitSystem; unitSystem = result.system.unitSystem;
newSettings.heating.enabled = result.heating.enabled; newSettings.heating.enabled = result.heating.enabled;
@@ -496,17 +493,17 @@
newSettings.dhw.target = result.dhw.target; newSettings.dhw.target = result.dhw.target;
if (result.opentherm.options.dhwSupport) { if (result.opentherm.options.dhwSupport) {
show('#thermostat-dhw'); show('.tDhw');
} else { } else {
hide('#thermostat-dhw'); hide('.tDhw');
} }
setCheckboxValue('#tHeatEnabled', result.heating.enabled); setCheckboxValue('#tHeatEnabled', result.heating.enabled);
setCheckboxValue('#tHeatTurbo', result.heating.turbo); setCheckboxValue('#tHeatTurbo', result.heating.turbo);
setValue('#tHeatTargetTemp', result.heating.target); setValue('.tHeat .targetTemp', result.heating.target);
setCheckboxValue('#tDhwEnabled', result.dhw.enabled); setCheckboxValue('#tDhwEnabled', result.dhw.enabled);
setValue('#tDhwTargetTemp', result.dhw.target); setValue('.tDhw .targetTemp', result.dhw.target);
setValue('.tempUnit', temperatureUnit(unitSystem)); setValue('.tempUnit', temperatureUnit(unitSystem));
setValue('.pressureUnit', pressureUnit(unitSystem)); setValue('.pressureUnit', pressureUnit(unitSystem));
@@ -523,20 +520,20 @@
cache: "no-cache", cache: "no-cache",
credentials: "include" credentials: "include"
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Response not valid'); throw new Error('Response not valid');
} }
const result = await response.json(); const result = await response.json();
// Graph // Graph
setValue('#tHeatCurrentTemp', result.master.heating.indoorTempControl setValue('#tHeatCurrentTemp', result.master.heating.indoorTempControl
? result.master.heating.indoorTemp ? result.master.heating.indoorTemp
: result.master.heating.currentTemp : result.master.heating.currentTemp
); );
setValue('#tDhwCurrentTemp', result.master.dhw.currentTemp); setValue('#tDhwCurrentTemp', result.master.dhw.currentTemp);
// SLAVE // SLAVE
setValue('.sMemberId', result.slave.memberId); setValue('.sMemberId', result.slave.memberId);
@@ -646,6 +643,14 @@
setState('.mCascadeControlInput', result.master.cascadeControl.input); setState('.mCascadeControlInput', result.master.cascadeControl.input);
setState('.mCascadeControlOutput', result.master.cascadeControl.output); setState('.mCascadeControlOutput', result.master.cascadeControl.output);
const tHeat = document.querySelector('.tHeat');
tHeat.dataset.min = result.master.heating.minTemp;
tHeat.dataset.max = result.master.heating.maxTemp;
const tDhw = document.querySelector('.tDhw');
tDhw.dataset.min = result.master.dhw.minTemp;
tDhw.dataset.max = result.master.dhw.maxTemp;
setBusy('#dashboard-busy', '#dashboard-container', false); setBusy('#dashboard-busy', '#dashboard-container', false);
} catch (error) { } catch (error) {
@@ -658,7 +663,7 @@
cache: "no-cache", cache: "no-cache",
credentials: "include" credentials: "include"
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Response not valid"); throw new Error("Response not valid");
} }
@@ -684,7 +689,7 @@
if (!sensorNode) { if (!sensorNode) {
continue; continue;
} }
const sData = result[sensorId]; const sData = result[sensorId];
if (!sData.enabled || sData.purpose == 255) { if (!sData.enabled || sData.purpose == 255) {
sensorNode.classList.toggle("hidden", true); sensorNode.classList.toggle("hidden", true);

View File

@@ -140,7 +140,7 @@
<label> <label>
<span data-i18n>sensors.correction.factor</span> <span data-i18n>sensors.correction.factor</span>
<input type="number" inputmode="decimal" name="factor" min="0.01" max="10" step="0.01" required> <input type="number" inputmode="decimal" name="factor" min="0.01" max="100" step="0.01" required>
</label> </label>
</div> </div>
</details> </details>

View File

@@ -57,6 +57,11 @@
<input type="checkbox" name="portal[auth]" value="true"> <input type="checkbox" name="portal[auth]" value="true">
<span data-i18n>settings.portal.auth</span> <span data-i18n>settings.portal.auth</span>
</label> </label>
<label>
<input type="checkbox" name="portal[mdns]" value="true">
<span data-i18n>settings.portal.mdns</span>
</label>
<br /> <br />
<button type="submit" data-i18n>button.save</button> <button type="submit" data-i18n>button.save</button>
@@ -773,6 +778,7 @@
setCheckboxValue("[name='portal[auth]']", data.portal.auth); setCheckboxValue("[name='portal[auth]']", data.portal.auth);
setInputValue("[name='portal[login]']", data.portal.login); setInputValue("[name='portal[login]']", data.portal.login);
setInputValue("[name='portal[password]']", data.portal.password); setInputValue("[name='portal[password]']", data.portal.password);
setCheckboxValue("[name='portal[mdns]']", data.portal.mdns);
setBusy('#portal-settings-busy', '#portal-settings', false); setBusy('#portal-settings-busy', '#portal-settings', false);
// Opentherm // Opentherm

2
src_data/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -132,10 +132,14 @@ const setupNetworkScanForm = (formSelector, tableSelector) => {
for (let i = 0; i < result.length; i++) { for (let i = 0; i < result.length; i++) {
let row = tbody.insertRow(-1); let row = tbody.insertRow(-1);
row.classList.add("network"); row.classList.add("network");
row.setAttribute('data-ssid', result[i].hidden ? '' : result[i].ssid); row.dataset.ssid = result[i].hidden ? '' : result[i].ssid;
row.onclick = () => { row.insertCell().textContent = `#${i + 1}`;
const input = document.querySelector('input#sta-ssid');
const ssid = this.getAttribute('data-ssid'); const nameCell = row.insertCell();
nameCell.innerHTML = result[i].hidden ? `<i>${result[i].bssid}</i>` : result[i].ssid;
nameCell.onclick = (event) => {
const input = document.querySelector("[name='sta[ssid]']");
const ssid = event.target.parentNode.dataset.ssid;
if (!input || !ssid) { if (!input || !ssid) {
return; return;
} }
@@ -144,9 +148,6 @@ const setupNetworkScanForm = (formSelector, tableSelector) => {
input.focus(); input.focus();
}; };
row.insertCell().textContent = `#${i + 1}`;
row.insertCell().innerHTML = result[i].hidden ? `<i>${result[i].bssid}</i>` : result[i].ssid;
// info cell // info cell
let infoCell = row.insertCell(); let infoCell = row.insertCell();
@@ -165,7 +166,7 @@ const setupNetworkScanForm = (formSelector, tableSelector) => {
} }
let signalQualityContainer = document.createElement("span"); let signalQualityContainer = document.createElement("span");
signalQualityContainer.setAttribute('data-tooltip', `${result[i].signalQuality}%`); signalQualityContainer.dataset.tooltip = `${result[i].signalQuality}%`;
signalQualityContainer.appendChild(signalQualityIcon); signalQualityContainer.appendChild(signalQualityIcon);
infoCell.appendChild(signalQualityContainer); infoCell.appendChild(signalQualityContainer);
@@ -192,7 +193,7 @@ const setupNetworkScanForm = (formSelector, tableSelector) => {
} }
let authContainer = document.createElement("span"); let authContainer = document.createElement("span");
authContainer.setAttribute('data-tooltip', (result[i].auth in authList) ? authList[result[i].auth] : "unknown"); authContainer.dataset.tooltip = (result[i].auth in authList) ? authList[result[i].auth] : "unknown";
authContainer.appendChild(authIcon); authContainer.appendChild(authIcon);
infoCell.appendChild(authContainer); infoCell.appendChild(authContainer);
} }
@@ -848,4 +849,8 @@ function dec2hex(i) {
} }
return hex.toUpperCase(); return hex.toUpperCase();
}
function constrain(amt, low, high) {
return ((amt) < (low) ? (low) : ((amt) > (high) ? (high) : (amt)));
} }