2 Commits

Author SHA1 Message Date
Roman Andriadi
2ffcda58ac Ensure messageId is a uint8_t 2025-02-03 17:30:55 +00:00
Roman Andriadi
0824066897 Add a page for sending custom OT requests 2025-02-03 17:30:55 +00:00
19 changed files with 433 additions and 197 deletions

9
.gitignore vendored
View File

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

View File

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

View File

@@ -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.13/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.11/platform-espressif32.zip
platform_packages = platform_packages =
board_build.partitions = esp32_partitions.csv board_build.partitions = esp32_partitions.csv
lib_deps = lib_deps =
@@ -287,26 +287,21 @@ 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 = ${esp32_defaults.lib_deps} lib_deps =
lib_ignore = ${esp32_defaults.lib_deps}
${esp32_defaults.lib_ignore} ;${esp32_defaults.nimble_lib}
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}
-D USE_BLE=1 ; Currently the NimBLE library is incompatible with ESP32 C6
-D DEFAULT_OT_IN_GPIO=15 ;-D USE_BLE=1
-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}

View File

@@ -1,33 +0,0 @@
# 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

@@ -70,8 +70,8 @@ public:
this->prevPubVarsTime = 0; this->prevPubVarsTime = 0;
} }
inline void reconfigureSensor(uint8_t sensorId, Sensors::Settings& prevSettings) { inline void rebuildHaEntity(uint8_t sensorId, Sensors::Settings& prevSettings) {
this->queueReconfigureSensors[sensorId] = prevSettings; this->queueRebuildingHaEntities[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> queueReconfigureSensors; std::unordered_map<uint8_t, Sensors::Settings> queueRebuildingHaEntities;
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->queueReconfigureSensors) { for (auto& [sensorId, prevSettings] : this->queueRebuildingHaEntities) {
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,6 +297,15 @@ 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);
@@ -324,51 +333,26 @@ 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;
} }
@@ -382,26 +366,6 @@ 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() {
@@ -549,6 +513,15 @@ protected:
this->haHelper->publishSignalQualityDynamicSensor(sSettings, false); this->haHelper->publishSignalQualityDynamicSensor(sSettings, false);
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem); this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::TEMPERATURE, settings.system.unitSystem);
break; break;
case Sensors::Type::MANUAL:
this->client->subscribe(
this->haHelper->getDeviceTopic(
F("sensors"),
Sensors::makeObjectId(sSettings.name).c_str(),
F("set")
).c_str()
);
default: default:
this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::PRIMARY, settings.system.unitSystem); this->haHelper->publishDynamicSensor(sSettings, Sensors::ValueType::PRIMARY, settings.system.unitSystem);

View File

@@ -9,6 +9,30 @@ 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;
@@ -214,12 +238,7 @@ protected:
); );
} }
// 5 request retries if (!vars.slave.connected && millis() - this->lastSuccessResponse < 1325) {
// 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."),
@@ -229,7 +248,7 @@ protected:
this->connectedTime = millis(); this->connectedTime = millis();
vars.slave.connected = true; vars.slave.connected = true;
} else if (vars.slave.connected && millis() - this->lastSuccessResponse > 6325) { } else if (vars.slave.connected && millis() - this->lastSuccessResponse > 1325) {
Log.swarningln( Log.swarningln(
FPSTR(L_OT), FPSTR(L_OT),
F("Disconnected, uptime: %lu s."), F("Disconnected, uptime: %lu s."),

View File

@@ -1,12 +1,10 @@
//#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
@@ -21,6 +19,7 @@ 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 {
@@ -55,7 +54,7 @@ protected:
bool webServerEnabled = false; bool webServerEnabled = false;
bool dnsServerEnabled = false; bool dnsServerEnabled = false;
unsigned long webServerChangeState = 0; unsigned long webServerChangeState = 0;
bool mDnsState = false; unsigned long dnsServerChangeState = 0;
#if defined(ARDUINO_ARCH_ESP32) #if defined(ARDUINO_ARCH_ESP32)
const char* getTaskName() override { const char* getTaskName() override {
@@ -179,6 +178,18 @@ 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()) {
@@ -618,7 +629,7 @@ protected:
} }
if (changed) { if (changed) {
tMqtt->reconfigureSensor(sensorId, prevSettings); tMqtt->rebuildHaEntity(sensorId, prevSettings);
fsSensorsSettings.update(); fsSensorsSettings.update();
} }
}); });
@@ -843,6 +854,41 @@ 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]() {
@@ -860,7 +906,6 @@ 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);
} }
@@ -875,16 +920,6 @@ 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
@@ -893,29 +928,13 @@ 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() && !network->isConnected() && network->isApEnabled() && this->stateWebServer()) { if (!this->stateDnsServer() && this->stateWebServer() && network->isApEnabled() && network->hasApClients() && millis() - this->dnsServerChangeState >= this->changeStateInterval) {
this->startDnsServer(); this->startDnsServer();
Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Started: AP up")); Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Started: AP up"));
@@ -923,9 +942,9 @@ protected:
::optimistic_yield(1000); ::optimistic_yield(1000);
#endif #endif
} else if (this->stateDnsServer() && (network->isConnected() || !network->isApEnabled() || !this->stateWebServer())) { } else if (this->stateDnsServer() && (!network->isApEnabled() || !this->stateWebServer())) {
this->stopDnsServer(); this->stopDnsServer();
Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Stopped: AP down/STA connected")); Log.straceln(FPSTR(L_PORTAL_DNSSERVER), F("Stopped: AP down"));
#ifdef ARDUINO_ARCH_ESP8266 #ifdef ARDUINO_ARCH_ESP8266
::optimistic_yield(1000); ::optimistic_yield(1000);
@@ -1035,6 +1054,7 @@ 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() {
@@ -1045,5 +1065,6 @@ protected:
//this->dnsServer->processNextRequest(); //this->dnsServer->processNextRequest();
this->dnsServer->stop(); this->dnsServer->stop();
this->dnsServerEnabled = false; this->dnsServerEnabled = false;
this->dnsServerChangeState = millis();
} }
}; };

View File

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

View File

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

View File

@@ -122,8 +122,8 @@ 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_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";
@@ -148,6 +148,7 @@ 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";
@@ -164,6 +165,8 @@ 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";
@@ -204,5 +207,6 @@ 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

@@ -440,7 +440,6 @@ 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);
@@ -708,15 +707,6 @@ 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()) {

View File

@@ -29,7 +29,8 @@
"wait": "Please wait...", "wait": "Please wait...",
"uploading": "Uploading...", "uploading": "Uploading...",
"success": "Success", "success": "Success",
"error": "Error" "error": "Error",
"send": "Send"
}, },
"index": { "index": {
@@ -296,8 +297,7 @@
"portal": { "portal": {
"login": "Login", "login": "Login",
"password": "Password", "password": "Password",
"auth": "Require authentication", "auth": "Require authentication"
"mdns": "Use mDNS"
}, },
"system": { "system": {
@@ -468,6 +468,31 @@
"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

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

View File

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

View File

@@ -146,6 +146,7 @@
<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

@@ -0,0 +1,132 @@
<!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

@@ -57,11 +57,6 @@
<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>
@@ -778,7 +773,6 @@
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

View File

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

View File

@@ -132,14 +132,10 @@ 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.dataset.ssid = result[i].hidden ? '' : result[i].ssid; row.setAttribute('data-ssid', result[i].hidden ? '' : result[i].ssid);
row.insertCell().textContent = `#${i + 1}`; row.onclick = () => {
const input = document.querySelector('input#sta-ssid');
const nameCell = row.insertCell(); const ssid = this.getAttribute('data-ssid');
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;
} }
@@ -148,6 +144,9 @@ 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();
@@ -166,7 +165,7 @@ const setupNetworkScanForm = (formSelector, tableSelector) => {
} }
let signalQualityContainer = document.createElement("span"); let signalQualityContainer = document.createElement("span");
signalQualityContainer.dataset.tooltip = `${result[i].signalQuality}%`; signalQualityContainer.setAttribute('data-tooltip', `${result[i].signalQuality}%`);
signalQualityContainer.appendChild(signalQualityIcon); signalQualityContainer.appendChild(signalQualityIcon);
infoCell.appendChild(signalQualityContainer); infoCell.appendChild(signalQualityContainer);
@@ -193,7 +192,7 @@ const setupNetworkScanForm = (formSelector, tableSelector) => {
} }
let authContainer = document.createElement("span"); let authContainer = document.createElement("span");
authContainer.dataset.tooltip = (result[i].auth in authList) ? authList[result[i].auth] : "unknown"; authContainer.setAttribute('data-tooltip', (result[i].auth in authList) ? authList[result[i].auth] : "unknown");
authContainer.appendChild(authIcon); authContainer.appendChild(authIcon);
infoCell.appendChild(authContainer); infoCell.appendChild(authContainer);
} }
@@ -523,6 +522,107 @@ 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) {
@@ -849,4 +949,36 @@ function dec2hex(i) {
} }
return hex.toUpperCase(); return hex.toUpperCase();
}
function u16ToHex(i) {
return (i >>> 0).toString(16).padStart(4, "0").toUpperCase();
}
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)
} }