Compare commits

..
23 Commits
Author SHA1 Message Date
P43YM 0d3adad446 refactor: added notes for equitherm parameters 2025-03-18 09:38:09 +03:00
Yurii 587678f184 Merge branch 'master' into new-equitherm 2025-03-06 04:52:12 +03:00
Yurii 72235286c0 chore: resolve conflicts 2025-03-06 04:50:02 +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 ee868b22ce chore: remove unused files 2025-03-04 21:31:44 +03:00
Yurii 95b18385ba chore: gitignore update 2025-03-04 17:50:59 +03:00
Yurii 8046a7e13f refactor: cosmetic changes 2025-03-03 04:03:08 +03:00
Yurii 0ec1fc5f24 chore: fix typo 2025-03-03 02:26:09 +03:00
Yurii 061136b13e refactor: cosmetic changes (equitherm chart) 2025-03-03 02:05:33 +03:00
Yurii 3e61dabeab refactor: refactoring after #144 2025-03-02 22:44:53 +03:00
P43YM e5f4281d4c feat: new equitherm algorithm and chart for it (#144) 2025-02-28 23:21:55 +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 831 additions and 332 deletions

No files matched your search

+7 -2
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
+7
View File
@@ -27,6 +27,9 @@ let paths = {
'src_data/scripts/i18n.min.js', 'src_data/scripts/i18n.min.js',
'src_data/scripts/lang.js', 'src_data/scripts/lang.js',
'src_data/scripts/utils.js' 'src_data/scripts/utils.js'
],
'chart.js': [
'src_data/scripts/chart.js'
] ]
} }
}, },
@@ -48,6 +51,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: {
-63
View File
@@ -1,63 +0,0 @@
#include <Arduino.h>
#if defined(EQUITHERM_INTEGER)
// расчёты с целыми числами
typedef int datatype;
#else
// расчёты с float числами
typedef float datatype;
#endif
class Equitherm {
public:
datatype targetTemp = 0;
datatype indoorTemp = 0;
datatype outdoorTemp = 0;
float Kn = 0.0;
float Kk = 0.0;
float Kt = 0.0;
Equitherm() = default;
// kn, kk, kt
Equitherm(float new_kn, float new_kk, float new_kt) {
Kn = new_kn;
Kk = new_kk;
Kt = new_kt;
}
// лимит выходной величины
void setLimits(unsigned short min_output, unsigned short max_output) {
_minOut = min_output;
_maxOut = max_output;
}
// возвращает новое значение при вызове
datatype getResult() {
datatype output = getResultN() + getResultK() + getResultT();
output = constrain(output, _minOut, _maxOut); // ограничиваем выход
return output;
}
private:
unsigned short _minOut = 20, _maxOut = 90;
// температура контура отопления в зависимости от наружной температуры
datatype getResultN() {
float a = (-0.21 * Kn) - 0.06; // a = -0,21k — 0,06
float b = (6.04 * Kn) + 1.98; // b = 6,04k + 1,98
float c = (-5.06 * Kn) + 18.06; // с = -5,06k + 18,06
float x = (-0.2 * outdoorTemp) + 5; // x = -0.2*t1 + 5
return (a * x * x) + (b * x) + c; // Tn = ax2 + bx + c
}
// поправка на желаемую комнатную температуру
datatype getResultK() {
return (targetTemp - 20) * Kk;
}
// Расчет поправки (ошибки) термостата
datatype getResultT() {
return constrain((targetTemp - indoorTemp), -3, 3) * Kt;
}
};
+12 -7
View File
@@ -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/53.03.13/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
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
+49 -25
View File
@@ -840,19 +840,19 @@ public:
return this->publish(this->makeConfigTopic(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) { bool publishInputEquithermSlope(bool enabledByDefault = true) {
JsonDocument doc; JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_n")); doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_slope"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG);
doc[FPSTR(HA_NAME)] = F("Equitherm factor N"); doc[FPSTR(HA_NAME)] = F("Equitherm slope");
doc[FPSTR(HA_ICON)] = F("mdi:alpha-n-circle-outline"); doc[FPSTR(HA_ICON)] = F("mdi:slope-uphill");
doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); 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_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.slope|float(0)|round(3) }}");
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"n_factor\" : {{ value }}}}"); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"slope\" : {{ value }}}}");
doc[FPSTR(HA_MIN)] = 0.001f; doc[FPSTR(HA_MIN)] = 0.001f;
doc[FPSTR(HA_MAX)] = 10; doc[FPSTR(HA_MAX)] = 10;
doc[FPSTR(HA_STEP)] = 0.001f; doc[FPSTR(HA_STEP)] = 0.001f;
@@ -860,56 +860,80 @@ public:
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit(); doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_n_factor")).c_str(), doc); return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_slope")).c_str(), doc);
} }
bool publishInputEquithermFactorK(bool enabledByDefault = true) { bool publishInputEquithermExponent(bool enabledByDefault = true) {
JsonDocument doc; JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_k")); doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_exponent"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG);
doc[FPSTR(HA_NAME)] = F("Equitherm factor K"); doc[FPSTR(HA_NAME)] = F("Equitherm exponent");
doc[FPSTR(HA_ICON)] = F("mdi:alpha-k-circle-outline"); doc[FPSTR(HA_ICON)] = F("mdi:exponent");
doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); 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_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.exponent|float(0)|round(3) }}");
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"k_factor\" : {{ value }}}}"); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"exponent\" : {{ value }}}}");
doc[FPSTR(HA_MIN)] = 0; doc[FPSTR(HA_MIN)] = 0.1;
doc[FPSTR(HA_MAX)] = 10; doc[FPSTR(HA_MAX)] = 2;
doc[FPSTR(HA_STEP)] = 0.001f;
doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX);
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_exponent")).c_str(), doc);
}
bool publishInputEquithermShift(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][FPSTR(HA_TOPIC)] = this->statusTopic.c_str();
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_shift"));
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);
doc[FPSTR(HA_NAME)] = F("Equitherm shift");
doc[FPSTR(HA_ICON)] = F("mdi:chart-areaspline");
doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str();
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.shift|float(0)|round(2) }}");
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"shift\" : {{ value }}}}");
doc[FPSTR(HA_MIN)] = -15;
doc[FPSTR(HA_MAX)] = 15;
doc[FPSTR(HA_STEP)] = 0.01f; doc[FPSTR(HA_STEP)] = 0.01f;
doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX);
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit(); doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_k_factor")).c_str(), doc); return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_shift")).c_str(), doc);
} }
bool publishInputEquithermFactorT(bool enabledByDefault = true) { bool publishInputEquithermTargetDiffFactor(bool enabledByDefault = true) {
JsonDocument doc; JsonDocument doc;
doc[FPSTR(HA_AVAILABILITY)][0][FPSTR(HA_TOPIC)] = this->statusTopic.c_str(); 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_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)][1][FPSTR(HA_VALUE_TEMPLATE)] = F("{{ iif(value_json.pid.enabled, 'offline', 'online') }}");
doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all"); doc[FPSTR(HA_AVAILABILITY_MODE)] = F("all");
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault; doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_t")); doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectIdWithPrefix(F("equitherm_target_diff_factor"));
doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)]; doc[FPSTR(HA_OBJECT_ID)] = doc[FPSTR(HA_UNIQUE_ID)];
doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG); doc[FPSTR(HA_ENTITY_CATEGORY)] = FPSTR(HA_ENTITY_CATEGORY_CONFIG);
doc[FPSTR(HA_NAME)] = F("Equitherm factor T"); doc[FPSTR(HA_NAME)] = F("Equitherm target diff factor");
doc[FPSTR(HA_ICON)] = F("mdi:alpha-t-circle-outline"); doc[FPSTR(HA_ICON)] = F("mdi:chart-timeline-variant-shimmer");
doc[FPSTR(HA_STATE_TOPIC)] = this->settingsTopic.c_str(); 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_VALUE_TEMPLATE)] = F("{{ value_json.equitherm.targetDiffFactor|float(0)|round(3) }}");
doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str(); doc[FPSTR(HA_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"t_factor\" : {{ value }}}}"); doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"equitherm\": {\"targetDiffFactor\" : {{ value }}}}");
doc[FPSTR(HA_MIN)] = 0; doc[FPSTR(HA_MIN)] = 0;
doc[FPSTR(HA_MAX)] = 10; doc[FPSTR(HA_MAX)] = 10;
doc[FPSTR(HA_STEP)] = 0.01f; doc[FPSTR(HA_STEP)] = 0.001f;
doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX); doc[FPSTR(HA_MODE)] = FPSTR(HA_MODE_BOX);
doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter; doc[FPSTR(HA_EXPIRE_AFTER)] = this->expireAfter;
doc.shrinkToFit(); doc.shrinkToFit();
return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_t_factor")).c_str(), doc); return this->publish(this->makeConfigTopic(FPSTR(HA_ENTITY_NUMBER), F("equitherm_target_diff_factor")).c_str(), doc);
} }
@@ -1215,7 +1239,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();
+64 -36
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() {
@@ -466,9 +502,10 @@ protected:
// equitherm // equitherm
this->haHelper->publishSwitchEquitherm(); this->haHelper->publishSwitchEquitherm();
this->haHelper->publishInputEquithermFactorN(false); this->haHelper->publishInputEquithermSlope(false);
this->haHelper->publishInputEquithermFactorK(false); this->haHelper->publishInputEquithermExponent(false);
this->haHelper->publishInputEquithermFactorT(false); this->haHelper->publishInputEquithermShift(false);
this->haHelper->publishInputEquithermTargetDiffFactor(false);
// states // states
this->haHelper->publishStatusState(); this->haHelper->publishStatusState();
@@ -513,15 +550,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);
+7 -2
View File
@@ -214,7 +214,12 @@ protected:
); );
} }
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 +229,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."),
+34 -7
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();
} }
}; };
+23 -32
View File
@@ -1,7 +1,5 @@
#include <Equitherm.h>
#include <GyverPID.h> #include <GyverPID.h>
Equitherm etRegulator;
GyverPID pidRegulator(0, 0, 0); GyverPID pidRegulator(0, 0, 0);
@@ -146,39 +144,32 @@ protected:
// if use equitherm // if use equitherm
if (settings.equitherm.enabled) { if (settings.equitherm.enabled) {
unsigned short minTemp = settings.heating.minTemp; float tempDelta = settings.heating.target - vars.master.heating.outdoorTemp;
unsigned short maxTemp = settings.heating.maxTemp; float maxPoint = settings.heating.target - (
float targetTemp = settings.heating.target; settings.heating.maxTemp - settings.heating.target
float indoorTemp = vars.master.heating.indoorTemp; ) / settings.equitherm.slope;
float outdoorTemp = vars.master.heating.outdoorTemp;
if (settings.system.unitSystem == UnitSystem::IMPERIAL) { float sf = (settings.heating.maxTemp - settings.heating.target) / pow(
minTemp = f2c(minTemp); settings.heating.target - maxPoint,
maxTemp = f2c(maxTemp); 1.0f / settings.equitherm.exponent
targetTemp = f2c(targetTemp); );
indoorTemp = f2c(indoorTemp); float etResult = settings.heating.target + settings.equitherm.shift + sf * (
outdoorTemp = f2c(outdoorTemp); tempDelta >= 0
? pow(tempDelta, 1.0f / settings.equitherm.exponent)
: -(pow(-(tempDelta), 1.0f / settings.equitherm.exponent))
);
// add diff
if (this->indoorSensorsConnected && !settings.pid.enabled && !settings.heating.turbo) {
etResult += constrain(
settings.heating.target - vars.master.heating.indoorTemp,
-3.0f,
3.0f
) * settings.equitherm.targetDiffFactor;
} }
if (!this->indoorSensorsConnected || settings.pid.enabled) { // limit
etRegulator.Kt = 0.0f; etResult = constrain(etResult, settings.heating.minTemp, settings.heating.maxTemp);
etRegulator.indoorTemp = 0.0f;
} else {
etRegulator.Kt = settings.heating.turbo ? 0.0f : settings.equitherm.t_factor;
etRegulator.indoorTemp = indoorTemp;
}
etRegulator.setLimits(minTemp, maxTemp);
etRegulator.Kn = settings.equitherm.n_factor;
etRegulator.Kk = settings.equitherm.k_factor;
etRegulator.targetTemp = targetTemp;
etRegulator.outdoorTemp = outdoorTemp;
float etResult = etRegulator.getResult();
if (settings.system.unitSystem == UnitSystem::IMPERIAL) {
etResult = c2f(etResult);
}
if (fabsf(prevEtResult - etResult) > 0.09f) { if (fabsf(prevEtResult - etResult) > 0.09f) {
prevEtResult = etResult; prevEtResult = etResult;
+6 -8
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;
+5 -3
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 {
@@ -133,9 +134,10 @@ struct Settings {
struct { struct {
bool enabled = false; bool enabled = false;
float n_factor = 0.7f; float slope = 0.7f;
float k_factor = 3.0f; float exponent = 1.3f;
float t_factor = 2.0f; float shift = 0.0f;
float targetDiffFactor = 2.0f;
} equitherm; } equitherm;
struct { struct {
+2 -2
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
View File
@@ -0,0 +1,3 @@
dependencies:
idf: ">=5.3.2"
h2zero/esp-nimble-cpp: ">=2.2.1"
+12
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();
+5 -3
View File
@@ -78,6 +78,7 @@ const char S_ENABLED[] PROGMEM = "enabled";
const char S_ENV[] PROGMEM = "env"; const char S_ENV[] PROGMEM = "env";
const char S_EPC[] PROGMEM = "epc"; const char S_EPC[] PROGMEM = "epc";
const char S_EQUITHERM[] PROGMEM = "equitherm"; const char S_EQUITHERM[] PROGMEM = "equitherm";
const char S_EXPONENT[] PROGMEM = "exponent";
const char S_EXTERNAL_PUMP[] PROGMEM = "externalPump"; const char S_EXTERNAL_PUMP[] PROGMEM = "externalPump";
const char S_FACTOR[] PROGMEM = "factor"; const char S_FACTOR[] PROGMEM = "factor";
const char S_FAULT[] PROGMEM = "fault"; const char S_FAULT[] PROGMEM = "fault";
@@ -111,7 +112,6 @@ const char S_INVERT_STATE[] PROGMEM = "invertState";
const char S_IP[] PROGMEM = "ip"; const char S_IP[] PROGMEM = "ip";
const char S_I_FACTOR[] PROGMEM = "i_factor"; const char S_I_FACTOR[] PROGMEM = "i_factor";
const char S_I_MULTIPLIER[] PROGMEM = "i_multiplier"; const char S_I_MULTIPLIER[] PROGMEM = "i_multiplier";
const char S_K_FACTOR[] PROGMEM = "k_factor";
const char S_LOGIN[] PROGMEM = "login"; const char S_LOGIN[] PROGMEM = "login";
const char S_LOG_LEVEL[] PROGMEM = "logLevel"; const char S_LOG_LEVEL[] PROGMEM = "logLevel";
const char S_MAC[] PROGMEM = "mac"; const char S_MAC[] PROGMEM = "mac";
@@ -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";
@@ -136,7 +137,6 @@ const char S_NAME[] PROGMEM = "name";
const char S_NATIVE_HEATING_CONTROL[] PROGMEM = "nativeHeatingControl"; const char S_NATIVE_HEATING_CONTROL[] PROGMEM = "nativeHeatingControl";
const char S_NETWORK[] PROGMEM = "network"; const char S_NETWORK[] PROGMEM = "network";
const char S_NTP[] PROGMEM = "ntp"; const char S_NTP[] PROGMEM = "ntp";
const char S_N_FACTOR[] PROGMEM = "n_factor";
const char S_OFFSET[] PROGMEM = "offset"; const char S_OFFSET[] PROGMEM = "offset";
const char S_ON_ENABLED_HEATING[] PROGMEM = "onEnabledHeating"; const char S_ON_ENABLED_HEATING[] PROGMEM = "onEnabledHeating";
const char S_ON_FAULT[] PROGMEM = "onFault"; const char S_ON_FAULT[] PROGMEM = "onFault";
@@ -172,9 +172,11 @@ const char S_SENSORS[] PROGMEM = "sensors";
const char S_SERIAL[] PROGMEM = "serial"; const char S_SERIAL[] PROGMEM = "serial";
const char S_SERVER[] PROGMEM = "server"; const char S_SERVER[] PROGMEM = "server";
const char S_SETTINGS[] PROGMEM = "settings"; const char S_SETTINGS[] PROGMEM = "settings";
const char S_SHIFT[] PROGMEM = "shift";
const char S_SIGNAL_QUALITY[] PROGMEM = "signalQuality"; const char S_SIGNAL_QUALITY[] PROGMEM = "signalQuality";
const char S_SIZE[] PROGMEM = "size"; const char S_SIZE[] PROGMEM = "size";
const char S_SLAVE[] PROGMEM = "slave"; const char S_SLAVE[] PROGMEM = "slave";
const char S_SLOPE[] PROGMEM = "slope";
const char S_SSID[] PROGMEM = "ssid"; const char S_SSID[] PROGMEM = "ssid";
const char S_STA[] PROGMEM = "sta"; const char S_STA[] PROGMEM = "sta";
const char S_STATE[] PROGMEM = "state"; const char S_STATE[] PROGMEM = "state";
@@ -185,6 +187,7 @@ const char S_SUBNET[] PROGMEM = "subnet";
const char S_SUMMER_WINTER_MODE[] PROGMEM = "summerWinterMode"; const char S_SUMMER_WINTER_MODE[] PROGMEM = "summerWinterMode";
const char S_SYSTEM[] PROGMEM = "system"; const char S_SYSTEM[] PROGMEM = "system";
const char S_TARGET[] PROGMEM = "target"; const char S_TARGET[] PROGMEM = "target";
const char S_TARGET_DIFF_FACTOR[] PROGMEM = "targetDiffFactor";
const char S_TARGET_TEMP[] PROGMEM = "targetTemp"; const char S_TARGET_TEMP[] PROGMEM = "targetTemp";
const char S_TELNET[] PROGMEM = "telnet"; const char S_TELNET[] PROGMEM = "telnet";
const char S_TEMPERATURE[] PROGMEM = "temperature"; const char S_TEMPERATURE[] PROGMEM = "temperature";
@@ -197,7 +200,6 @@ const char S_TRESHOLD_TIME[] PROGMEM = "tresholdTime";
const char S_TURBO[] PROGMEM = "turbo"; const char S_TURBO[] PROGMEM = "turbo";
const char S_TURBO_FACTOR[] PROGMEM = "turboFactor"; const char S_TURBO_FACTOR[] PROGMEM = "turboFactor";
const char S_TYPE[] PROGMEM = "type"; const char S_TYPE[] PROGMEM = "type";
const char S_T_FACTOR[] PROGMEM = "t_factor";
const char S_UNIT_SYSTEM[] PROGMEM = "unitSystem"; const char S_UNIT_SYSTEM[] PROGMEM = "unitSystem";
const char S_UPTIME[] PROGMEM = "uptime"; const char S_UPTIME[] PROGMEM = "uptime";
const char S_USE[] PROGMEM = "use"; const char S_USE[] PROGMEM = "use";
+68 -16
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);
@@ -467,9 +500,10 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
auto equitherm = dst[FPSTR(S_EQUITHERM)].to<JsonObject>(); auto equitherm = dst[FPSTR(S_EQUITHERM)].to<JsonObject>();
equitherm[FPSTR(S_ENABLED)] = src.equitherm.enabled; equitherm[FPSTR(S_ENABLED)] = src.equitherm.enabled;
equitherm[FPSTR(S_N_FACTOR)] = roundf(src.equitherm.n_factor, 3); equitherm[FPSTR(S_SLOPE)] = roundf(src.equitherm.slope, 3);
equitherm[FPSTR(S_K_FACTOR)] = roundf(src.equitherm.k_factor, 3); equitherm[FPSTR(S_EXPONENT)] = roundf(src.equitherm.exponent, 3);
equitherm[FPSTR(S_T_FACTOR)] = roundf(src.equitherm.t_factor, 3); equitherm[FPSTR(S_SHIFT)] = roundf(src.equitherm.shift, 2);
equitherm[FPSTR(S_TARGET_DIFF_FACTOR)] = roundf(src.equitherm.targetDiffFactor, 3);
auto pid = dst[FPSTR(S_PID)].to<JsonObject>(); auto pid = dst[FPSTR(S_PID)].to<JsonObject>();
pid[FPSTR(S_ENABLED)] = src.pid.enabled; pid[FPSTR(S_ENABLED)] = src.pid.enabled;
@@ -675,6 +709,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()) {
@@ -1040,29 +1083,38 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
} }
} }
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_N_FACTOR)].isNull()) { if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_SLOPE)].isNull()) {
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_N_FACTOR)].as<float>(); float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_SLOPE)].as<float>();
if (value > 0 && value <= 10 && fabsf(value - dst.equitherm.n_factor) > 0.0001f) { if (value > 0.0f && value <= 10.0f && fabsf(value - dst.equitherm.slope) > 0.0001f) {
dst.equitherm.n_factor = roundf(value, 3); dst.equitherm.slope = roundf(value, 3);
changed = true; changed = true;
} }
} }
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_K_FACTOR)].isNull()) { if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_EXPONENT)].isNull()) {
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_K_FACTOR)].as<float>(); float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_EXPONENT)].as<float>();
if (value >= 0 && value <= 10 && fabsf(value - dst.equitherm.k_factor) > 0.0001f) { if (value > 0.0f && value <= 2.0f && fabsf(value - dst.equitherm.exponent) > 0.0001f) {
dst.equitherm.k_factor = roundf(value, 3); dst.equitherm.exponent = roundf(value, 3);
changed = true; changed = true;
} }
} }
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_T_FACTOR)].isNull()) { if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_SHIFT)].isNull()) {
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_T_FACTOR)].as<float>(); float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_SHIFT)].as<float>();
if (value >= 0 && value <= 10 && fabsf(value - dst.equitherm.t_factor) > 0.0001f) { if (value >= -15.0f && value <= 15.0f && fabsf(value - dst.equitherm.shift) > 0.0001f) {
dst.equitherm.t_factor = roundf(value, 3); dst.equitherm.shift = roundf(value, 2);
changed = true;
}
}
if (!src[FPSTR(S_EQUITHERM)][FPSTR(S_TARGET_DIFF_FACTOR)].isNull()) {
float value = src[FPSTR(S_EQUITHERM)][FPSTR(S_TARGET_DIFF_FACTOR)].as<float>();
if (value >= 0.0f && value <= 10.0f && fabsf(value - dst.equitherm.targetDiffFactor) > 0.0001f) {
dst.equitherm.targetDiffFactor = roundf(value, 3);
changed = true; changed = true;
} }
} }
@@ -1791,7 +1843,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;
} }
+20 -4
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": {
@@ -339,11 +340,26 @@
}, },
"equitherm": { "equitherm": {
"n": "N factor", "slope": {
"k": "K factor", "title": "Slope",
"t": { "note": "Heat loss compensation. Main tuning parameter."
},
"exponent": {
"title": "Exponent",
"note": "Radiator efficiency. Typical values: 1.1 - Floor heating, 1.2 - Cast iron, 1.3 - Panel radiators, 1.4 - Convectors."
},
"shift": {
"title": "Shift",
"note": "Compensates for additional heat losses (e.g., in pipes) or extra heat sources."
},
"targetDiffFactor": {
"title": "T factor", "title": "T factor",
"note": "Not used if PID is enabled" "note": "Not used if PID is enabled"
},
"chart": {
"targetTemp": "Target indoor temperature",
"setpointTemp": "Heat carrier temperature",
"outdoorTemp": "Outdoor temperature"
} }
}, },
+20 -4
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": {
@@ -339,11 +340,26 @@
}, },
"equitherm": { "equitherm": {
"n": "Fattore N", "slope": {
"k": "Fattore K", "title": "Pendenza",
"t": { "note": "Compensazione delle perdite di calore. Principale parametro di regolazione."
},
"exponent": {
"title": "Esponente",
"note": "Efficienza del radiatore. Valori tipici: 1.1 - Riscaldamento a pavimento, 1.2 - Radiatori in ghisa, 1.3 - Radiatori a pannelli, 1.4 - Convetttori."
},
"shift": {
"title": "Spostare",
"note": "Compensa le perdite di calore aggiuntive (ad esempio, nelle tubature) o fonti di calore extra."
},
"targetDiffFactor": {
"title": "Fattore T", "title": "Fattore T",
"note": "Non usato se PID è attivato" "note": "Non usato se PID è attivato"
},
"chart": {
"targetTemp": "Temperatura interna target",
"setpointTemp": "Temperatura del portatore di calore",
"outdoorTemp": "Temperatura esterna"
} }
}, },
+20 -4
View File
@@ -296,7 +296,8 @@
"portal": { "portal": {
"login": "Логин", "login": "Логин",
"password": "Пароль", "password": "Пароль",
"auth": "Требовать аутентификацию" "auth": "Требовать аутентификацию",
"mdns": "Использовать mDNS"
}, },
"system": { "system": {
@@ -339,11 +340,26 @@
}, },
"equitherm": { "equitherm": {
"n": "Коэффициент N", "slope": {
"k": "Коэффициент K", "title": "Наклон",
"t": { "note": "Компенсация теплопотерь. Основной параметр настройки."
},
"exponent": {
"title": "Экспонента",
"note": "Эффективность радиатора. Типичные значения: 1.1 - Тёплый пол, 1.2 - Чугунные радиаторы, 1.3 - Панельные радиаторы, 1.4 - Конвекторы."
},
"shift": {
"title": "Смещение",
"note": "Компенсирует дополнительные теплопотери (например, в трубах) или дополнительные источники тепла."
},
"targetDiffFactor": {
"title": "Коэффициент T", "title": "Коэффициент T",
"note": "Не используется, если ПИД включен" "note": "Не используется, если ПИД включен"
},
"chart": {
"targetTemp": "Целевая внутренняя температура",
"setpointTemp": "Температура теплоносителя",
"outdoorTemp": "Наружная температура"
} }
}, },
+91 -86
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);
+4 -2
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>
@@ -230,7 +230,9 @@
setCheckboxValue("[name='filtering']", data.filtering, sensorForm); setCheckboxValue("[name='filtering']", data.filtering, sensorForm);
setInputValue("[name='filteringFactor']", data.filteringFactor, {}, sensorForm); setInputValue("[name='filteringFactor']", data.filteringFactor, {}, sensorForm);
sensorForm.querySelector("[name='type']").dispatchEvent(new Event("change")); setTimeout(() => {
sensorForm.querySelector("[name='type']").dispatchEvent(new Event("change"));
}, 10);
setBusy(".form-busy", "form", false, sensorNode); setBusy(".form-busy", "form", false, sensorNode);
}; };
+270 -12
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>
@@ -256,7 +261,7 @@
</details> </details>
<hr /> <hr />
<details> <details>
<summary><b data-i18n>settings.section.equitherm</b></summary> <summary><b data-i18n>settings.section.equitherm</b></summary>
<div> <div>
@@ -269,21 +274,44 @@
</label> </label>
</fieldset> </fieldset>
<div>
<div>
<canvas id="etChart"></canvas>
</div>
<label>
<div>
<span data-i18n>settings.equitherm.chart.targetTemp</span>: <b class="etChartTargetTempValue"></b>°
</div>
<input class="etChartTargetTemp" type="range" value="0" min="0" max="0" step="0.5">
</label>
</div>
<div class="grid"> <div class="grid">
<label> <label>
<span data-i18n>settings.equitherm.n</span> <span data-i18n>settings.equitherm.slope.title</span>
<input type="number" inputmode="decimal" name="equitherm[n_factor]" min="0.001" max="10" step="0.001" required> <input type="number" inputmode="decimal" name="equitherm[slope]" min="0.001" max="10" step="0.001" required>
<small data-i18n>settings.equitherm.slope.note</small>
</label> </label>
<label> <label>
<span data-i18n>settings.equitherm.k</span> <span data-i18n>settings.equitherm.exponent.title</span>
<input type="number" inputmode="decimal" name="equitherm[k_factor]" min="0" max="10" step="0.01" required> <input type="number" inputmode="decimal" name="equitherm[exponent]" min="0.1" max="2" step="0.001" required>
<small data-i18n>settings.equitherm.exponent.note</small>
</label>
</div>
<div class="grid">
<label>
<span data-i18n>settings.equitherm.shift.title</span>
<input type="number" inputmode="decimal" name="equitherm[shift]" min="-15" max="15" step="0.01" required>
<small data-i18n>settings.equitherm.shift.note</small>
</label> </label>
<label> <label>
<span data-i18n>settings.equitherm.t.title</span> <span data-i18n>settings.equitherm.targetDiffFactor.title</span>
<input type="number" inputmode="decimal" name="equitherm[t_factor]" min="0" max="10" step="0.01" required> <input type="number" inputmode="decimal" name="equitherm[targetDiffFactor]" min="0" max="10" step="0.01" required>
<small data-i18n>settings.equitherm.t.note</small> <small data-i18n>settings.equitherm.targetDiffFactor.note</small>
</label> </label>
</div> </div>
@@ -293,7 +321,7 @@
</details> </details>
<hr /> <hr />
<details> <details>
<summary><b data-i18n>settings.section.pid</b></summary> <summary><b data-i18n>settings.section.pid</b></summary>
<div> <div>
@@ -751,11 +779,169 @@
</footer> </footer>
<script src="/static/app.js?{BUILD_TIME}"></script> <script src="/static/app.js?{BUILD_TIME}"></script>
<script src="/static/chart.js?{BUILD_TIME}"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
const lang = new Lang(document.getElementById('lang')); const lang = new Lang(document.getElementById('lang'));
lang.build(); lang.build();
let etChart = null;
let etChartConfig = {
slope: null,
exponent: null,
shift: null,
unitSystem: null,
targetTemp: null,
minTemp: null,
maxTemp: null
};
const hasNeedDecimationChart = () => {
return window.innerWidth <= 800;
}
const makeEquithermChart = () => {
if (etChart == null) {
const ctx = document.getElementById('etChart').getContext('2d');
try {
etChart = new Chart(ctx, {
type: 'line',
data: {
datasets: [{
borderColor: (context) => {
const chart = context.chart;
const {ctx, chartArea} = chart;
if (!chartArea) {
return;
}
const gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top);
gradient.addColorStop(0, 'rgba(1, 114, 173, 1)');
gradient.addColorStop(0.5, 'rgba(255, 99, 132, 1)');
return gradient;
},
borderWidth: 3,
fill: false,
tension: 0.1,
pointRadius: 2,
pointHoverRadius: 4,
indexAxis: "x",
data: []
}]
},
options: {
responsive: true,
resizeDelay: 500,
parsing: false,
interaction: {
mode: 'nearest',
intersect: false
},
plugins: {
tooltip: {
enabled: true,
position: 'nearest',
displayColors: false,
callbacks: {
title: (items) => {
return `${i18n("settings.equitherm.chart.outdoorTemp")}: ${items[0].label}`;
}
}
},
legend: {
display: false
},
decimation: {
enabled: hasNeedDecimationChart(),
algorithm: 'lttb',
samples: 30,
threshold: 30
}
},
scales: {
x: {
display: true,
type: "linear",
reverse: true,
title: {
display: true
},
ticks: {
stepSize: 1,
format: {
style: "unit",
unit: "degree",
unitDisplay: "narrow"
}
}
},
y: {
display: true,
title: {
display: true
},
ticks: {
format: {
style: "unit",
unit: "degree",
unitDisplay: "narrow"
}
}
}
}
}
});
} catch (error) {
console.log(error);
}
}
if (!etChart) {
return;
}
while (etChart.data.datasets[0].data.length) {
etChart.data.datasets[0].data.pop();
}
for (let value = 30; value >= -30; value--) {
const outdoorTemp = etChartConfig.unitSystem == 0 ? value : c2f(value);
etChart.data.datasets[0].data.push({
x: parseFloat(outdoorTemp.toFixed(1)),
y: parseFloat(calculateEquithermTemp(outdoorTemp).toFixed(1))
});
}
etChart.data.datasets[0].label = i18n("settings.equitherm.chart.setpointTemp");
etChart.options.scales.x.title.text = i18n("settings.equitherm.chart.outdoorTemp");
etChart.options.scales.y.title.text = i18n("settings.equitherm.chart.setpointTemp");
etChart.update();
}
const calculateEquithermTemp = (outdoorTemp) => {
const tempDelta = etChartConfig.targetTemp - outdoorTemp;
const maxPoint = etChartConfig.targetTemp - (
etChartConfig.maxTemp - etChartConfig.targetTemp
) / etChartConfig.slope;
const sf = (etChartConfig.maxTemp - etChartConfig.targetTemp) / Math.pow(
etChartConfig.targetTemp - maxPoint,
1 / etChartConfig.exponent
);
const result = etChartConfig.targetTemp + etChartConfig.shift + sf * (
tempDelta >= 0
? Math.pow(tempDelta, 1 / etChartConfig.exponent)
: -(Math.pow(-(tempDelta), 1 / etChartConfig.exponent))
);
return Math.max(Math.min(result, etChartConfig.maxTemp), etChartConfig.minTemp);
}
const fillData = (data) => { const fillData = (data) => {
// System // System
setSelectValue("[name='system[logLevel]']", data.system.logLevel); setSelectValue("[name='system[logLevel]']", data.system.logLevel);
@@ -773,6 +959,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
@@ -877,9 +1064,10 @@
// Equitherm // Equitherm
setCheckboxValue("[name='equitherm[enabled]']", data.equitherm.enabled); setCheckboxValue("[name='equitherm[enabled]']", data.equitherm.enabled);
setInputValue("[name='equitherm[n_factor]']", data.equitherm.n_factor); setInputValue("[name='equitherm[slope]']", data.equitherm.slope);
setInputValue("[name='equitherm[k_factor]']", data.equitherm.k_factor); setInputValue("[name='equitherm[exponent]']", data.equitherm.exponent);
setInputValue("[name='equitherm[t_factor]']", data.equitherm.t_factor); setInputValue("[name='equitherm[shift]']", data.equitherm.shift);
setInputValue("[name='equitherm[targetDiffFactor]']", data.equitherm.targetDiffFactor);
setBusy('#equitherm-settings-busy', '#equitherm-settings', false); setBusy('#equitherm-settings-busy', '#equitherm-settings', false);
// PID // PID
@@ -903,6 +1091,24 @@
setInputValue("[name='pid[deadband][thresholdHigh]']", data.pid.deadband.thresholdHigh); setInputValue("[name='pid[deadband][thresholdHigh]']", data.pid.deadband.thresholdHigh);
setInputValue("[name='pid[deadband][thresholdLow]']", data.pid.deadband.thresholdLow); setInputValue("[name='pid[deadband][thresholdLow]']", data.pid.deadband.thresholdLow);
setBusy('#pid-settings-busy', '#pid-settings', false); setBusy('#pid-settings-busy', '#pid-settings', false);
const etMinTemp = parseInt(data.system.unitSystem == 0 ? 5 : 41);
const etMaxTemp = parseInt(data.system.unitSystem == 0 ? 30 : 86);
const etTargetTemp = constrain(parseFloat(data.heating.target), etMinTemp, etMaxTemp);
setInputValue(".etChartTargetTemp", etTargetTemp.toFixed(1), {
"min": etMinTemp,
"max": etMaxTemp
});
etChartConfig.slope = data.equitherm.slope;
etChartConfig.exponent = data.equitherm.exponent;
etChartConfig.shift = data.equitherm.shift;
etChartConfig.unitSystem = data.system.unitSystem;
etChartConfig.minTemp = data.heating.minTemp;
etChartConfig.maxTemp = data.heating.maxTemp;
makeEquithermChart();
}; };
try { try {
@@ -955,6 +1161,58 @@
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
document.querySelector(".etChartTargetTemp").addEventListener("input", async (event) => {
setValue('.etChartTargetTempValue', parseFloat(event.target.value).toFixed(1));
});
document.querySelector(".etChartTargetTemp").addEventListener("change", async (event) => {
if (!event.target.checkValidity()) {
return;
}
etChartConfig.targetTemp = parseFloat(event.target.value);
setValue('.etChartTargetTempValue', etChartConfig.targetTemp.toFixed(1));
makeEquithermChart();
});
document.querySelector("[name='equitherm[slope]']").addEventListener("change", async (event) => {
if (!event.target.checkValidity()) {
return;
}
etChartConfig.slope = parseFloat(event.target.value);
makeEquithermChart();
});
document.querySelector("[name='equitherm[exponent]']").addEventListener("change", async (event) => {
if (!event.target.checkValidity()) {
return;
}
etChartConfig.exponent = parseFloat(event.target.value);
makeEquithermChart();
});
document.querySelector("[name='equitherm[shift]']").addEventListener("change", async (event) => {
if (!event.target.checkValidity()) {
return;
}
etChartConfig.shift = parseFloat(event.target.value);
makeEquithermChart();
});
window.addEventListener('resize', async (event) => {
if (etChart) {
etChart.resize();
if (etChart.options.plugins.decimation.enabled != hasNeedDecimationChart()) {
etChart.options.plugins.decimation.enabled = hasNeedDecimationChart();
etChart.update();
}
}
});
}); });
</script> </script>
</body> </body>
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow: /
+20
View File
@@ -0,0 +1,20 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/chart.js@4.4.8/dist/chart.umd.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
/*!
* Chart.js v4.4.8
* https://www.chartjs.org
* (c) 2025 Chart.js Contributors
* Released under the MIT License
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Go},get Decimation(){return Qo},get Filler(){return ma},get Legend(){return ya},get SubTitle(){return ka},get Title(){return Ma},get Tooltip(){return Ba}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;a<r;a++)e.call(i,t[a],a);else if(o(t))for(l=Object.keys(t),r=l.length,a=0;a<r;a++)e.call(i,t[l[a]],l[a])}function f(t,e){let i,s,n,o;if(!t||!e||t.length!==e.length)return!1;for(i=0,s=t.length;i<s;++i)if(n=t[i],o=e[i],n.datasetIndex!==o.datasetIndex||n.index!==o.index)return!1;return!0}function g(t){if(n(t))return t.map(g);if(o(t)){const e=Object.create(null),i=Object.keys(t),s=i.length;let n=0;for(;n<s;++n)e[i[n]]=g(t[i[n]]);return e}return t}function p(t){return-1===["__proto__","prototype","constructor"].indexOf(t)}function m(t,e,i,s){if(!p(t))return;const n=e[t],a=i[t];o(n)&&o(a)?x(n,a,s):e[t]=g(a)}function x(t,e,i){const s=n(e)?e:[e],a=s.length;if(!o(t))return t;const r=(i=i||{}).merger||m;let l;for(let e=0;e<a;++e){if(l=s[e],!o(l))continue;const n=Object.keys(l);for(let e=0,s=n.length;e<s;++e)r(n[e],t,l,i)}return t}function b(t,e){return x(t,e,{merger:_})}function _(t,e,i){if(!p(t))return;const s=e[t],n=i[t];o(s)&&o(n)?b(s,n):Object.prototype.hasOwnProperty.call(e,t)||(e[t]=g(n))}const y={"":t=>t,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)<i}function B(t){const e=Math.round(t);t=V(t,e,t/1e3)?e:t;const i=Math.pow(10,Math.floor(z(t))),s=t/i;return(s<=1?1:s<=2?2:s<=5?5:10)*i}function W(t){const e=[],i=Math.sqrt(t);let s;for(s=1;s<i;s++)t%s==0&&(e.push(s),e.push(t/s));return i===(0|i)&&e.push(i),e.sort(((t,e)=>t-e)).pop(),e}function N(t){return!function(t){return"symbol"==typeof t||"object"==typeof t&&null!==t&&!(Symbol.toPrimitive in t||"toString"in t||"valueOf"in t)}(t)&&!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;s<n;s++)o=t[s][i],isNaN(o)||(e.min=Math.min(e.min,o),e.max=Math.max(e.max,o))}function $(t){return t*(C/180)}function Y(t){return t*(180/C)}function U(t){if(!a(t))return;let e=1,i=0;for(;Math.round(t*e)/e!==t;)e*=10,i++;return i}function X(t,e){const i=e.x-t.x,s=e.y-t.y,n=Math.sqrt(i*i+s*s);let o=Math.atan2(s,i);return o<-.5*C&&(o+=O),{angle:o,distance:n}}function q(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function K(t,e){return(t-e+A)%O-C}function G(t){return(t%O+O)%O}function Z(t,e,i,s){const n=G(t),o=G(e),a=G(i),r=G(o-n),l=G(a-n),h=G(n-o),c=G(n-a);return n===o||n===a||s&&o===a||r>l&&h<c}function J(t,e,i){return Math.max(e,Math.min(i,t))}function Q(t){return J(t,-32768,32767)}function tt(t,e,i,s=1e-6){return t>=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]<e);let s,n=t.length-1,o=0;for(;n-o>1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return n<i||n===i&&t[s+1][e]===i}:s=>t[s][e]<i),st=(t,e,i)=>et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;s<n&&t[s]<e;)s++;for(;n>s&&t[n-1]>i;)n--;return s>0||n<t.length?t.slice(s,n):t}const ot=["push","pop","shift","splice","unshift"];function at(t,e){t._chartjs?t._chartjs.listeners.push(e):(Object.defineProperty(t,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[e]}}),ot.forEach((e=>{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...eLine truncated
/*!
* @kurkle/color v0.3.2
* https://github.com/kurkle/color#readme
* (c) 2023 Jukka Kurkela
* Released under the MIT License
*/function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e<i?6:0):e===n?(i-t)/s+2:(t-e)/s+4}(e,i,s,h,n),r=60*r+.5),[0|r,l||0,a]}function zt(t,e,i,s){return(Array.isArray(e)?t(e[0],e[1],e[2]):t(e,i,s)).map(Mt)}function Ft(t,e,i){return zt(Lt,t,e,i)}function Vt(t){return(t%360+360)%360}function Bt(t){const e=Tt.exec(t);let i,s=255;if(!e)return;e[5]!==i&&(s=e[6]?vt(+e[5]):Mt(+e[5]));const n=Vt(+e[2]),o=+e[3]/100,a=+e[4]/100;return i="hwb"===e[1]?function(t,e,i){return zt(Rt,t,e,i)}(n,o,a):"hsv"===e[1]?function(t,e,i){return zt(Et,t,e,i)}(n,o,a):Ft(n,o,a),{r:i[0],g:i[1],b:i[2],a:s}}const Wt={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Nt={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};let Ht;function jt(t){Ht||(Ht=function(){const t={},e=Object.keys(Nt),i=Object.keys(Wt);let s,n,o,a,r;for(s=0;s<e.length;s++){for(a=r=e[s],n=0;n<i.length;n++)o=i[n],r=r.replace(o,Wt[o]);o=parseInt(Nt[a],16),t[r]=[o>>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),Line truncated
//# sourceMappingURL=chart.umd.js.map
+47 -14
View File
@@ -5,8 +5,13 @@ const setupForm = (formSelector, onResultCallback = null, noCastItems = []) => {
} }
form.querySelectorAll('input').forEach(item => { form.querySelectorAll('input').forEach(item => {
item.addEventListener('change', (e) => { item.addEventListener('change', (event) => {
e.target.setAttribute('aria-invalid', !e.target.checkValidity()); if (!event.target.checkValidity()) {
event.target.setAttribute('aria-invalid', true);
} else if (event.target.hasAttribute('aria-invalid')) {
event.target.removeAttribute('aria-invalid');
}
}) })
}); });
@@ -132,10 +137,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 +153,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 +171,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 +198,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);
} }
@@ -629,6 +635,10 @@ const setCheckboxValue = (selector, value, parent = undefined) => {
} }
item.checked = value; item.checked = value;
setTimeout(() => {
item.dispatchEvent(new Event("change"));
}, 10);
} }
const setRadioValue = (selector, value, parent = undefined) => { const setRadioValue = (selector, value, parent = undefined) => {
@@ -642,7 +652,14 @@ const setRadioValue = (selector, value, parent = undefined) => {
} }
for (let item of items) { for (let item of items) {
item.checked = item.value == value; const checked = item.value == value;
if (item.checked != checked) {
item.checked = checked;
setTimeout(() => {
item.dispatchEvent(new Event("change"));
}, 10);
}
} }
} }
@@ -657,13 +674,17 @@ const setInputValue = (selector, value, attrs = {}, parent = undefined) => {
} }
for (let item of items) { for (let item of items) {
item.value = value;
if (attrs instanceof Object) { if (attrs instanceof Object) {
for (let attrKey of Object.keys(attrs)) { for (let attrKey of Object.keys(attrs)) {
item.setAttribute(attrKey, attrs[attrKey]); item.setAttribute(attrKey, attrs[attrKey]);
} }
} }
item.value = value;
setTimeout(() => {
item.dispatchEvent(new Event("change"));
}, 10);
} }
} }
@@ -848,4 +869,16 @@ function dec2hex(i) {
} }
return hex.toUpperCase(); return hex.toUpperCase();
}
function constrain(amt, low, high) {
return ((amt) < (low) ? (low) : ((amt) > (high) ? (high) : (amt)));
}
function c2f(value) {
return (9 / 5) * value + 32;
}
function f2c(value) {
return (value - 32) * (5 / 9);
} }