mirror of
https://github.com/Laxilef/OTGateway.git
synced 2025-12-26 18:13:36 +05:00
Compare commits
13 Commits
1.5.1
...
57f1129cee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57f1129cee | ||
|
|
0425cdc499 | ||
|
|
53ff69f03a | ||
|
|
e7cae4b950 | ||
|
|
3ff8f40654 | ||
|
|
d2499a2727 | ||
|
|
5b7da4ed2a | ||
|
|
8d516c7f95 | ||
|
|
d756716497 | ||
|
|
9a2f9d64ec | ||
|
|
0d0926cdac | ||
|
|
3ce3ce5016 | ||
|
|
6ca6d3cab7 |
@@ -45,9 +45,11 @@ All available information and instructions can be found in the wiki:
|
|||||||
* [Home](https://github.com/Laxilef/OTGateway/wiki)
|
* [Home](https://github.com/Laxilef/OTGateway/wiki)
|
||||||
* [Quick Start](https://github.com/Laxilef/OTGateway/wiki#quick-start)
|
* [Quick Start](https://github.com/Laxilef/OTGateway/wiki#quick-start)
|
||||||
* [Build firmware](https://github.com/Laxilef/OTGateway/wiki#build-firmware)
|
* [Build firmware](https://github.com/Laxilef/OTGateway/wiki#build-firmware)
|
||||||
* [Flash firmware via ESP Flash Download Tool](https://github.com/Laxilef/OTGateway/wiki#flash-firmware-via-esp-flash-download-tool)
|
* [Flashing via Web Flasher](https://github.com/Laxilef/OTGateway/wiki#flashing-via-web-flasher)
|
||||||
|
* [Flashing via ESP Flash Download Tool](https://github.com/Laxilef/OTGateway/wiki#flashing-via-esp-flash-download-tool)
|
||||||
* [Settings](https://github.com/Laxilef/OTGateway/wiki#settings)
|
* [Settings](https://github.com/Laxilef/OTGateway/wiki#settings)
|
||||||
* [External temperature sensors](https://github.com/Laxilef/OTGateway/wiki#external-temperature-sensors)
|
* [External temperature sensors](https://github.com/Laxilef/OTGateway/wiki#external-temperature-sensors)
|
||||||
|
* [Other external sensors](https://github.com/Laxilef/OTGateway/wiki#other-external-sensors)
|
||||||
* [Reporting indoor/outdoor temperature from any Home Assistant sensor](https://github.com/Laxilef/OTGateway/wiki#reporting-indooroutdoor-temperature-from-any-home-assistant-sensor)
|
* [Reporting indoor/outdoor temperature from any Home Assistant sensor](https://github.com/Laxilef/OTGateway/wiki#reporting-indooroutdoor-temperature-from-any-home-assistant-sensor)
|
||||||
* [Reporting outdoor temperature from Home Assistant weather integration](https://github.com/Laxilef/OTGateway/wiki#reporting-outdoor-temperature-from-home-assistant-weather-integration)
|
* [Reporting outdoor temperature from Home Assistant weather integration](https://github.com/Laxilef/OTGateway/wiki#reporting-outdoor-temperature-from-home-assistant-weather-integration)
|
||||||
* [DHW meter](https://github.com/Laxilef/OTGateway/wiki#dhw-meter)
|
* [DHW meter](https://github.com/Laxilef/OTGateway/wiki#dhw-meter)
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ public:
|
|||||||
typedef std::function<void(unsigned long, byte)> BeforeSendRequestCallback;
|
typedef std::function<void(unsigned long, byte)> BeforeSendRequestCallback;
|
||||||
typedef std::function<void(unsigned long, unsigned long, OpenThermResponseStatus, byte)> AfterSendRequestCallback;
|
typedef std::function<void(unsigned long, unsigned long, OpenThermResponseStatus, byte)> AfterSendRequestCallback;
|
||||||
|
|
||||||
CustomOpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false) : OpenTherm(inPin, outPin, isSlave) {
|
CustomOpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false) : OpenTherm(inPin, outPin, isSlave) {}
|
||||||
this->_outPin = outPin;
|
|
||||||
}
|
|
||||||
~CustomOpenTherm() {}
|
~CustomOpenTherm() {}
|
||||||
|
|
||||||
CustomOpenTherm* setDelayCallback(DelayCallback callback = nullptr) {
|
CustomOpenTherm* setDelayCallback(DelayCallback callback = nullptr) {
|
||||||
@@ -30,22 +28,6 @@ public:
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
|
||||||
if (this->status == OpenThermStatus::NOT_INITIALIZED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->end();
|
|
||||||
this->status = OpenThermStatus::NOT_INITIALIZED;
|
|
||||||
|
|
||||||
digitalWrite(this->_outPin, LOW);
|
|
||||||
if (this->delayCallback) {
|
|
||||||
this->delayCallback(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->begin();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long sendRequest(unsigned long request, byte attempts = 5, byte _attempt = 0) {
|
unsigned long sendRequest(unsigned long request, byte attempts = 5, byte _attempt = 0) {
|
||||||
_attempt++;
|
_attempt++;
|
||||||
|
|
||||||
@@ -166,5 +148,4 @@ protected:
|
|||||||
DelayCallback delayCallback;
|
DelayCallback delayCallback;
|
||||||
BeforeSendRequestCallback beforeSendRequestCallback;
|
BeforeSendRequestCallback beforeSendRequestCallback;
|
||||||
AfterSendRequestCallback afterSendRequestCallback;
|
AfterSendRequestCallback afterSendRequestCallback;
|
||||||
int _outPin;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
#include <FS.h>
|
#include <FS.h>
|
||||||
#include <detail/mimetable.h>
|
#include <detail/mimetable.h>
|
||||||
#if defined(ARDUINO_ARCH_ESP32)
|
|
||||||
#include <detail/RequestHandlersImpl.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
using namespace mime;
|
using namespace mime;
|
||||||
|
|
||||||
@@ -54,13 +51,6 @@ public:
|
|||||||
if (this->eTag.isEmpty()) {
|
if (this->eTag.isEmpty()) {
|
||||||
if (server._eTagFunction) {
|
if (server._eTagFunction) {
|
||||||
this->eTag = (server._eTagFunction)(*this->fs, this->path);
|
this->eTag = (server._eTagFunction)(*this->fs, this->path);
|
||||||
|
|
||||||
} else {
|
|
||||||
#if defined(ARDUINO_ARCH_ESP8266)
|
|
||||||
this->eTag = esp8266webserver::calcETag(*this->fs, this->path);
|
|
||||||
#elif defined(ARDUINO_ARCH_ESP32)
|
|
||||||
this->eTag = StaticRequestHandler::calcETag(*this->fs, this->path);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ lib_deps =
|
|||||||
bblanchon/ArduinoJson@^7.3.0
|
bblanchon/ArduinoJson@^7.3.0
|
||||||
;ihormelnyk/OpenTherm Library@^1.1.5
|
;ihormelnyk/OpenTherm Library@^1.1.5
|
||||||
https://github.com/ihormelnyk/opentherm_library#master
|
https://github.com/ihormelnyk/opentherm_library#master
|
||||||
;arduino-libraries/ArduinoMqttClient@^0.1.8
|
arduino-libraries/ArduinoMqttClient@^0.1.8
|
||||||
https://github.com/Laxilef/ArduinoMqttClient.git#esp32_core_310
|
|
||||||
lennarthennigs/ESP Telnet@^2.2
|
lennarthennigs/ESP Telnet@^2.2
|
||||||
gyverlibs/FileData@^1.0.2
|
gyverlibs/FileData@^1.0.2
|
||||||
gyverlibs/GyverPID@^3.3.2
|
gyverlibs/GyverPID@^3.3.2
|
||||||
@@ -85,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.10/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 =
|
||||||
@@ -300,4 +299,28 @@ 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
|
; Currently the NimBLE library is incompatible with ESP32 C6
|
||||||
;-D USE_BLE=1
|
;-D USE_BLE=1
|
||||||
|
|
||||||
|
[env:otthing]
|
||||||
|
platform = ${esp32_defaults.platform}
|
||||||
|
platform_packages = ${esp32_defaults.platform_packages}
|
||||||
|
board = lolin_c3_mini
|
||||||
|
board_build.partitions = ${esp32_defaults.board_build.partitions}
|
||||||
|
lib_deps =
|
||||||
|
${esp32_defaults.lib_deps}
|
||||||
|
${esp32_defaults.nimble_lib}
|
||||||
|
lib_ignore = ${esp32_defaults.lib_ignore}
|
||||||
|
extra_scripts = ${esp32_defaults.extra_scripts}
|
||||||
|
build_unflags =
|
||||||
|
-mtext-section-literals
|
||||||
|
build_type = ${esp32_defaults.build_type}
|
||||||
|
build_flags =
|
||||||
|
${esp32_defaults.build_flags}
|
||||||
|
-D USE_BLE=1
|
||||||
|
-D DEFAULT_OT_IN_GPIO=3
|
||||||
|
-D DEFAULT_OT_OUT_GPIO=1
|
||||||
|
; -D DEFAULT_SENSOR_OUTDOOR_GPIO=0
|
||||||
|
; -D DEFAULT_SENSOR_INDOOR_GPIO=1
|
||||||
|
-D DEFAULT_STATUS_LED_GPIO=8
|
||||||
|
-D DEFAULT_OT_RX_LED_GPIO=2
|
||||||
|
-D OT_BYPASS_RELAY_GPIO=20
|
||||||
|
|||||||
@@ -197,14 +197,13 @@ protected:
|
|||||||
|
|
||||||
this->haHelper->setDevicePrefix(settings.mqtt.prefix);
|
this->haHelper->setDevicePrefix(settings.mqtt.prefix);
|
||||||
this->haHelper->updateCachedTopics();
|
this->haHelper->updateCachedTopics();
|
||||||
|
|
||||||
this->client->stop();
|
this->client->stop();
|
||||||
this->client->setId(networkSettings.hostname);
|
this->client->setId(networkSettings.hostname);
|
||||||
this->client->setUsernamePassword(settings.mqtt.user, settings.mqtt.password);
|
this->client->setUsernamePassword(settings.mqtt.user, settings.mqtt.password);
|
||||||
|
|
||||||
this->client->beginWill(this->haHelper->getDeviceTopic(F("status")).c_str(), 7, true, 1);
|
this->client->beginWill(this->haHelper->getDeviceTopic(F("status")).c_str(), 7, true, 1);
|
||||||
this->client->print(F("offline"));
|
this->client->print(F("offline"));
|
||||||
this->client->endWill();
|
this->client->endWill();
|
||||||
|
|
||||||
this->client->connect(settings.mqtt.server, settings.mqtt.port);
|
this->client->connect(settings.mqtt.server, settings.mqtt.port);
|
||||||
this->lastReconnectTime = millis();
|
this->lastReconnectTime = millis();
|
||||||
this->yield();
|
this->yield();
|
||||||
|
|||||||
@@ -10,17 +10,21 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
const unsigned short readyTime = 60000;
|
const unsigned short readyTime = 60000u;
|
||||||
const unsigned short heatingSetTempInterval = 60000;
|
const unsigned int resetBusInterval = 120000u;
|
||||||
const unsigned short dhwSetTempInterval = 60000;
|
const unsigned short heatingSetTempInterval = 60000u;
|
||||||
const unsigned short ch2SetTempInterval = 60000;
|
const unsigned short dhwSetTempInterval = 60000u;
|
||||||
const unsigned int initializingInterval = 3600000;
|
const unsigned short ch2SetTempInterval = 60000u;
|
||||||
|
const unsigned int initializingInterval = 3600000u;
|
||||||
|
|
||||||
CustomOpenTherm* instance = nullptr;
|
CustomOpenTherm* instance = nullptr;
|
||||||
unsigned long instanceCreatedTime = 0;
|
unsigned long instanceCreatedTime = 0;
|
||||||
byte instanceInGpio = 0;
|
byte instanceInGpio = 0;
|
||||||
byte instanceOutGpio = 0;
|
byte instanceOutGpio = 0;
|
||||||
bool initialized = false;
|
bool initialized = false;
|
||||||
|
unsigned long connectedTime = 0;
|
||||||
|
unsigned long disconnectedTime = 0;
|
||||||
|
unsigned long resetBusTime = 0;
|
||||||
unsigned long initializedTime = 0;
|
unsigned long initializedTime = 0;
|
||||||
unsigned long lastSuccessResponse = 0;
|
unsigned long lastSuccessResponse = 0;
|
||||||
unsigned long prevUpdateNonEssentialVars = 0;
|
unsigned long prevUpdateNonEssentialVars = 0;
|
||||||
@@ -69,6 +73,11 @@ protected:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef OT_BYPASS_RELAY_GPIO
|
||||||
|
pinMode(OT_BYPASS_RELAY_GPIO, OUTPUT);
|
||||||
|
digitalWrite(OT_BYPASS_RELAY_GPIO, true);
|
||||||
|
#endif
|
||||||
|
|
||||||
// create instance
|
// create instance
|
||||||
this->instance = new CustomOpenTherm(settings.opentherm.inGpio, settings.opentherm.outGpio);
|
this->instance = new CustomOpenTherm(settings.opentherm.inGpio, settings.opentherm.outGpio);
|
||||||
|
|
||||||
@@ -76,6 +85,7 @@ protected:
|
|||||||
this->instanceCreatedTime = millis();
|
this->instanceCreatedTime = millis();
|
||||||
this->instanceInGpio = settings.opentherm.inGpio;
|
this->instanceInGpio = settings.opentherm.inGpio;
|
||||||
this->instanceOutGpio = settings.opentherm.outGpio;
|
this->instanceOutGpio = settings.opentherm.outGpio;
|
||||||
|
this->resetBusTime = millis();
|
||||||
this->initialized = false;
|
this->initialized = false;
|
||||||
|
|
||||||
Log.sinfoln(FPSTR(L_OT), F("Started. GPIO IN: %hhu, GPIO OUT: %hhu"), settings.opentherm.inGpio, settings.opentherm.outGpio);
|
Log.sinfoln(FPSTR(L_OT), F("Started. GPIO IN: %hhu, GPIO OUT: %hhu"), settings.opentherm.inGpio, settings.opentherm.outGpio);
|
||||||
@@ -101,8 +111,6 @@ protected:
|
|||||||
this->instance->setDelayCallback([this](unsigned int time) {
|
this->instance->setDelayCallback([this](unsigned int time) {
|
||||||
this->delay(time);
|
this->delay(time);
|
||||||
});
|
});
|
||||||
|
|
||||||
this->instance->begin();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
@@ -128,6 +136,9 @@ protected:
|
|||||||
if (this->instance == nullptr) {
|
if (this->instance == nullptr) {
|
||||||
this->delay(5000);
|
this->delay(5000);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
} else if (this->instance->status == OpenThermStatus::NOT_INITIALIZED) {
|
||||||
|
this->instance->begin();
|
||||||
}
|
}
|
||||||
|
|
||||||
// RX LED GPIO setup
|
// RX LED GPIO setup
|
||||||
@@ -204,12 +215,21 @@ protected:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!vars.slave.connected && millis() - this->lastSuccessResponse < 1325) {
|
if (!vars.slave.connected && millis() - this->lastSuccessResponse < 1325) {
|
||||||
Log.sinfoln(FPSTR(L_OT), F("Connected"));
|
Log.sinfoln(
|
||||||
|
FPSTR(L_OT),
|
||||||
|
F("Connected, downtime: %lu s."),
|
||||||
|
(millis() - this->disconnectedTime) / 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
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 > 1325) {
|
||||||
Log.swarningln(FPSTR(L_OT), F("Disconnected"));
|
Log.swarningln(
|
||||||
|
FPSTR(L_OT),
|
||||||
|
F("Disconnected, uptime: %lu s."),
|
||||||
|
(millis() - this->connectedTime) / 1000
|
||||||
|
);
|
||||||
|
|
||||||
// Mark sensors as disconnected
|
// Mark sensors as disconnected
|
||||||
Sensors::setConnectionStatusByType(Sensors::Type::OT_OUTDOOR_TEMP, false);
|
Sensors::setConnectionStatusByType(Sensors::Type::OT_OUTDOOR_TEMP, false);
|
||||||
@@ -233,6 +253,7 @@ protected:
|
|||||||
Sensors::setConnectionStatusByType(Sensors::Type::OT_FAN_SPEED_CURRENT, false);
|
Sensors::setConnectionStatusByType(Sensors::Type::OT_FAN_SPEED_CURRENT, false);
|
||||||
|
|
||||||
this->initialized = false;
|
this->initialized = false;
|
||||||
|
this->disconnectedTime = millis();
|
||||||
vars.slave.connected = false;
|
vars.slave.connected = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +269,19 @@ protected:
|
|||||||
vars.slave.diag.active = false;
|
vars.slave.diag.active = false;
|
||||||
vars.slave.diag.code = 0;
|
vars.slave.diag.code = 0;
|
||||||
|
|
||||||
this->instance->reset();
|
// reset bus
|
||||||
|
if (millis() - this->disconnectedTime > this->resetBusInterval) {
|
||||||
|
if (millis() - this->resetBusTime > this->resetBusInterval) {
|
||||||
|
Log.sinfoln(FPSTR(L_OT), F("Reset bus..."));
|
||||||
|
|
||||||
|
this->instance->end();
|
||||||
|
this->instance->status = OpenThermStatus::NOT_INITIALIZED;
|
||||||
|
|
||||||
|
digitalWrite(this->instanceOutGpio, LOW);
|
||||||
|
this->resetBusTime = millis();
|
||||||
|
this->delay(5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -602,6 +602,11 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dst.portal.auth && (!strlen(dst.portal.login) || !strlen(dst.portal.password))) {
|
||||||
|
dst.portal.auth = false;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// opentherm
|
// opentherm
|
||||||
if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_UNIT_SYSTEM)].isNull()) {
|
if (!src[FPSTR(S_OPENTHERM)][FPSTR(S_UNIT_SYSTEM)].isNull()) {
|
||||||
|
|||||||
@@ -301,7 +301,7 @@
|
|||||||
|
|
||||||
"system": {
|
"system": {
|
||||||
"unit": "Система единиц",
|
"unit": "Система единиц",
|
||||||
"metric": "Метрическая <small>(цильсии, литры, бары)</small>",
|
"metric": "Метрическая <small>(цельсии, литры, бары)</small>",
|
||||||
"imperial": "Imperial <small>(фаренгейты, галлоны, psi)</small>",
|
"imperial": "Imperial <small>(фаренгейты, галлоны, psi)</small>",
|
||||||
"statusLedGpio": "Статус LED GPIO",
|
"statusLedGpio": "Статус LED GPIO",
|
||||||
"logLevel": "Уровень логирования",
|
"logLevel": "Уровень логирования",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
</hgroup>
|
</hgroup>
|
||||||
|
|
||||||
<details id="template" class="sensor hidden" data-id="" data-preloaded="0">
|
<details id="template" class="sensor hidden" data-id="" data-preloaded="0">
|
||||||
<summary><b>#<span class="id"></span>: <span class="name"></span></b></summary>
|
<summary><b>#<span class="pos"></span>: <span class="name"></span></b></summary>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="form-busy" aria-busy="true"></div>
|
<div class="form-busy" aria-busy="true"></div>
|
||||||
@@ -210,6 +210,7 @@
|
|||||||
sensorNode.classList.remove("hidden");
|
sensorNode.classList.remove("hidden");
|
||||||
sensorNode.dataset.id = sensorId;
|
sensorNode.dataset.id = sensorId;
|
||||||
setValue(".id", sensorId, sensorNode);
|
setValue(".id", sensorId, sensorNode);
|
||||||
|
setValue(".pos", sensorId + 1, sensorNode);
|
||||||
setValue(".name", result[sensorId], sensorNode);
|
setValue(".name", result[sensorId], sensorNode);
|
||||||
|
|
||||||
container.appendChild(sensorNode);
|
container.appendChild(sensorNode);
|
||||||
|
|||||||
@@ -43,12 +43,12 @@
|
|||||||
<div class="grid">
|
<div class="grid">
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n>settings.portal.login</span>
|
<span data-i18n>settings.portal.login</span>
|
||||||
<input type="text" name="portal[login]" maxlength="12" required>
|
<input type="text" name="portal[login]" maxlength="12">
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n>settings.portal.password</span>
|
<span data-i18n>settings.portal.password</span>
|
||||||
<input type="password" name="portal[password]" maxlength="32" required>
|
<input type="password" name="portal[password]" maxlength="32">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -511,7 +511,7 @@
|
|||||||
<div class="grid">
|
<div class="grid">
|
||||||
<label>
|
<label>
|
||||||
<span data-i18n>settings.mqtt.user</span>
|
<span data-i18n>settings.mqtt.user</span>
|
||||||
<input type="text" name="mqtt[user]" maxlength="32" required>
|
<input type="text" name="mqtt[user]" maxlength="32">
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
Reference in New Issue
Block a user