23 Commits

Author SHA1 Message Date
github-actions[bot]
bef31da3d5 Bump NimBLE-Arduino to 2.1.0 2024-12-15 00:35:16 +00:00
Yurii
bae7770371 chore: update pcb 2024-12-07 23:47:21 +03:00
Yurii
4e5a3e9da5 refactor: little changes 2024-12-07 23:10:50 +03:00
Yurii
412e1594e9 chore: update readme & assets 2024-12-07 23:10:27 +03:00
Yurii
9701e8c97b chore: update readme 2024-12-07 22:59:29 +03:00
Yurii
2fe546812c chore: update readme 2024-12-07 22:47:24 +03:00
Yurii
7efcbaa57e chore: bump version to 1.5.0 2024-12-05 00:04:58 +03:00
Yurii
43c065b97a Merge branch 'master' into 1.5.0-dev 2024-12-04 07:56:19 +03:00
Yurii
5c1e967fdc refactor: improved display of sensors on Dashboard 2024-12-04 06:09:20 +03:00
Yurii
105a79f72c refactor: optimization of OT requests for CH2 2024-12-04 05:55:01 +03:00
Yurii
50280f6db3 fix: negative temperature values from BLE devices 2024-12-04 05:36:04 +03:00
Yurii
c97e50669c fix: typo in purpose of sensors for emergency mode 2024-12-04 05:35:19 +03:00
Yurii
43fd095714 fix: digest auth changed to basic
Digest auth not working on ios #99
2024-12-02 06:26:03 +03:00
Yurii
1bb9b61017 feat: display sensor values on dashboard page 2024-12-01 05:26:14 +03:00
Yurii
1eb10563ed chore: update readme 2024-11-30 23:58:54 +03:00
Yurii
222ea4feaa refactor: some changes 2024-11-29 20:58:52 +03:00
Yurii
ea1406c509 fix: action state for HA `climate` entities fixed 2024-11-20 23:11:30 +03:00
Yurii
b20b450736 fix: fixed ERR_CONNECTION_RESET for Chrome based browsers 2024-11-20 23:10:42 +03:00
Yurii
e8c7f58e67 fix: fixed power calculation 2024-11-18 19:41:12 +03:00
Yurii
2589020428 refactor: optimized polling of unused OT IDs 2024-11-16 22:57:52 +03:00
Yurii
2bb771a4a7 chore: bump pioarduino/platform-espressif32 from 3.1.0 rc2 to 3.1.0 rc3 2024-11-16 22:56:01 +03:00
Yurii
f4af237472 chore: bump version to 1.4.6 2024-11-16 14:19:40 +03:00
Yurii
5473034421 chore: deleted .ino file 2024-11-16 14:15:26 +03:00
37 changed files with 15996 additions and 7343 deletions

View File

@@ -1,40 +1,44 @@
<div align="center">
![logo](/assets/logo.svg)
<br>
[![GitHub version](https://img.shields.io/github/release/Laxilef/OTGateway.svg?include_prereleases)](https://github.com/Laxilef/OTGateway/releases)
[![GitHub download](https://img.shields.io/github/downloads/Laxilef/OTGateway/total.svg)](https://github.com/Laxilef/OTGateway/releases/latest)
[![License](https://img.shields.io/github/license/Laxilef/OTGateway.svg)](LICENSE.txt)
[![Telegram](https://img.shields.io/badge/Telegram-Channel-33A8E3)](https://t.me/otgateway)
</div>
<hr />
![Dashboard](/assets/poster-1.png)
![Configuration](/assets/poster-2.png)
![Integration with HomeAssistant](/assets/poster-3.png)
## Features
- Hot water temperature control
- DHW temperature control
- Heating temperature control
- Smart heating temperature control modes:
- PID
- Equithermic curves - adjusts the temperature based on indoor and outdoor temperatures
- Hysteresis setting (for accurate maintenance of room temperature)
- Ability to connect an external sensors to monitor outdoor and indoor temperature ([compatible sensors](https://github.com/Laxilef/OTGateway/wiki/Compatibility#temperature-sensors))
- Emergency mode. If the Wi-Fi connection is lost or the gateway cannot connect to the MQTT server, the mode will turn on. This mode will automatically maintain the set temperature and prevent your home from freezing. In this mode it is also possible to use equithermal curves (weather-compensated control).
- Automatic error reset (not with all boilers)
- Hysteresis setting _(for accurate maintenance of room temperature)_
- Ability to connect [additional (external) sensors](https://github.com/Laxilef/OTGateway/wiki/Compatibility#temperature-sensors): Dallas (1-wire), NTC 10k, Bluetooth (BLE). Makes it possible to monitor indoor and outdoor temperatures, temperatures on pipes/heat exchangers/etc.
- Emergency mode. In any dangerous situation _(loss of connection with Wifi, MQTT, sensors, etc)_ it will not let you and your home freeze.
- Ability of remote fault reset _(not with all boilers)_
- Diagnostics:
- The process of heating: works/does not work
- The process of heating water for hot water: working/not working
- Display of boiler errors
- Burner status (flame): on/off
- Burner modulation level in percent
- Pressure in the heating system
- Gateway status (depending on errors and connection status)
- Boiler connection status via OpenTherm interface
- The current temperature of the heat carrier (usually the return heat carrier)
- Set heat carrier temperature (depending on the selected mode)
- Current hot water temperature
- Displaying gateway status
- Displaying the connection status to the boiler via OpenTherm
- Displaying the fault status and fault code
- Displaying the diagnostic status & diagnostic code
- Display of the process of heating: works/does not work
- Display of burner (flame) status: on/off
- Display of burner modulation level in percent
- Display of pressure in the heating system
- Display of current temperature of the heat carrier
- Display of return temperature of the heat carrier
- Display of setpoint heat carrier temperature (useful when using PID or Equitherm)
- Display of the process of DHW: working/not working
- Display of current DHW temperature
- _And other information..._
- [Home Assistant](https://www.home-assistant.io/) integration via MQTT. The ability to create any automation for the boiler!
![logo](/assets/ha.png)
## Documentation
All available information and instructions can be found in the wiki:
@@ -52,6 +56,7 @@ All available information and instructions can be found in the wiki:
* [Ratios](https://github.com/Laxilef/OTGateway/wiki#ratios)
* [Fit coefficients](https://github.com/Laxilef/OTGateway/wiki#fit-coefficients)
* [PID mode](https://github.com/Laxilef/OTGateway/wiki#pid-mode)
* [Logs and debug](https://github.com/Laxilef/OTGateway/wiki#logs-and-debug)
* [Compatibility](https://github.com/Laxilef/OTGateway/wiki/Compatibility)
* [Boilers](https://github.com/Laxilef/OTGateway/wiki/Compatibility#boilers)
* [Boards](https://github.com/Laxilef/OTGateway/wiki/Compatibility#boards)
@@ -64,21 +69,5 @@ All available information and instructions can be found in the wiki:
* [Connection](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#connection)
* [Leds on board](https://github.com/Laxilef/OTGateway/wiki/OT-adapters#leds-on-board)
## Dependencies
- [ESP8266Scheduler](https://github.com/nrwiersma/ESP8266Scheduler) (for ESP8266)
- [ESP32Scheduler](https://github.com/laxilef/ESP32Scheduler) (for ESP32)
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson)
- [OpenTherm Library](https://github.com/ihormelnyk/opentherm_library)
- [ArduinoMqttClient](https://github.com/arduino-libraries/ArduinoMqttClient)
- [ESPTelnet](https://github.com/LennartHennigs/ESPTelnet)
- [FileData](https://github.com/GyverLibs/FileData)
- [GyverPID](https://github.com/GyverLibs/GyverPID)
- [GyverBlinker](https://github.com/GyverLibs/GyverBlinker)
- [DallasTemperature](https://github.com/milesburton/Arduino-Temperature-Control-Library)
- [TinyLogger](https://github.com/laxilef/TinyLogger)
## Debug
To display DEBUG messages you must enable debug in settings (switch is disabled by default).
You can connect via Telnet to read messages. IP: ESP8266 ip, port: 23
___
This project is tested with BrowserStack.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 KiB

After

Width:  |  Height:  |  Size: 849 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

BIN
assets/poster-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
assets/poster-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
assets/poster-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 KiB

View File

@@ -1,4 +0,0 @@
/*
This file is needed by the Arduino IDE because the ino file needs to be named as the directory name.
Don't worry, the Arduino compiler will "merge" all files, including src/main.cpp
*/

View File

@@ -14,7 +14,7 @@ extra_configs = secrets.default.ini
core_dir = .pio
[env]
version = 1.5.0-alpha
version = 1.5.0
framework = arduino
lib_deps =
bblanchon/ArduinoJson@^7.1.0
@@ -85,13 +85,13 @@ board_build.ldscript = eagle.flash.4m1m.ld
;platform_packages =
; 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
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc2/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.10-rc3/platform-espressif32.zip
platform_packages =
board_build.partitions = esp32_partitions.csv
lib_deps =
${env.lib_deps}
laxilef/ESP32Scheduler@^1.0.1
nimble_lib = h2zero/NimBLE-Arduino@^1.4.2
nimble_lib = h2zero/NimBLE-Arduino@^2.1.0
lib_ignore =
extra_scripts =
post:tools/esp32.py

View File

@@ -1204,7 +1204,7 @@ public:
doc[FPSTR(HA_MODES)][1] = F("heat");
doc[FPSTR(HA_ACTION_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_ACTION_TEMPLATE)] = F("{{ iif(value_json.master.heating.enabled, iif(value_json.slave.heating.active, 'heating', 'idle'), 'off') }}");
doc[FPSTR(HA_ACTION_TEMPLATE)] = F("{{ iif(value_json.slave.heating.active, 'heating', 'idle') }}");
doc[FPSTR(HA_PRESET_MODE_COMMAND_TOPIC)] = this->setSettingsTopic.c_str();
doc[FPSTR(HA_PRESET_MODE_COMMAND_TEMPLATE)] = F("{% if value == 'boost' %}{\"heating\": {\"turbo\" : true}}"
@@ -1256,7 +1256,7 @@ public:
doc[FPSTR(HA_MODES)][1] = F("heat");
doc[FPSTR(HA_ACTION_TOPIC)] = this->stateTopic.c_str();
doc[FPSTR(HA_ACTION_TEMPLATE)] = F("{{ iif(value_json.master.dhw.enabled, iif(value_json.slave.dhw.active, 'heating', 'idle'), 'off') }}");
doc[FPSTR(HA_ACTION_TEMPLATE)] = F("{{ iif(value_json.slave.dhw.active, 'heating', 'idle') }}");
doc[FPSTR(HA_MIN_TEMP)] = minTemp;
doc[FPSTR(HA_MAX_TEMP)] = maxTemp;

View File

@@ -218,13 +218,13 @@ protected:
// set outdoor sensor flag
if (settings.equitherm.enabled) {
if (!Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::INDOOR_TEMP)) {
if (!Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::OUTDOOR_TEMP)) {
emergencyFlags |= 0b00000001;
}
}
// set indoor sensor flags
if (!Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::OUTDOOR_TEMP)) {
if (!Sensors::existsConnectedSensorsByPurpose(Sensors::Purpose::INDOOR_TEMP)) {
if (!settings.equitherm.enabled && settings.pid.enabled) {
emergencyFlags |= 0b00000010;
}

View File

@@ -462,20 +462,27 @@ protected:
// Update modulation level
if (
Sensors::getAmountByType(Sensors::Type::OT_MODULATION_LEVEL) ||
Sensors::getAmountByType(Sensors::Type::OT_CURRENT_POWER)
Sensors::getAmountByType(Sensors::Type::OT_MODULATION_LEVEL, true) ||
Sensors::getAmountByType(Sensors::Type::OT_CURRENT_POWER, true)
) {
if (vars.slave.flame) {
if (this->updateModulationLevel()) {
float power = 0.0f;
if (vars.slave.modulation.current > 0 && settings.opentherm.maxPower > 0.1f) {
float modulatedPower = settings.opentherm.maxPower - settings.opentherm.minPower;
power = settings.opentherm.minPower + (modulatedPower / 100.0f * vars.slave.modulation.current);
if (settings.opentherm.maxPower > 0.1f) {
power += settings.opentherm.minPower;
if (vars.slave.modulation.current > 0) {
power += (
settings.opentherm.maxPower - settings.opentherm.minPower
) / 100.0f * vars.slave.modulation.current;
}
}
vars.slave.power.current = power;
Log.snoticeln(
FPSTR(L_OT), F("Received modulation level: %hhu%%, power: %.2f of %.2f kW (min: %.2f kW)"),
vars.slave.modulation.current, power, settings.opentherm.maxPower, settings.opentherm.minPower
vars.slave.modulation.current, vars.slave.power.current,
settings.opentherm.maxPower, settings.opentherm.minPower
);
// Modulation level sensors
@@ -486,7 +493,7 @@ protected:
// Power sensors
Sensors::setValueByType(
Sensors::Type::OT_CURRENT_POWER, power,
Sensors::Type::OT_CURRENT_POWER, vars.slave.power.current,
Sensors::ValueType::PRIMARY, true, true
);
@@ -496,23 +503,24 @@ protected:
} else {
vars.slave.modulation.current = 0;
vars.slave.power.current = 0.0f;
// Modulation level sensors
Sensors::setValueByType(
Sensors::Type::OT_MODULATION_LEVEL, 0.0f,
Sensors::Type::OT_MODULATION_LEVEL, vars.slave.modulation.current,
Sensors::ValueType::PRIMARY, true, true
);
// Power sensors
Sensors::setValueByType(
Sensors::Type::OT_CURRENT_POWER, 0.0f,
Sensors::Type::OT_CURRENT_POWER, vars.slave.power.current,
Sensors::ValueType::PRIMARY, true, true
);
}
}
// Update DHW temp
if (settings.opentherm.dhwPresent && Sensors::getAmountByType(Sensors::Type::OT_DHW_TEMP)) {
if (settings.opentherm.dhwPresent && Sensors::getAmountByType(Sensors::Type::OT_DHW_TEMP, true)) {
bool result = this->updateDhwTemp();
if (result) {
@@ -538,7 +546,7 @@ protected:
}
// Update DHW temp 2
if (settings.opentherm.dhwPresent && Sensors::getAmountByType(Sensors::Type::OT_DHW_TEMP2)) {
if (settings.opentherm.dhwPresent && Sensors::getAmountByType(Sensors::Type::OT_DHW_TEMP2, true)) {
if (this->updateDhwTemp2()) {
float convertedDhwTemp2 = convertTemp(
vars.slave.dhw.currentTemp2,
@@ -562,7 +570,7 @@ protected:
}
// Update DHW flow rate
if (settings.opentherm.dhwPresent && Sensors::getAmountByType(Sensors::Type::OT_DHW_FLOW_RATE)) {
if (settings.opentherm.dhwPresent && Sensors::getAmountByType(Sensors::Type::OT_DHW_FLOW_RATE, true)) {
if (this->updateDhwFlowRate()) {
float convertedDhwFlowRate = convertVolume(
vars.slave.dhw.flowRate,
@@ -586,7 +594,7 @@ protected:
}
// Update heating temp
if (Sensors::getAmountByType(Sensors::Type::OT_HEATING_TEMP)) {
if (Sensors::getAmountByType(Sensors::Type::OT_HEATING_TEMP, true)) {
if (this->updateHeatingTemp()) {
float convertedHeatingTemp = convertTemp(
vars.slave.heating.currentTemp,
@@ -610,7 +618,7 @@ protected:
}
// Update heating return temp
if (Sensors::getAmountByType(Sensors::Type::OT_HEATING_RETURN_TEMP)) {
if (Sensors::getAmountByType(Sensors::Type::OT_HEATING_RETURN_TEMP, true)) {
if (this->updateHeatingReturnTemp()) {
float convertedHeatingReturnTemp = convertTemp(
vars.slave.heating.returnTemp,
@@ -634,7 +642,7 @@ protected:
}
// Update CH2 temp
if (Sensors::getAmountByType(Sensors::Type::OT_CH2_TEMP)) {
if (Sensors::getAmountByType(Sensors::Type::OT_CH2_TEMP, true)) {
if (vars.master.ch2.enabled && !settings.opentherm.nativeHeatingControl) {
if (this->updateCh2Temp()) {
float convertedCh2Temp = convertTemp(
@@ -660,7 +668,7 @@ protected:
}
// Update exhaust temp
if (Sensors::getAmountByType(Sensors::Type::OT_EXHAUST_TEMP)) {
if (Sensors::getAmountByType(Sensors::Type::OT_EXHAUST_TEMP, true)) {
if (this->updateExhaustTemp()) {
float convertedExhaustTemp = convertTemp(
vars.slave.exhaust.temp,
@@ -684,7 +692,7 @@ protected:
}
// Update heat exchanger temp
if (Sensors::getAmountByType(Sensors::Type::OT_HEAT_EXCHANGER_TEMP)) {
if (Sensors::getAmountByType(Sensors::Type::OT_HEAT_EXCHANGER_TEMP, true)) {
if (this->updateHeatExchangerTemp()) {
float convertedHeatExchTemp = convertTemp(
vars.slave.heatExchangerTemp,
@@ -708,7 +716,7 @@ protected:
}
// Update outdoor temp
if (Sensors::getAmountByType(Sensors::Type::OT_OUTDOOR_TEMP)) {
if (Sensors::getAmountByType(Sensors::Type::OT_OUTDOOR_TEMP, true)) {
if (this->updateOutdoorTemp()) {
float convertedOutdoorTemp = convertTemp(
vars.slave.heating.outdoorTemp,
@@ -732,7 +740,7 @@ protected:
}
// Update solar storage temp
if (Sensors::getAmountByType(Sensors::Type::OT_SOLAR_STORAGE_TEMP)) {
if (Sensors::getAmountByType(Sensors::Type::OT_SOLAR_STORAGE_TEMP, true)) {
if (this->updateSolarStorageTemp()) {
float convertedSolarStorageTemp = convertTemp(
vars.slave.solar.storage,
@@ -756,7 +764,7 @@ protected:
}
// Update solar collector temp
if (Sensors::getAmountByType(Sensors::Type::OT_SOLAR_COLLECTOR_TEMP)) {
if (Sensors::getAmountByType(Sensors::Type::OT_SOLAR_COLLECTOR_TEMP, true)) {
if (this->updateSolarCollectorTemp()) {
float convertedSolarCollectorTemp = convertTemp(
vars.slave.solar.collector,
@@ -781,8 +789,8 @@ protected:
// Update fan speed
if (
Sensors::getAmountByType(Sensors::Type::OT_FAN_SPEED_SETPOINT) ||
Sensors::getAmountByType(Sensors::Type::OT_FAN_SPEED_CURRENT)
Sensors::getAmountByType(Sensors::Type::OT_FAN_SPEED_SETPOINT, true) ||
Sensors::getAmountByType(Sensors::Type::OT_FAN_SPEED_CURRENT, true)
) {
if (this->updateFanSpeed()) {
Log.snoticeln(
@@ -802,7 +810,7 @@ protected:
}
// Update pressure
if (Sensors::getAmountByType(Sensors::Type::OT_PRESSURE)) {
if (Sensors::getAmountByType(Sensors::Type::OT_PRESSURE, true)) {
if (this->updatePressure()) {
float convertedPressure = convertPressure(
vars.slave.pressure,
@@ -826,7 +834,7 @@ protected:
}
// Update exhaust CO2
if (Sensors::getAmountByType(Sensors::Type::OT_EXHAUST_CO2)) {
if (Sensors::getAmountByType(Sensors::Type::OT_EXHAUST_CO2, true)) {
if (this->updateExhaustCo2()) {
Log.snoticeln(
FPSTR(L_OT), F("Received exhaust CO2: %hu ppm"),
@@ -844,7 +852,7 @@ protected:
}
// Update exhaust fan speed
if (Sensors::getAmountByType(Sensors::Type::OT_EXHAUST_FAN_SPEED)) {
if (Sensors::getAmountByType(Sensors::Type::OT_EXHAUST_FAN_SPEED, true)) {
if (this->updateExhaustFanSpeed()) {
Log.snoticeln(
FPSTR(L_OT), F("Received exhaust fan speed: %hu rpm"),
@@ -862,7 +870,7 @@ protected:
}
// Update supply fan speed
if (Sensors::getAmountByType(Sensors::Type::OT_SUPPLY_FAN_SPEED)) {
if (Sensors::getAmountByType(Sensors::Type::OT_SUPPLY_FAN_SPEED, true)) {
if (this->updateSupplyFanSpeed()) {
Log.snoticeln(
FPSTR(L_OT), F("Received supply fan speed: %hu rpm"),
@@ -1030,25 +1038,27 @@ protected:
}
// Set CH2 temp
if (vars.master.ch2.enabled && !settings.opentherm.nativeHeatingControl) {
// Converted target CH2 temp
float convertedTemp = convertTemp(
vars.master.ch2.targetTemp,
settings.system.unitSystem,
settings.opentherm.unitSystem
);
if (!settings.opentherm.nativeHeatingControl && vars.master.ch2.enabled) {
if (settings.opentherm.heatingCh1ToCh2 || settings.opentherm.dhwToCh2) {
// Converted target CH2 temp
float convertedTemp = convertTemp(
vars.master.ch2.targetTemp,
settings.system.unitSystem,
settings.opentherm.unitSystem
);
if (this->needSetCh2Temp(convertedTemp)) {
if (this->setCh2Temp(convertedTemp)) {
this->ch2SetTempTime = millis();
if (this->needSetCh2Temp(convertedTemp)) {
if (this->setCh2Temp(convertedTemp)) {
this->ch2SetTempTime = millis();
Log.sinfoln(
FPSTR(L_OT_CH2), F("Set temp: %.2f (converted: %.2f, response: %.2f)"),
vars.master.ch2.targetTemp, convertedTemp, vars.slave.ch2.targetTemp
);
Log.sinfoln(
FPSTR(L_OT_CH2), F("Set temp: %.2f (converted: %.2f, response: %.2f)"),
vars.master.ch2.targetTemp, convertedTemp, vars.slave.ch2.targetTemp
);
} else {
Log.swarningln(FPSTR(L_OT_CH2), F("Failed set temp"));
} else {
Log.swarningln(FPSTR(L_OT_CH2), F("Failed set temp"));
}
}
}
}

View File

@@ -108,8 +108,8 @@ protected:
// dashboard page
auto dashboardPage = (new StaticPage("/dashboard.html", &LittleFS, F("/pages/dashboard.html"), PORTAL_CACHE))
->setBeforeSendCallback([this]() {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
if (this->isAuthRequired() && !this->isValidCredentials()) {
this->webServer->requestAuthentication(BASIC_AUTH);
return false;
}
@@ -119,11 +119,9 @@ protected:
// restart
this->webServer->on(F("/restart.html"), HTTP_GET, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->send(401);
return;
}
if (this->isAuthRequired() && !this->isValidCredentials()) {
this->webServer->requestAuthentication(BASIC_AUTH);
return;
}
vars.actions.restart = true;
@@ -134,8 +132,8 @@ protected:
// network settings page
auto networkPage = (new StaticPage("/network.html", &LittleFS, F("/pages/network.html"), PORTAL_CACHE))
->setBeforeSendCallback([this]() {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
if (this->isAuthRequired() && !this->isValidCredentials()) {
this->webServer->requestAuthentication(BASIC_AUTH);
return false;
}
@@ -146,8 +144,8 @@ protected:
// settings page
auto settingsPage = (new StaticPage("/settings.html", &LittleFS, F("/pages/settings.html"), PORTAL_CACHE))
->setBeforeSendCallback([this]() {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
if (this->isAuthRequired() && !this->isValidCredentials()) {
this->webServer->requestAuthentication(BASIC_AUTH);
return false;
}
@@ -158,8 +156,8 @@ protected:
// sensors page
auto sensorsPage = (new StaticPage("/sensors.html", &LittleFS, F("/pages/sensors.html"), PORTAL_CACHE))
->setBeforeSendCallback([this]() {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
if (this->isAuthRequired() && !this->isValidCredentials()) {
this->webServer->requestAuthentication(BASIC_AUTH);
return false;
}
@@ -170,8 +168,8 @@ protected:
// upgrade page
auto upgradePage = (new StaticPage("/upgrade.html", &LittleFS, F("/pages/upgrade.html"), PORTAL_CACHE))
->setBeforeSendCallback([this]() {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->requestAuthentication(DIGEST_AUTH);
if (this->isAuthRequired() && !this->isValidCredentials()) {
this->webServer->requestAuthentication(BASIC_AUTH);
return false;
}
@@ -181,7 +179,7 @@ protected:
// OTA
auto upgradeHandler = (new UpgradeHandler("/api/upgrade"))->setCanUploadCallback([this](const String& uri) {
if (this->isAuthRequired() && !this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
if (this->isAuthRequired() && !this->isValidCredentials()) {
this->webServer->sendHeader(F("Connection"), F("close"));
this->webServer->send(401);
return false;
@@ -222,10 +220,8 @@ protected:
// backup
this->webServer->on(F("/api/backup/save"), HTTP_GET, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
if (this->isAuthRequired() && !this->isValidCredentials()) {
return this->webServer->send(401);
}
JsonDocument doc;
@@ -248,10 +244,8 @@ protected:
});
this->webServer->on(F("/api/backup/restore"), HTTP_POST, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
if (this->isAuthRequired() && !this->isValidCredentials()) {
return this->webServer->send(401);
}
if (vars.states.restarting) {
@@ -329,10 +323,8 @@ protected:
// network
this->webServer->on(F("/api/network/settings"), HTTP_GET, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
if (this->isAuthRequired() && !this->isValidCredentials()) {
return this->webServer->send(401);
}
JsonDocument doc;
@@ -343,10 +335,8 @@ protected:
});
this->webServer->on(F("/api/network/settings"), HTTP_POST, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
if (this->isAuthRequired() && !this->isValidCredentials()) {
return this->webServer->send(401);
}
if (vars.states.restarting) {
@@ -402,11 +392,8 @@ protected:
});
this->webServer->on(F("/api/network/scan"), HTTP_GET, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
this->webServer->send(401);
return;
}
if (this->isAuthRequired() && !this->isValidCredentials()) {
return this->webServer->send(401);
}
auto apCount = WiFi.scanComplete();
@@ -448,10 +435,8 @@ protected:
// settings
this->webServer->on(F("/api/settings"), HTTP_GET, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
if (this->isAuthRequired() && !this->isValidCredentials()) {
return this->webServer->send(401);
}
JsonDocument doc;
@@ -462,10 +447,8 @@ protected:
});
this->webServer->on(F("/api/settings"), HTTP_POST, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
if (this->isAuthRequired() && !this->isValidCredentials()) {
return this->webServer->send(401);
}
if (vars.states.restarting) {
@@ -513,10 +496,8 @@ protected:
// sensors list
this->webServer->on(F("/api/sensors"), HTTP_GET, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
if (this->isAuthRequired() && !this->isValidCredentials()) {
return this->webServer->send(401);
}
bool detailed = false;
@@ -528,6 +509,7 @@ protected:
for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) {
if (detailed) {
auto& sSensor = Sensors::settings[sensorId];
doc[sensorId][FPSTR(S_ENABLED)] = sSensor.enabled;
doc[sensorId][FPSTR(S_NAME)] = sSensor.name;
doc[sensorId][FPSTR(S_PURPOSE)] = static_cast<uint8_t>(sSensor.purpose);
sensorResultToJson(sensorId, doc[sensorId]);
@@ -543,10 +525,8 @@ protected:
// sensor settings
this->webServer->on(F("/api/sensor"), HTTP_GET, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
if (this->isAuthRequired() && !this->isValidCredentials()) {
return this->webServer->send(401);
}
if (!this->webServer->hasArg(F("id"))) {
@@ -571,10 +551,8 @@ protected:
});
this->webServer->on(F("/api/sensor"), HTTP_POST, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
if (this->isAuthRequired() && !this->isValidCredentials()) {
return this->webServer->send(401);
}
if (vars.states.restarting) {
@@ -654,10 +632,8 @@ protected:
});
this->webServer->on(F("/api/vars"), HTTP_POST, [this]() {
if (this->isAuthRequired()) {
if (!this->webServer->authenticate(settings.portal.login, settings.portal.password)) {
return this->webServer->send(401);
}
if (this->isAuthRequired() && !this->isValidCredentials()) {
return this->webServer->send(401);
}
const String& plain = this->webServer->arg(0);
@@ -936,6 +912,14 @@ protected:
}
if (this->stateWebServer()) {
#ifdef ARDUINO_ARCH_ESP32
// Fix ERR_CONNECTION_RESET for Chrome based browsers
auto& client = this->webServer->client();
if (!client.getNoDelay()) {
client.setNoDelay(true);
}
#endif
this->webServer->handleClient();
}
@@ -948,6 +932,10 @@ protected:
return !network->isApEnabled() && settings.portal.auth && strlen(settings.portal.password);
}
bool isValidCredentials() {
return this->webServer->authenticate(settings.portal.login, settings.portal.password);
}
void onCaptivePortal() {
const String& uri = this->webServer->uri();

View File

@@ -122,14 +122,14 @@ public:
return true;
}
static uint8_t getAmountByType(Type type) {
static uint8_t getAmountByType(Type type, bool onlyEnabled = false) {
if (settings == nullptr) {
return 0;
}
uint8_t amount = 0;
for (uint8_t id = 0; id < getMaxSensorId(); id++) {
if (settings[id].type == type) {
if (settings[id].type == type && (!onlyEnabled || settings[id].enabled)) {
amount++;
}
}

View File

@@ -561,7 +561,7 @@ protected:
return;
}
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.01f);
float rawTemp = (pChar->getValue<int16_t>() * 0.01f);
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': received temp: %.2f"),
@@ -634,7 +634,7 @@ protected:
return;
}
float rawTemp = ((pData[0] | (pData[1] << 8)) * 0.1f);
float rawTemp = (pChar->getValue<int16_t>() * 0.1f);
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': received temp: %.2f"),
@@ -719,7 +719,7 @@ protected:
return;
}
float rawHumidity = ((pData[0] | (pData[1] << 8)) * 0.01f);
float rawHumidity = (pChar->getValue<uint16_t>() * 0.01f);
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': received humidity: %.2f"),
@@ -818,7 +818,7 @@ protected:
return;
}
uint8_t rawBattery = pData[0];
auto rawBattery = pChar->getValue<uint8_t>();
Log.straceln(
FPSTR(L_SENSORS_BLE),
F("Sensor #%hhu '%s': received battery: %.2f"),

Binary file not shown.

View File

@@ -49,6 +49,12 @@
<glyph glyph-name="wifi-strength-2-1"
unicode="&#x77;&#x69;&#x66;&#x69;&#x5F;&#x73;&#x74;&#x72;&#x65;&#x6E;&#x67;&#x74;&#x68;&#x5F;&#x32;"
horiz-adv-x="1022.9733303792667" d="M511.4866651896334 768.1283337025917C332.0397604864727 768.1283337025917 157.7087214326013 708.0286505428097 16.1967447318741 597.6324456402496C187.9713494571903 381.9559014844185 336.3024823621839 198.2472739029395 511.4866651896334 -20.4132754656288C685.3921313541088 196.1154134663874 862.7071756207173 416.9078232715078 1008.0553023103668 597.6324456402497C866.1177527202434 708.0286505428097 691.3591427821901 768.1283337025917 511.4866651896334 768.1283337025917M511.4866651896334 682.8808891701171C642.3423366997788 682.8808891701171 771.0661477733722 646.2236788332648 882.7410693389065 578.4516956956384L745.9183864006797 408.8089514068742C693.0644313319532 438.2194346552781 612.505281564586 469.7607793428413 511.4866651896334 469.7607793428413C410.0414769278917 469.7607793428413 329.9088990473136 437.7928627684892 277.0549439785871 408.8089514068743L139.3801162641751 578.8782675824273C251.9071826058945 646.6502507200537 380.6309936794878 682.8808891701171 511.4866651896334 682.8808891701171z" />
<glyph glyph-name="error"
unicode="&#xE007;"
horiz-adv-x="1024" d="M512 810.667C747.605 810.667 938.667 619.605 938.667 384C938.667 148.395 747.6049999999999 -42.6669999999999 511.9999999999999 -42.6669999999999C276.395 -42.6669999999999 85.333 148.3950000000001 85.333 384.0000000000001C85.333 619.605 276.395 810.667 512 810.667zM512 725.333C323.7970000000001 725.333 170.667 572.203 170.667 384C170.667 195.797 323.797 42.6669999999999 512 42.6669999999999C700.203 42.6669999999999 853.3330000000001 195.7969999999999 853.3330000000001 384C853.3330000000001 572.203 700.2030000000001 725.3330000000001 512 725.3330000000001zM609.835 542.165L670.1650000000001 481.835L572.33 384L670.1650000000001 286.165L609.835 225.8349999999999L512 323.67L414.165 225.8349999999999L353.8350000000001 286.165L451.67 384L353.8350000000001 481.835L414.165 542.165L512 444.33L609.835 542.165z" />
<glyph glyph-name="error-1"
unicode="&#x65;&#x72;&#x72;&#x6F;&#x72;"
horiz-adv-x="1024" d="M512 810.667C747.605 810.667 938.667 619.605 938.667 384C938.667 148.395 747.6049999999999 -42.6669999999999 511.9999999999999 -42.6669999999999C276.395 -42.6669999999999 85.333 148.3950000000001 85.333 384.0000000000001C85.333 619.605 276.395 810.667 512 810.667zM512 725.333C323.7970000000001 725.333 170.667 572.203 170.667 384C170.667 195.797 323.797 42.6669999999999 512 42.6669999999999C700.203 42.6669999999999 853.3330000000001 195.7969999999999 853.3330000000001 384C853.3330000000001 572.203 700.2030000000001 725.3330000000001 512 725.3330000000001zM609.835 542.165L670.1650000000001 481.835L572.33 384L670.1650000000001 286.165L609.835 225.8349999999999L512 323.67L414.165 225.8349999999999L353.8350000000001 286.165L451.67 384L353.8350000000001 481.835L414.165 542.165L512 444.33L609.835 542.165z" />
<glyph glyph-name="wifi-strength-3"
unicode="&#xE008;"
horiz-adv-x="1022.9733303792667" d="M511.4866651896334 768.1283337025917C332.0397604864727 768.1283337025917 157.7087214326013 708.0286505428097 16.1967447318741 597.6324456402496C187.9713494571903 381.9559014844185 336.3024823621839 198.2472739029395 511.4866651896334 -20.4132754656288C685.3921313541088 196.1154134663874 862.7071756207173 416.9078232715078 1008.0553023103668 597.6324456402497C866.1177527202434 708.0286505428097 691.3591427821901 768.1283337025917 511.4866651896334 768.1283337025917M511.4866651896334 682.8808891701171C642.3423366997788 682.8808891701171 771.0661477733722 646.2236788332648 882.7410693389065 578.4516956956384L800.0500591349871 474.8756459947375C735.688653096887 512.385001107775 634.2434648351453 555.0092228727087 511.4866651896334 555.0092228727087C383.614998892225 555.0092228727087 284.7272439564316 512.385001107775 222.0701274707015 476.5809345445006L139.3801162641751 578.8782675824273C251.9071826058945 646.6502507200537 380.6309936794878 682.8808891701171 511.4866651896334 682.8808891701171z" />
@@ -67,12 +73,24 @@
<glyph glyph-name="wifi-strength-4-1"
unicode="&#x77;&#x69;&#x66;&#x69;&#x5F;&#x73;&#x74;&#x72;&#x65;&#x6E;&#x67;&#x74;&#x68;&#x5F;&#x34;"
horiz-adv-x="1022.9733303792667" d="M511.4866651896334 768.1283337025917C332.0397604864727 768.1283337025917 157.7087214326013 708.0286505428097 16.1967447318741 597.6324456402496C187.9713494571903 381.9559014844185 336.3024823621839 198.2472739029395 511.4866651896334 -20.4132754656288C685.3921313541088 196.1154134663874 862.7071756207173 416.9078232715078 1008.0553023103668 597.6324456402497C866.1177527202434 708.0286505428097 691.3591427821901 768.1283337025917 511.4866651896334 768.1283337025917z" />
<glyph glyph-name="success"
unicode="&#xE00B;"
horiz-adv-x="1024" d="M512 810.667C276.36 810.667 85.333 619.64 85.333 384C85.333 148.36 276.3590000000001 -42.6669999999999 512 -42.6669999999999C747.64 -42.6669999999999 938.667 148.3590000000002 938.667 384.0000000000001C938.667 619.64 747.64 810.667 512 810.667zM512 42.667C323.79 42.667 170.667 195.789 170.667 384.0000000000001C170.667 572.21 323.789 725.3330000000001 512 725.3330000000001C700.21 725.3330000000001 853.3330000000001 572.211 853.3330000000001 384.0000000000001C853.3330000000001 195.7900000000001 700.211 42.667 512 42.667zM672.672 536.437L733.005 476.104L469.333 211.328L311.167 369.495L371.5 429.828L469.333 331.995L672.673 536.438z" />
<glyph glyph-name="success-1"
unicode="&#x73;&#x75;&#x63;&#x63;&#x65;&#x73;&#x73;"
horiz-adv-x="1024" d="M512 810.667C276.36 810.667 85.333 619.64 85.333 384C85.333 148.36 276.3590000000001 -42.6669999999999 512 -42.6669999999999C747.64 -42.6669999999999 938.667 148.3590000000002 938.667 384.0000000000001C938.667 619.64 747.64 810.667 512 810.667zM512 42.667C323.79 42.667 170.667 195.789 170.667 384.0000000000001C170.667 572.21 323.789 725.3330000000001 512 725.3330000000001C700.21 725.3330000000001 853.3330000000001 572.211 853.3330000000001 384.0000000000001C853.3330000000001 195.7900000000001 700.211 42.667 512 42.667zM672.672 536.437L733.005 476.104L469.333 211.328L311.167 369.495L371.5 429.828L469.333 331.995L672.673 536.438z" />
<glyph glyph-name="up"
unicode="&#xE00C;"
horiz-adv-x="893.7641930109211" d="M69.6258266430383 317.8562626928574L25.3366188643721 362.1454704715236C6.5835308860179 380.8985584498777 6.5835308860179 411.2227007127483 25.3366188643721 429.7762877551625L412.9669373956711 817.6021172036826C431.7200253740253 836.3552051820368 462.0441676368958 836.3552051820368 480.59775467931 817.6021172036826L868.2280732106092 429.9717986723836C886.9811611889633 411.2187106940295 886.9811611889633 380.894568431159 868.2280732106092 362.3409813887447L823.938865431943 318.0517736100786C804.9862765176489 299.0991846957845 774.0636314469585 299.4981865676644 755.5100444045443 318.8497773538383L526.6824708814356 559.0528942442421V-14.3127956471388C526.6824708814356 -40.8464201271506 505.3358707358623 -62.1930202727239 478.8022462558506 -62.1930202727239H414.9619467550705C388.4283222750588 -62.1930202727239 367.0817221294855 -40.8464201271506 367.0817221294855 -14.3127956471388V559.0528942442422L138.0546476704369 318.6542664366173C119.5010606280227 299.1031747145034 88.5784155573324 298.7041728426234 69.6258266430383 317.8562626928575z" />
<glyph glyph-name="up-1"
unicode="&#x75;&#x70;"
horiz-adv-x="893.7641930109211" d="M69.6258266430383 317.8562626928574L25.3366188643721 362.1454704715236C6.5835308860179 380.8985584498777 6.5835308860179 411.2227007127483 25.3366188643721 429.7762877551625L412.9669373956711 817.6021172036826C431.7200253740253 836.3552051820368 462.0441676368958 836.3552051820368 480.59775467931 817.6021172036826L868.2280732106092 429.9717986723836C886.9811611889633 411.2187106940295 886.9811611889633 380.894568431159 868.2280732106092 362.3409813887447L823.938865431943 318.0517736100786C804.9862765176489 299.0991846957845 774.0636314469585 299.4981865676644 755.5100444045443 318.8497773538383L526.6824708814356 559.0528942442421V-14.3127956471388C526.6824708814356 -40.8464201271506 505.3358707358623 -62.1930202727239 478.8022462558506 -62.1930202727239H414.9619467550705C388.4283222750588 -62.1930202727239 367.0817221294855 -40.8464201271506 367.0817221294855 -14.3127956471388V559.0528942442422L138.0546476704369 318.6542664366173C119.5010606280227 299.1031747145034 88.5784155573324 298.7041728426234 69.6258266430383 317.8562626928575z" />
<glyph glyph-name="alarm"
unicode="&#xE00D;"
horiz-adv-x="1024" d="M512 810.667C747.22 810.667 938.667 619.22 938.667 384C938.667 148.78 747.2199999999999 -42.6669999999999 511.9999999999999 -42.6669999999999C276.78 -42.667 85.333 148.78 85.333 384C85.333 619.22 276.78 810.667 512 810.667zM512 725.333C323.082 725.333 170.667 572.918 170.667 384C170.667 195.082 323.082 42.6669999999999 512 42.6669999999999C700.918 42.6669999999999 853.3330000000001 195.0819999999999 853.3330000000001 384C853.3330000000001 572.918 700.9180000000001 725.3330000000001 512 725.3330000000001zM512 266.581C542.476 266.581 565.333 244.054 565.333 213.3340000000001S542.476 160.085 512 160.085C480.83 160.085 458.667 182.6130000000001 458.667 214.015C458.667 244.053 481.524 266.581 512 266.581zM554.667 640V341.3330000000001H469.333V640H554.667z" />
<glyph glyph-name="alarm-1"
unicode="&#x61;&#x6C;&#x61;&#x72;&#x6D;"
horiz-adv-x="1024" d="M512 810.667C747.22 810.667 938.667 619.22 938.667 384C938.667 148.78 747.2199999999999 -42.6669999999999 511.9999999999999 -42.6669999999999C276.78 -42.667 85.333 148.78 85.333 384C85.333 619.22 276.78 810.667 512 810.667zM512 725.333C323.082 725.333 170.667 572.918 170.667 384C170.667 195.082 323.082 42.6669999999999 512 42.6669999999999C700.918 42.6669999999999 853.3330000000001 195.0819999999999 853.3330000000001 384C853.3330000000001 572.918 700.9180000000001 725.3330000000001 512 725.3330000000001zM512 266.581C542.476 266.581 565.333 244.054 565.333 213.3340000000001S542.476 160.085 512 160.085C480.83 160.085 458.667 182.6130000000001 458.667 214.015C458.667 244.053 481.524 266.581 512 266.581zM554.667 640V341.3330000000001H469.333V640H554.667z" />
</font>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -117,6 +117,15 @@
"mDhwTargetTemp": "DHW target temp",
"mDhwCurrTemp": "DHW current temp",
"mDhwRetTemp": "DHW return temp"
},
"sensors": {
"values": {
"temp": "Temperature",
"humidity": "Humidity",
"battery": "Battery",
"rssi": "RSSI"
}
}
},

View File

@@ -117,6 +117,15 @@
"mDhwTargetTemp": "ГВС, целевая температура",
"mDhwCurrTemp": "ГВС, текущая температура",
"mDhwRetTemp": "ГВС, температура обратки"
},
"sensors": {
"values": {
"temp": "Температура",
"humidity": "Влажность",
"battery": "Уровень заряда",
"rssi": "RSSI"
}
}
},

View File

@@ -11,12 +11,13 @@
<header class="container">
<nav>
<ul>
<li><a href="/">
<li>
<a href="/">
<div class="logo" data-i18n>logo</div>
</a></li>
</a>
</li>
</ul>
<ul>
<!--<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>-->
<li>
<select id="lang" aria-label="Lang">
<option value="en" selected>EN</option>
@@ -80,43 +81,43 @@
<tbody>
<tr>
<th scope="row" data-i18n>dashboard.states.mNetworkConnected</th>
<td><input type="radio" class="mNetworkConnected" aria-invalid="false" checked disabled /></td>
<td><i class="mNetworkConnected"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.mMqttConnected</th>
<td><input type="radio" class="mMqttConnected" aria-invalid="false" checked disabled /></td>
<td><i class="mMqttConnected"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.mEmergencyState</th>
<td><input type="radio" class="mEmergencyState" aria-invalid="false" checked disabled /></td>
<td><i class="mEmergencyState"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.mExtPumpState</th>
<td><input type="radio" class="mExtPumpState" aria-invalid="false" checked disabled /></td>
<td><i class="mExtPumpState"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.mCascadeControlInput</th>
<td><input type="radio" id="mCascadeControlInput" aria-invalid="false" checked disabled /></td>
<td><i class="mCascadeControlInput"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.mCascadeControlOutput</th>
<td><input type="radio" id="mCascadeControlOutput" aria-invalid="false" checked disabled /></td>
<td><i class="mCascadeControlOutput"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.sConnected</th>
<td><input type="radio" class="sConnected" aria-invalid="false" checked disabled /></td>
<td><i class="sConnected"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.sFlame</th>
<td><input type="radio" class="sFlame" aria-invalid="false" checked disabled /></td>
<td><i class="sFlame"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.sFaultActive</th>
<td><input type="radio" class="sFaultActive" aria-invalid="false" checked disabled /></td>
<td><i class="sFaultActive"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.sFaultCode</th>
@@ -124,7 +125,7 @@
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.sDiagActive</th>
<td><input type="radio" class="sDiagActive" aria-invalid="false" checked disabled /></td>
<td><i class="sDiagActive"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.sDiagCode</th>
@@ -134,15 +135,15 @@
<tr>
<th scope="row" data-i18n>dashboard.states.mHeatEnabled</th>
<td><input type="radio" class="mHeatEnabled" aria-invalid="false" checked disabled /></td>
<td><i class="mHeatEnabled"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.mHeatBlocking</th>
<td><input type="radio" class="mHeatBlocking" aria-invalid="false" checked disabled /></td>
<td><i class="mHeatBlocking"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.sHeatActive</th>
<td><input type="radio" class="sHeatActive" aria-invalid="false" checked disabled /></td>
<td><i class="sHeatActive"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.mHeatTargetTemp</th>
@@ -172,11 +173,11 @@
<tr>
<th scope="row" data-i18n>dashboard.states.mDhwEnabled</th>
<td><input type="radio" class="mDhwEnabled" aria-invalid="false" checked disabled /></td>
<td><i class="mDhwEnabled"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.sDhwActive</th>
<td><input type="radio" class="sDhwActive" aria-invalid="false" checked disabled /></td>
<td><i class="sDhwActive"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>dashboard.states.mDhwTargetTemp</th>
@@ -196,6 +197,25 @@
<hr />
<details>
<summary><b data-i18n>dashboard.section.sensors</b></summary>
<table>
<tbody class="sensors">
<tr class="sensor template hidden">
<td style="width: 1.35rem">
<span class="sStatusContainer">
<i class="sStatus"></i>
</span>
</td>
<th scope="row" class="sName"></th>
<td class="sValue"></td>
</tr>
</tbody>
</table>
</details>
<hr />
<details>
<summary><b data-i18n>dashboard.section.diag</b></summary>
<pre><b>Vendor:</b> <span class="sVendor"></span>
@@ -335,6 +355,8 @@
});
setTimeout(async function onLoadPage() {
let unitSystem = null;
if (modifiedTime) {
if ((Date.now() - modifiedTime) < 5000) {
setTimeout(onLoadPage, 1000);
@@ -358,13 +380,18 @@
console.log(newSettings);
}
let parameters = { cache: 'no-cache' };
let parameters = {
method: "GET",
cache: "no-cache",
credentials: "include"
};
if (modified) {
parameters.method = "POST";
parameters.body = JSON.stringify(newSettings);
}
const response = await fetch('/api/settings', parameters);
const response = await fetch("/api/settings", parameters);
if (!response.ok) {
throw new Error('Response not valid');
}
@@ -372,6 +399,7 @@
const result = await response.json();
noRegulators = !result.opentherm.nativeHeatingControl && !result.equitherm.enabled && !result.pid.enabled;
prevSettings = result;
unitSystem = result.system.unitSystem;
newSettings.heating.enabled = result.heating.enabled;
newSettings.heating.turbo = result.heating.turbo;
newSettings.heating.target = result.heating.target;
@@ -391,9 +419,9 @@
setCheckboxValue('#tDhwEnabled', result.dhw.enabled);
setValue('#tDhwTargetTemp', result.dhw.target);
setValue('.tempUnit', temperatureUnit(result.system.unitSystem));
setValue('.pressureUnit', pressureUnit(result.system.unitSystem));
setValue('.volumeUnit', volumeUnit(result.system.unitSystem));
setValue('.tempUnit', temperatureUnit(unitSystem));
setValue('.pressureUnit', pressureUnit(unitSystem));
setValue('.volumeUnit', volumeUnit(unitSystem));
} catch (error) {
console.log(error);
@@ -401,7 +429,11 @@
// vars
try {
const response = await fetch('/api/vars', { cache: 'no-cache' });
const response = await fetch("/api/vars", {
cache: "no-cache",
credentials: "include"
});
if (!response.ok) {
throw new Error('Response not valid');
}
@@ -424,7 +456,11 @@
setValue('.sAppVersion', result.slave.appVersion);
setValue('.sProtocolVersion', result.slave.protocolVersion);
setState('.sConnected', result.slave.connected);
setStatus(
'.sConnected',
result.slave.connected ? "success" : "error",
result.slave.connected ? "green" : "red"
);
setState('.sFlame', result.slave.flame);
setValue('.sModMin', result.slave.modulation.min);
@@ -441,26 +477,38 @@
setValue('.sDhwMinTemp', result.slave.dhw.minTemp);
setValue('.sDhwMaxTemp', result.slave.dhw.maxTemp);
setState('.sFaultActive', result.slave.fault.active);
setStatus(
'.sFaultActive',
result.slave.fault.active ? "success" : "error",
result.slave.fault.active ? "red" : "green"
);
setValue(
'.sFaultCode',
result.slave.fault.active
? (result.slave.fault.code + " (0x" + dec2hex(result.slave.fault.code) + ")")
? `${result.slave.fault.code} (0x${dec2hex(result.slave.fault.code)})`
: "-"
);
setState('.sDiagActive', result.slave.diag.active);
setStatus(
'.sDiagActive',
result.slave.diag.active ? "success" : "error",
result.slave.diag.active ? "red" : "green"
);
setValue(
'.sDiagCode',
result.slave.diag.active
? (result.slave.diag.code + " (0x" + dec2hex(result.slave.diag.code) + ")")
? `${result.slave.diag.code} (0x${dec2hex(result.slave.diag.code)})`
: "-"
);
// MASTER
setState('.mHeatEnabled', result.master.heating.enabled);
setState('.mHeatBlocking', result.master.heating.blocking);
setStatus(
'.mHeatBlocking',
result.master.heating.blocking ? "success" : "error",
result.master.heating.blocking ? "red" : "green"
);
setState('.mHeatIndoorTempControl', result.master.heating.indoorTempControl);
setValue('.mHeatSetpointTemp', result.master.heating.setpointTemp);
setValue('.mHeatTargetTemp', result.master.heating.targetTemp);
@@ -478,9 +526,17 @@
setValue('.mDhwMinTemp', result.master.dhw.minTemp);
setValue('.mDhwMaxTemp', result.master.dhw.maxTemp);
setState('.mNetworkConnected', result.master.network.connected);
setStatus(
'.mNetworkConnected',
result.master.network.connected ? "success" : "error",
result.master.network.connected ? "green" : "red"
);
setState('.mMqttConnected', result.master.mqtt.connected);
setState('.mEmergencyState', result.master.emergency.state);
setStatus(
'.mEmergencyState',
result.master.emergency.state ? "success" : "error",
result.master.emergency.state ? "red" : "green"
);
setState('.mExtPumpState', result.master.externalPump.state);
setState('.mCascadeControlInput', result.master.cascadeControl.input);
setState('.mCascadeControlOutput', result.master.cascadeControl.output);
@@ -491,6 +547,88 @@
console.log(error);
}
// sensors
try {
const response = await fetch("/api/sensors?detailed=1", {
cache: "no-cache",
credentials: "include"
});
if (!response.ok) {
throw new Error("Response not valid");
}
const container = document.querySelector(".sensors");
const templateNode = container.querySelector(".template");
const result = await response.json();
for (const sensorId in result) {
let sensorNode = container.querySelector(`.sensor[data-id='${sensorId}']`);
if (sensorNode) {
continue;
}
sensorNode = templateNode.cloneNode(true);
sensorNode.dataset.id = sensorId;
sensorNode.classList.remove("template");
container.appendChild(sensorNode);
}
for (const sensorId in result) {
const sensorNode = container.querySelector(`.sensor[data-id='${sensorId}']`);
if (!sensorNode) {
continue;
}
const sData = result[sensorId];
if (!sData.enabled || sData.purpose == 255) {
sensorNode.classList.toggle("hidden", true);
continue;
}
sensorNode.classList.toggle("hidden", false);
setStatus(
".sStatus",
sData.connected ? "success" : "error",
sData.connected ? "green" : "red",
sensorNode
);
setValue(".sName", sData.name, sensorNode);
setValue(".sValue", "", sensorNode);
const statusNode = sensorNode.querySelector(`.sStatusContainer`);
if (statusNode) {
statusNode.dataset.tooltip = `${sData.signalQuality}%`;
}
if (sData.value !== undefined) {
const sUnit = purposeUnit(sData.purpose, unitSystem);
appendValue(".sValue", `<b>${sData.value.toFixed(2)}</b> ${sUnit !== null ? sUnit : ``}`, `<br />`, sensorNode);
}
if (sData.temperature !== undefined) {
const sUnit = temperatureUnit(unitSystem);
appendValue(".sValue", `${i18n('dashboard.sensors.values.temp')}: <b>${sData.temperature.toFixed(2)}</b> ${sUnit !== null ? sUnit : ``}`, `<br />`, sensorNode);
}
if (sData.humidity !== undefined) {
appendValue(".sValue", `${i18n('dashboard.sensors.values.humidity')}: <b>${sData.humidity.toFixed(2)}</b> %`, `<br />`, sensorNode);
}
if (sData.battery !== undefined) {
appendValue(".sValue", `${i18n('dashboard.sensors.values.battery')}: <b>${sData.battery.toFixed(2)}</b> %`, `<br />`, sensorNode);
}
if (sData.rssi !== undefined) {
appendValue(".sValue", `${i18n('dashboard.sensors.values.rssi')}: <b>${sData.rssi.toFixed(0)}</b> ${i18n('dbm')}`, `<br />`, sensorNode);
}
}
} catch (error) {
console.log(error);
}
setTimeout(onLoadPage, 10000);
}, 1000);
});

View File

@@ -11,12 +11,13 @@
<header class="container">
<nav>
<ul>
<li><a href="/">
<li>
<a href="/">
<div class="logo" data-i18n>logo</div>
</a></li>
</a>
</li>
</ul>
<ul>
<!--<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>-->
<li>
<select id="lang" aria-label="Lang">
<option value="en" selected>EN</option>
@@ -48,7 +49,7 @@
</tr>
<tr>
<th scope="row" data-i18n>network.wifi.connected</th>
<td><input type="radio" id="network-connected" aria-invalid="false" checked disabled /></td>
<td><i id="network-connected"></i></td>
</tr>
<tr>
<th scope="row" data-i18n>network.wifi.ssid</th>
@@ -169,7 +170,11 @@
setTimeout(async function onLoadPage() {
try {
const response = await fetch('/api/info', { cache: 'no-cache' });
const response = await fetch("/api/info", {
cache: "no-cache",
credentials: "include"
});
if (!response.ok) {
throw new Error('Response not valid');
}
@@ -184,7 +189,11 @@
setValue('#network-hostname', result.network.hostname);
setValue('#network-mac', result.network.mac);
setState('#network-connected', result.network.connected);
setStatus(
'#network-connected',
result.network.connected ? "success" : "error",
result.network.connected ? "green" : "red"
);
setValue('#network-ssid', result.network.ssid);
setValue('#network-signal', result.network.signalQuality);
setValue('#network-ip', result.network.ip);

View File

@@ -11,12 +11,13 @@
<header class="container">
<nav>
<ul>
<li><a href="/">
<li>
<a href="/">
<div class="logo" data-i18n>logo</div>
</a></li>
</a>
</li>
</ul>
<ul>
<!--<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>-->
<li>
<select id="lang" aria-label="Lang">
<option value="en" selected>EN</option>
@@ -198,7 +199,11 @@
};
try {
const response = await fetch('/api/network/settings', { cache: 'no-cache' });
const response = await fetch("/api/network/settings", {
cache: "no-cache",
credentials: "include"
});
if (!response.ok) {
throw new Error('Response not valid');
}

View File

@@ -11,12 +11,13 @@
<header class="container">
<nav>
<ul>
<li><a href="/">
<li>
<a href="/">
<div class="logo" data-i18n>logo</div>
</a></li>
</a>
</li>
</ul>
<ul>
<!--<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>-->
<li>
<select id="lang" aria-label="Lang">
<option value="en" selected>EN</option>
@@ -193,7 +194,11 @@
const templateNode = container.querySelector("#template");
try {
const response = await fetch("/api/sensors", { cache: "no-cache" });
const response = await fetch("/api/sensors", {
cache: "no-cache",
credentials: "include"
});
if (!response.ok) {
throw new Error("Response not valid");
}
@@ -286,7 +291,11 @@
}
try {
const response = await fetch(sensorForm.action, { cache: "no-cache" });
const response = await fetch(sensorForm.action, {
cache: "no-cache",
credentials: "include"
});
if (response.status != 200) {
return;
}
@@ -300,7 +309,7 @@
}
});
setupForm(".sensor[data-id='" + sensorId + "'] form", fillData, ['address']);
setupForm(`.sensor[data-id='${sensorId}'] form`, fillData, ['address']);
}
} catch (error) {

View File

@@ -11,12 +11,13 @@
<header class="container">
<nav>
<ul>
<li><a href="/">
<li>
<a href="/">
<div class="logo" data-i18n>logo</div>
</a></li>
</a>
</li>
</ul>
<ul>
<!--<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>-->
<li>
<select id="lang" aria-label="Lang">
<option value="en" selected>EN</option>
@@ -807,7 +808,11 @@
};
try {
const response = await fetch('/api/settings', { cache: 'no-cache' });
const response = await fetch("/api/settings", {
cache: "no-cache",
credentials: "include"
});
if (!response.ok) {
throw new Error('Response not valid');
}

View File

@@ -11,12 +11,13 @@
<header class="container">
<nav>
<ul>
<li><a href="/">
<li>
<a href="/">
<div class="logo" data-i18n>logo</div>
</a></li>
</a>
</li>
</ul>
<ul>
<!--<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>-->
<li>
<select id="lang" aria-label="Lang">
<option value="en" selected>EN</option>
@@ -41,7 +42,7 @@
<input type="file" name="settings" id="restore-file" accept=".json">
</label>
<div class="grid">
<div role="group">
<button type="submit" data-i18n>button.restore</button>
<button type="button" class="secondary" onclick="window.location='/api/backup/save';" data-i18n>button.backup</button>
</div>

View File

@@ -32,7 +32,7 @@ class Lang {
}
if (!this.localeIsSupported(this.defaultLocale)) {
const selected = this.switcher.selectedIndex ?? 0;
const selected = this.switcher.selectedIndex ? this.switcher.selectedIndex : 0;
this.defaultLocale = this.switcher.options[selected].value;
}

View File

@@ -60,10 +60,11 @@ const setupForm = (formSelector, onResultCallback = null, noCastItems = []) => {
}
let response = await fetch(url, {
method: 'POST',
cache: 'no-cache',
method: "POST",
cache: "no-cache",
credentials: "include",
headers: {
'Content-Type': 'application/json'
"Content-Type": "application/json"
},
body: form2json(fd, noCastItems)
});
@@ -143,8 +144,8 @@ const setupNetworkScanForm = (formSelector, tableSelector) => {
input.focus();
};
row.insertCell().textContent = "#" + (i + 1);
row.insertCell().innerHTML = result[i].hidden ? ("<i>" + result[i].bssid + "</i>") : result[i].ssid;
row.insertCell().textContent = `#${i + 1}`;
row.insertCell().innerHTML = result[i].hidden ? `<i>${result[i].bssid}</i>` : result[i].ssid;
// info cell
let infoCell = row.insertCell();
@@ -164,7 +165,7 @@ const setupNetworkScanForm = (formSelector, tableSelector) => {
}
let signalQualityContainer = document.createElement("span");
signalQualityContainer.setAttribute('data-tooltip', result[i].signalQuality + "%");
signalQualityContainer.setAttribute('data-tooltip', `${result[i].signalQuality}%`);
signalQualityContainer.appendChild(signalQualityIcon);
infoCell.appendChild(signalQualityContainer);
@@ -218,7 +219,10 @@ const setupNetworkScanForm = (formSelector, tableSelector) => {
attempts--;
try {
let response = await fetch(url, { cache: 'no-cache' });
let response = await fetch(url, {
cache: "no-cache",
credentials: "include"
});
if (response.status == 200) {
await onSuccess(response);
@@ -309,10 +313,11 @@ const setupRestoreBackupForm = (formSelector) => {
if (data.settings != undefined) {
let response = await fetch(url, {
method: 'POST',
cache: 'no-cache',
method: "POST",
cache: "no-cache",
credentials: "include",
headers: {
'Content-Type': 'application/json'
"Content-Type": "application/json"
},
body: JSON.stringify({"settings": data.settings})
});
@@ -331,10 +336,11 @@ const setupRestoreBackupForm = (formSelector) => {
payload["sensors"][sensorId] = data.sensors[sensorId];
const response = await fetch(url, {
method: 'POST',
cache: 'no-cache',
method: "POST",
cache: "no-cache",
credentials: "include",
headers: {
'Content-Type': 'application/json'
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
@@ -348,10 +354,11 @@ const setupRestoreBackupForm = (formSelector) => {
if (data.network != undefined) {
let response = await fetch(url, {
method: 'POST',
cache: 'no-cache',
method: "POST",
cache: "no-cache",
credentials: "include",
headers: {
'Content-Type': 'application/json'
"Content-Type": "application/json"
},
body: JSON.stringify({"network": data.network})
});
@@ -427,7 +434,7 @@ const setupUpgradeForm = (formSelector) => {
resItem.classList.add('failed');
if (result.firmware.error != "") {
resItem.textContent += ": " + result.firmware.error;
resItem.textContent += `: ${result.firmware.error}`;
}
}
}
@@ -445,7 +452,7 @@ const setupUpgradeForm = (formSelector) => {
resItem.classList.add('failed');
if (result.filesystem.error != "") {
resItem.textContent += ": " + result.filesystem.error;
resItem.textContent += `: ${result.filesystem.error}`;
}
}
}
@@ -496,8 +503,9 @@ const setupUpgradeForm = (formSelector) => {
try {
let fd = new FormData(form);
let response = await fetch(url, {
method: 'POST',
cache: 'no-cache',
method: "POST",
cache: "no-cache",
credentials: "include",
body: fd
});
@@ -526,8 +534,8 @@ const setBusy = (busySelector, contentSelector, value, parent = undefined) => {
}
}
const setState = (selector, value, parent = undefined) => {
if (parent == undefined) {
const setAriaState = (selector, value, parent = undefined) => {
if (parent === undefined) {
parent = document;
}
@@ -539,8 +547,40 @@ const setState = (selector, value, parent = undefined) => {
item.setAttribute('aria-invalid', !value);
}
const setStatus = (selector, state, color = undefined, parent = undefined) => {
if (parent === undefined) {
parent = document;
}
let item = parent.querySelector(selector);
if (!item) {
return;
}
item.classList.forEach(cName => {
if (cName.indexOf("icons-") === 0) {
item.classList.remove(cName);
}
});
item.classList.add(`icons-${state}`);
if (color !== undefined) {
item.classList.add(`icons-color-${color}`);
}
}
const setState = (selector, state, parent = undefined) => {
return setStatus(
selector,
state ? "success" : "error",
state ? "green" : "gray",
parent
);
}
const setValue = (selector, value, parent = undefined) => {
if (parent == undefined) {
if (parent === undefined) {
parent = document;
}
@@ -554,8 +594,32 @@ const setValue = (selector, value, parent = undefined) => {
}
}
const appendValue = (selector, value, nl = null, parent = undefined) => {
if (parent === undefined) {
parent = document;
}
let items = parent.querySelectorAll(selector);
if (!items.length) {
return;
}
for (let item of items) {
if (item.innerHTML.trim().length > 0) {
if (nl !== null) {
item.innerHTML += nl;
}
item.innerHTML += value;
} else {
item.innerHTML = value;
}
}
}
const setCheckboxValue = (selector, value, parent = undefined) => {
if (parent == undefined) {
if (parent === undefined) {
parent = document;
}
@@ -568,7 +632,7 @@ const setCheckboxValue = (selector, value, parent = undefined) => {
}
const setRadioValue = (selector, value, parent = undefined) => {
if (parent == undefined) {
if (parent === undefined) {
parent = document;
}
@@ -583,7 +647,7 @@ const setRadioValue = (selector, value, parent = undefined) => {
}
const setInputValue = (selector, value, attrs = {}, parent = undefined) => {
if (parent == undefined) {
if (parent === undefined) {
parent = document;
}
@@ -604,7 +668,7 @@ const setInputValue = (selector, value, attrs = {}, parent = undefined) => {
}
const setSelectValue = (selector, value, parent = undefined) => {
if (parent == undefined) {
if (parent === undefined) {
parent = document;
}
@@ -619,7 +683,7 @@ const setSelectValue = (selector, value, parent = undefined) => {
}
const show = (selector, parent = undefined) => {
if (parent == undefined) {
if (parent === undefined) {
parent = document;
}
@@ -636,7 +700,7 @@ const show = (selector, parent = undefined) => {
}
const hide = (selector, parent = undefined) => {
if (parent == undefined) {
if (parent === undefined) {
parent = document;
}
@@ -679,6 +743,29 @@ const volumeUnit = (unitSystem) => {
});
}
const purposeUnit = (purpose, unitSystem) => {
const tUnit = temperatureUnit(unitSystem);
return unit2str(purpose, {
0: tUnit,
1: tUnit,
2: tUnit,
3: tUnit,
4: tUnit,
5: tUnit,
6: `${volumeUnit(unitSystem)}/${i18n('time.min')}`,
7: tUnit,
8: "%",
248: "%",
249: i18n('kw'),
250: "RPM",
251: "ppm",
252: pressureUnit(unitSystem),
253: "%",
254: tUnit
}, null);
}
const memberIdToVendor = (memberId) => {
// https://github.com/Jeroen88/EasyOpenTherm/blob/main/src/EasyOpenTherm.h
// https://github.com/Evgen2/SmartTherm/blob/v0.7/src/Web.cpp
@@ -757,7 +844,7 @@ function form2json(data, noCastItems = []) {
function dec2hex(i) {
let hex = parseInt(i).toString(16);
if (hex.length % 2 != 0) {
hex = "0" + hex;
hex = `0${hex}`;
}
return hex.toUpperCase();

View File

@@ -59,7 +59,7 @@ footer {
/*nav li a:has(> div.logo) {
margin-bottom: 0;
}*/
nav li :where(a,[role=link]) {
nav li :where(a, [role=link]) {
margin: 0;
}
@@ -71,6 +71,11 @@ pre {
padding: 0.5rem;
}
:nth-last-child(1 of table tr:not(.hidden)) th,
:nth-last-child(1 of table tr:not(.hidden)) td {
border-bottom: 0 !important;
}
.hidden {
display: none !important;
@@ -200,4 +205,20 @@ tr.network:hover {
[data-tooltip]:has(> [class*=" icons-"], > [class=icons], > [class^=icons-]) {
border: 0 !important;
}
.icons-color-green {
color: var(--pico-form-element-valid-active-border-color);
}
.icons-color-yellow {
color: #c89048;
}
.icons-color-gray {
color: var(--pico-form-element-placeholder-color);
}
.icons-color-red {
color: var(--pico-form-element-invalid-active-border-color);
}

View File

@@ -7,8 +7,8 @@
font-family: "Icons";
font-style: normal;
font-weight: 400;
src: url("/static/fonts/iconly.eot?1718563596894");
src: url("/static/fonts/iconly.eot?#iefix") format("embedded-opentype"), url("/static/fonts/iconly.woff2?1718563596894") format("woff2"), url("/static/fonts/iconly.woff?1718563596894") format("woff"), url("/static/fonts/iconly.ttf?1718563596894") format("truetype"), url("/static/fonts/iconly.svg?1718563596894#Icons") format("svg");
src: url("/static/fonts/iconly.eot?1732913937966");
src: url("/static/fonts/iconly.eot?#iefix") format("embedded-opentype"), url("/static/fonts/iconly.woff2?1732913937966") format("woff2"), url("/static/fonts/iconly.woff?1732913937966") format("woff"), url("/static/fonts/iconly.ttf?1732913937966") format("truetype"), url("/static/fonts/iconly.svg?1732913937966#Icons") format("svg");
}
[class="icons"], [class^="icons-"], [class*=" icons-"] {
@@ -51,6 +51,10 @@
content: "\e006";
}
.icons-error:before {
content: "\e007";
}
.icons-wifi-strength-3:before {
content: "\e008";
}
@@ -63,6 +67,14 @@
content: "\e00a";
}
.icons-success:before {
content: "\e00b";
}
.icons-up:before {
content: "\e00c";
}
.icons-alarm:before {
content: "\e00d";
}