5 Commits

Author SHA1 Message Date
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
16 changed files with 158 additions and 525 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

@@ -14,7 +14,7 @@ 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
@@ -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

@@ -9,30 +9,6 @@ public:
delete this->instance; delete this->instance;
} }
struct ReadResult{
bool valid = false;
bool parityValid = false;
bool responseMessageIdValid = false;
const char* responseType = "";
uint16_t value = 0;
};
ReadResult readRequest(byte messageId) {
ReadResult result;
OpenThermMessageID eMessageId = (OpenThermMessageID)messageId;
auto response = this->instance->sendRequest(CustomOpenTherm::buildRequest(
OpenThermRequestType::READ_DATA,
eMessageId,
0
));
result.valid = CustomOpenTherm::isValidResponse(response);
result.parityValid = !CustomOpenTherm::parity(response);
result.responseMessageIdValid = CustomOpenTherm::isValidResponseId(response, eMessageId);
result.responseType = CustomOpenTherm::messageTypeToString(CustomOpenTherm::getMessageType(response));
result.value = CustomOpenTherm::getUInt(response);
return result;
}
protected: protected:
const unsigned short readyTime = 60000u; const unsigned short readyTime = 60000u;
const unsigned int resetBusInterval = 120000u; const unsigned int resetBusInterval = 120000u;

View File

@@ -21,7 +21,6 @@ using namespace NetworkUtils;
extern NetworkMgr* network; extern NetworkMgr* network;
extern FileData fsNetworkSettings, fsSettings, fsSensorsSettings; extern FileData fsNetworkSettings, fsSettings, fsSensorsSettings;
extern MqttTask* tMqtt; extern MqttTask* tMqtt;
extern OpenThermTask* tOt;
class PortalTask : public LeanTask { class PortalTask : public LeanTask {
@@ -180,18 +179,6 @@ protected:
}); });
this->webServer->addHandler(upgradePage); this->webServer->addHandler(upgradePage);
// Opentherm request page
auto openthermRequestPage = (new StaticPage("/opentherm_request.html", &LittleFS, F("/pages/opentherm_request.html"), PORTAL_CACHE))
->setBeforeSendCallback([this]() {
if (this->isAuthRequired() && !this->isValidCredentials()) {
this->webServer->requestAuthentication(BASIC_AUTH);
return false;
}
return true;
});
this->webServer->addHandler(openthermRequestPage);
// OTA // OTA
auto upgradeHandler = (new UpgradeHandler("/api/upgrade"))->setCanUploadCallback([this](const String& uri) { auto upgradeHandler = (new UpgradeHandler("/api/upgrade"))->setCanUploadCallback([this](const String& uri) {
if (this->isAuthRequired() && !this->isValidCredentials()) { if (this->isAuthRequired() && !this->isValidCredentials()) {
@@ -856,41 +843,6 @@ protected:
this->bufferedWebServer->send(200, F("application/json"), doc, true); this->bufferedWebServer->send(200, F("application/json"), doc, true);
}); });
this->webServer->on(F("/api/opentherm_request/read"), HTTP_POST, [this]() {
if (this->isAuthRequired() && !this->isValidCredentials()) {
return this->webServer->send(401);
}
const String& plain = this->webServer->arg(0);
Log.straceln(FPSTR(L_PORTAL_WEBSERVER), F("Request /api/opentherm_request/read %d bytes: %s"), plain.length(), plain.c_str());
JsonDocument doc;
DeserializationError dErr = deserializeJson(doc, plain);
if (dErr != DeserializationError::Ok || doc.isNull() || !doc.size()) {
this->webServer->send(400);
return;
}
if (doc[FPSTR(S_MESSAGE_ID)].isNull() || !doc[FPSTR(S_MESSAGE_ID)].is<byte>()) {
this->webServer->send(400);
return;
}
auto messageId = doc[FPSTR(S_MESSAGE_ID)].as<byte>();
doc.clear();
doc.shrinkToFit();
auto result = tOt->readRequest(messageId);
doc[FPSTR(S_VALID)] = result.valid;
doc[FPSTR(S_PARITY_VALID)] = result.parityValid;
doc[FPSTR(S_RESPONSE_MESSAGE_ID_VALID)] = result.responseMessageIdValid;
doc[FPSTR(S_RESPONSE_TYPE)] = result.responseType;
doc[FPSTR(S_VALUE)] = result.value;
doc.shrinkToFit();
this->bufferedWebServer->send(200, F("application/json"), doc);
});
// not found // not found
this->webServer->onNotFound([this]() { this->webServer->onNotFound([this]() {

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;

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

@@ -124,7 +124,6 @@ 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_MDNS[] PROGMEM = "mdns";
const char S_MEMBER_ID[] PROGMEM = "memberId"; const char S_MEMBER_ID[] PROGMEM = "memberId";
const char S_MESSAGE_ID[] PROGMEM = "messageId";
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";
const char S_MIN_MAX_FREE_BLOCK[] PROGMEM = "minMaxFreeBlock"; const char S_MIN_MAX_FREE_BLOCK[] PROGMEM = "minMaxFreeBlock";
@@ -149,7 +148,6 @@ const char S_OUTDOOR_TEMP[] PROGMEM = "outdoorTemp";
const char S_OUT_GPIO[] PROGMEM = "outGpio"; const char S_OUT_GPIO[] PROGMEM = "outGpio";
const char S_OUTPUT[] PROGMEM = "output"; const char S_OUTPUT[] PROGMEM = "output";
const char S_PASSWORD[] PROGMEM = "password"; const char S_PASSWORD[] PROGMEM = "password";
const char S_PARITY_VALID[] PROGMEM = "parityValid";
const char S_PID[] PROGMEM = "pid"; const char S_PID[] PROGMEM = "pid";
const char S_PORT[] PROGMEM = "port"; const char S_PORT[] PROGMEM = "port";
const char S_PORTAL[] PROGMEM = "portal"; const char S_PORTAL[] PROGMEM = "portal";
@@ -166,8 +164,6 @@ const char S_RESET_DIAGNOSTIC[] PROGMEM = "resetDiagnostic";
const char S_RESET_FAULT[] PROGMEM = "resetFault"; const char S_RESET_FAULT[] PROGMEM = "resetFault";
const char S_RESET_REASON[] PROGMEM = "resetReason"; const char S_RESET_REASON[] PROGMEM = "resetReason";
const char S_RESTART[] PROGMEM = "restart"; const char S_RESTART[] PROGMEM = "restart";
const char S_RESPONSE_MESSAGE_ID_VALID[] PROGMEM = "responseMessageIdValid";
const char S_RESPONSE_TYPE[] PROGMEM = "responseType";
const char S_RETURN_TEMP[] PROGMEM = "returnTemp"; const char S_RETURN_TEMP[] PROGMEM = "returnTemp";
const char S_REV[] PROGMEM = "rev"; const char S_REV[] PROGMEM = "rev";
const char S_RSSI[] PROGMEM = "rssi"; const char S_RSSI[] PROGMEM = "rssi";
@@ -208,6 +204,5 @@ const char S_UPTIME[] PROGMEM = "uptime";
const char S_USE[] PROGMEM = "use"; const char S_USE[] PROGMEM = "use";
const char S_USE_DHCP[] PROGMEM = "useDhcp"; const char S_USE_DHCP[] PROGMEM = "useDhcp";
const char S_USER[] PROGMEM = "user"; const char S_USER[] PROGMEM = "user";
const char S_VALID[] PROGMEM = "valid";
const char S_VALUE[] PROGMEM = "value"; const char S_VALUE[] PROGMEM = "value";
const char S_VERSION[] PROGMEM = "version"; const char S_VERSION[] PROGMEM = "version";

View File

@@ -29,8 +29,7 @@
"wait": "Please wait...", "wait": "Please wait...",
"uploading": "Uploading...", "uploading": "Uploading...",
"success": "Success", "success": "Success",
"error": "Error", "error": "Error"
"send": "Send"
}, },
"index": { "index": {
@@ -469,31 +468,6 @@
"settingsFile": "Settings file", "settingsFile": "Settings file",
"fw": "Firmware", "fw": "Firmware",
"fs": "Filesystem" "fs": "Filesystem"
},
"opentherm_request": {
"title": "Custom requests - OpenTherm Gateway",
"name": "Custom requests",
"section": {
"read": "Read",
"read.desc": "Send a read request with a custom message ID"
},
"messageId": "Message ID",
"result": {
"valid": "Valid",
"parityValid": "Parity valid",
"responseMessageIdValid": "Response message ID valid",
"responseType": "Response type",
"data": "Data value",
"flags": "As flags",
"hex": "As hex",
"fixedPoint": "As f8.8",
"u8": "As two u8",
"s8": "As two s8",
"u16": "As u16",
"s16": "As s16"
}
} }
} }
} }

View File

@@ -29,8 +29,7 @@
"wait": "Attendi...", "wait": "Attendi...",
"uploading": "caricamento...", "uploading": "caricamento...",
"success": "Riuscito", "success": "Riuscito",
"error": "Errore", "error": "Errore"
"send": "Invia"
}, },
"index": { "index": {
@@ -469,30 +468,6 @@
"settingsFile": "Settings file", "settingsFile": "Settings file",
"fw": "Firmware", "fw": "Firmware",
"fs": "Filesystem" "fs": "Filesystem"
},
"opentherm_request": {
"title": "Richiesta personalizzata - OpenTherm Gateway",
"name": "Richiesta personalizzata",
"section": {
"read": "Lettura",
"read.desc": "Invia una lettura con un ID messaggio personalizzato"
},
"messageId": "ID messaggio",
"result": {
"valid": "Valido",
"parityValid": "Parità valida",
"responseMessageIdValid": "ID messaggio di risposta valido",
"responseType": "Tipo risposta",
"data": "Valore",
"flags": "Formato flags",
"hex": "Formato esadecimale",
"fixedPoint": "Formato f8.8",
"u8": "Formato due u8",
"s8": "Formato due s8",
"u16": "Formato u16",
"s16": "Formato s16"
}
} }
} }
} }

View File

@@ -29,8 +29,7 @@
"wait": "Пожалуйста, подождите...", "wait": "Пожалуйста, подождите...",
"uploading": "Загрузка...", "uploading": "Загрузка...",
"success": "Успешно", "success": "Успешно",
"error": "Ошибка", "error": "Ошибка"
"send": "Отправить"
}, },
"index": { "index": {
@@ -469,31 +468,6 @@
"settingsFile": "Файл настроек", "settingsFile": "Файл настроек",
"fw": "Прошивка", "fw": "Прошивка",
"fs": "Файловая система" "fs": "Файловая система"
},
"opentherm_request": {
"title": "Специальные запросы - OpenTherm Gateway",
"name": "Специальные запросы",
"section": {
"read": "Чтение",
"read.desc": "В этом разделе вы можете отправить запрос на чтение с произвольным message ID."
},
"messageId": "Message ID",
"result": {
"valid": "Ответ верен",
"parityValid": "Чётность верна",
"responseMessageIdValid": "Message ID ответа верно",
"responseType": "Тип ответа",
"data": "Данные",
"flags": "В виде флагов",
"hex": "В шестнадцатеричном виде",
"fixedPoint": "В виде f8.8",
"u8": "В виде двух u8",
"s8": "В виде двух s8",
"u16": "В виде u16",
"s16": "В виде s16"
}
} }
} }
} }

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

@@ -146,7 +146,6 @@
<a href="/settings.html" role="button" data-i18n>settings.name</a> <a href="/settings.html" role="button" data-i18n>settings.name</a>
<a href="/sensors.html" role="button" data-i18n>sensors.name</a> <a href="/sensors.html" role="button" data-i18n>sensors.name</a>
<a href="/upgrade.html" role="button" data-i18n>upgrade.name</a> <a href="/upgrade.html" role="button" data-i18n>upgrade.name</a>
<a href="/opentherm_request.html" role="button" data-i18n>opentherm_request.name</a>
<a href="/restart.html" role="button" class="secondary restart" data-i18n>button.restart</a> <a href="/restart.html" role="button" class="secondary restart" data-i18n>button.restart</a>
</div> </div>
</div> </div>

View File

@@ -1,132 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title data-i18n>opentherm_request.title</title>
<link rel="stylesheet" href="/static/app.css?{BUILD_TIME}">
</head>
<body>
<header class="container">
<nav>
<ul>
<li>
<a href="/">
<div class="logo" data-i18n>logo</div>
</a>
</li>
</ul>
<ul>
<li>
<select id="lang" aria-label="Lang">
<option value="en" selected>EN</option>
<option value="it">IT</option>
<option value="ru">RU</option>
</select>
</li>
</ul>
</nav>
</header>
<main class="container">
<article>
<div>
<hgroup>
<h2 data-i18n>opentherm_request.section.read</h2>
<p data-i18n>opentherm_request.section.read.desc</p>
</hgroup>
<form action="/api/opentherm_request/read" id="read">
<label for="message_id">
<span data-i18n>opentherm_request.messageId</span>
<input type="number" inputmode="numeric" name="messageId" id="message-id" min="0" max="255" step="1" required>
</label>
<div role="group">
<button type="submit" data-i18n>button.send</button>
</div>
</form>
<div role="group" id="read-result">
<table>
<tbody>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.valid</th>
<td><i class="mValid"></i></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.parityValid</th>
<td><i class="mParityValid"></i></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.responseMessageIdValid</th>
<td><i class="mResponseMessageIdValid"></i></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.responseType</th>
<td><b class="mResponseType"></b></td>
</tr>
<tr>
<th colspan="3" scope="row"><span data-i18n>opentherm_request.result.data</span>:</th>
</tr>
<tr>
<th scope="row" data-i18n>opentherm_request.result.flags</th>
<td><b class="mDataFlagsHigh"></b></td>
<td><b class="mDataFlagsLow"></b></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.hex</th>
<td><b class="mDataHex"></b></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.fixedPoint</th>
<td><b class="mDataFixedPoint"></b></td>
</tr>
<tr>
<th scope="row" data-i18n>opentherm_request.result.u8</th>
<td><b class="mDataU8High"></b></td>
<td><b class="mDataU8Low"></b></td>
</tr>
<tr>
<th scope="row" data-i18n>opentherm_request.result.s8</th>
<td><b class="mDataS8High"></b></td>
<td><b class="mDataS8Low"></b></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.u16</th>
<td><b class="mDataU16"></b></td>
</tr>
<tr>
<th colspan="2" scope="row" data-i18n>opentherm_request.result.s16</th>
<td><b class="mDataS16"></b></td>
</tr>
</tbody>
</table>
</div>
</div>
</article>
</main>
<footer class="container">
<small>
<b>Made by Laxilef</b>
<a href="https://github.com/Laxilef/OTGateway/blob/master/LICENSE" target="_blank" class="secondary" data-i18n>nav.license</a>
<a href="https://github.com/Laxilef/OTGateway/blob/master/" target="_blank" class="secondary" data-i18n>nav.source</a>
<a href="https://github.com/Laxilef/OTGateway/wiki" target="_blank" class="secondary" data-i18n>nav.help</a>
<a href="https://github.com/Laxilef/OTGateway/issues" target="_blank" class="secondary" data-i18n>nav.issues</a>
<a href="https://github.com/Laxilef/OTGateway/releases" target="_blank" class="secondary" data-i18n>nav.releases</a>
</small>
</footer>
<script src="/static/app.js?{BUILD_TIME}"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
const lang = new Lang(document.getElementById('lang'));
lang.build();
setupOTReadForm('#read');
});
</script>
</body>
</html>

View File

@@ -523,107 +523,6 @@ const setupUpgradeForm = (formSelector) => {
}); });
} }
const setupOTReadForm = (formSelector) => {
const form = document.querySelector(formSelector);
if (!form) {
return;
}
const url = form.action;
let button = form.querySelector('button[type="submit"]');
let defaultText;
hide("#read-result");
form.addEventListener('submit', async (event) => {
event.preventDefault();
if (button) {
defaultText = button.textContent;
button.textContent = i18n('button.wait');
button.setAttribute('disabled', true);
button.setAttribute('aria-busy', true);
}
hide("#read-result");
const onSuccess = (result) => {
if (button) {
button.textContent = i18n('button.success');
button.classList.add('success');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 2000);
}
setState(".mValid", result.valid);
setState(".mParityValid", result.parityValid);
setState(".m", );
setState(".mResponseMessageIdValid", result.responseMessageIdValid);
setValue(".mResponseType", result.responseType);
const u16 = result.value;
const [flagsHigh, flagsLow] = u16ToFlags(u16);
const hex = u16ToHex(u16);
const fixedPoint = u16ToFixedPoint(u16);
const [u8High, u8Low] = u16ToU8s(u16);
const [s8High, s8Low] = u16ToS8s(u16);
const s16 = u16ToS16(u16);
setValue(".mDataFlagsHigh", flagsHigh);
setValue(".mDataFlagsLow", flagsLow);
setValue(".mDataHex", hex);
setValue(".mDataFixedPoint", fixedPoint);
setValue(".mDataU8High", u8High);
setValue(".mDataU8Low", u8Low);
setValue(".mDataS8High", s8High);
setValue(".mDataS8Low", s8Low);
setValue(".mDataU16", u16);
setValue(".mDataS16", s16);
show("#read-result");
};
const onFailed = () => {
if (button) {
button.textContent = i18n('button.error');
button.classList.add('failed');
button.removeAttribute('aria-busy');
setTimeout(() => {
button.removeAttribute('disabled');
button.classList.remove('success', 'failed');
button.textContent = defaultText;
}, 5000);
}
};
const messageId = form.querySelector('#message-id').value;
try {
let fd = new FormData(form);
let response = await fetch(url, {
method: "POST",
cache: "no-cache",
credentials: "include",
body: form2json(fd)
});
if (!response.ok) {
throw new Error('Response not valid');
}
const result = await response.json();
onSuccess(result);
} catch (err) {
onFailed(false);
}
});
}
const setBusy = (busySelector, contentSelector, value, parent = undefined) => { const setBusy = (busySelector, contentSelector, value, parent = undefined) => {
if (!value) { if (!value) {
@@ -952,34 +851,6 @@ function dec2hex(i) {
return hex.toUpperCase(); return hex.toUpperCase();
} }
function u16ToHex(i) { function constrain(amt, low, high) {
return (i >>> 0).toString(16).padStart(4, "0").toUpperCase(); return ((amt) < (low) ? (low) : ((amt) > (high) ? (high) : (amt)));
}
function u16ToU8s(i) {
let low = (i >>> 0) & 0xFF;
let high = ((i >>> 0) & 0xFF00) >> 8;
return [high, low];
}
function u16ToS8s(i) {
let [high, low] = u16ToU8s(i);
return [high << 24 >> 24, low << 24 >> 24];
}
function u16ToS16(i) {
return (i >>> 0) << 16 >> 16;
}
function u16ToFlags(i) {
let [high, low] = u16ToU8s(i);
return [
high.toString(2).padStart(8, "0"),
low.toString(2).padStart(8, "0")
];
}
function u16ToFixedPoint(i) {
let [high, low] = u16ToU8s(i);
return (high + low / 256).toFixed(3)
} }