refactor: sensors type settings moved to portal, entities for HA have been deleted; logging settings moved; bump version

This commit is contained in:
Yurii
2024-01-14 19:16:24 +03:00
parent 30ae602ab9
commit 520baa4920
17 changed files with 207 additions and 164 deletions

View File

@@ -12,7 +12,7 @@
<header class="container">
<nav>
<ul>
<li><a href="/"><kbd>OpenTherm Gateway</kbd></a></li>
<li><a href="/"><div class="logo">OpenTherm Gateway</div></a></li>
</ul>
<ul>
<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Network settings - OpenTherm Gateway</title>
<title>Network - OpenTherm Gateway</title>
<link rel="stylesheet" href="/static/pico.min.css">
<link rel="stylesheet" href="/static/app.css" />
</head>
@@ -13,7 +13,7 @@
<header class="container">
<nav>
<ul>
<li><a href="/"><kbd>OpenTherm Gateway</kbd></a></li>
<li><a href="/"><div class="logo">OpenTherm Gateway</div></a></li>
</ul>
<ul>
<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>

View File

@@ -13,7 +13,7 @@
<header class="container">
<nav>
<ul>
<li><a href="/"><kbd>OpenTherm Gateway</kbd></a></li>
<li><a href="/"><div class="logo">OpenTherm Gateway</div></a></li>
</ul>
<ul>
<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>
@@ -64,11 +64,11 @@
<form action="/api/settings" id="opentherm-settings" class="hidden">
<div class="grid">
<label for="opentherm-in-pin">
IN gpio
In GPIO
<input type="number" class="opentherm-in-pin" name="opentherm[inPin]" maxlength="2" required>
</label>
<label for="opentherm-in-pin">
OUT gpio
Out GPIO
<input type="number" class="opentherm-out-pin" name="opentherm[outPin]" maxlength="2" required>
</label>
<label for="opentherm-member-id-code">
@@ -168,40 +168,83 @@
<article>
<div>
<hgroup>
<h2>Sensor settings</h2>
<h2>Outdoor sensor settings</h2>
<p></p>
</hgroup>
<div id="sensors-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="sensors-settings" class="hidden">
<div class="grid">
<label for="sensors-outdoor-pin">
Outdoor GPIO
<input type="number" class="sensors-outdoor-pin" name="sensors[outdoor][pin]" maxlength="2" required>
<div id="outdoor-sensor-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="outdoor-sensor-settings" class="hidden">
<fieldset>
<legend>Source type</legend>
<label>
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="0" />
From boiler via OpenTherm
</label>
<label for="sensors-outdoor-offset">
Outdoor offset
<input type="number" class="sensors-outdoor-offset" name="sensors[outdoor][offset]" maxlength="2" required>
<label>
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="1" />
Manual via MQTT/API
</label>
</div>
<hr><br>
<label>
<input type="radio" class="outdoor-sensor-type" name="sensors[outdoor][type]" value="2" />
External (DS18B20)
</label>
</fieldset>
<div class="grid">
<label for="sensors-indoor-pin">
Indoor GPIO
<input type="number" class="sensors-indoor-pin" name="sensors[indoor][pin]" maxlength="2" required>
</label>
<label for="sensors-indoor-offset">
Indoor offset
<input type="number" class="sensors-indoor-offset" name="sensors[indoor][offset]" maxlength="2" required>
</label>
</div>
<label for="sensors-indoor-ble-addresss">
BLE addresss
<input type="text" class="sensors-indoor-ble-addresss" name="sensors[indoor][bleAddresss]" maxlength="17">
<small>ONLY for some ESP32 which support BLE</small>
<label for="outdoor-sensor-pin">
GPIO
<input type="number" class="outdoor-sensor-pin" name="sensors[outdoor][pin]" maxlength="2" required>
</label>
<label for="outdoor-sensor-offset">
Temp offset (calibration)
<input type="number" class="outdoor-sensor-offset" name="sensors[outdoor][offset]" maxlength="2" required>
</label>
<button type="submit">Save</button>
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h2>Indoor sensor settings</h2>
<p></p>
</hgroup>
<div id="indoor-sensor-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="indoor-sensor-settings" class="hidden">
<fieldset>
<legend>Source type</legend>
<label>
<input type="radio" class="indoor-sensor-type" name="sensors[indoor][type]" value="1" />
Manual via MQTT/API
</label>
<label>
<input type="radio" class="indoor-sensor-type" name="sensors[indoor][type]" value="2" />
External (DS18B20)
</label>
<label>
<input type="radio" class="indoor-sensor-type" name="sensors[indoor][type]" value="3" />
BLE device <i>(ONLY for some ESP32 which support BLE)</i>
</label>
</fieldset>
<label for="indoor-sensor-pin">
GPIO
<input type="number" class="indoor-sensor-pin" name="sensors[indoor][pin]" maxlength="2" required>
</label>
<div class="grid">
<label for="indoor-sensor-offset">
Temp offset (calibration)
<input type="number" class="indoor-sensor-offset" name="sensors[indoor][offset]" maxlength="2" required>
</label>
<label for="indoor-sensor-ble-addresss">
BLE addresss
<input type="text" class="indoor-sensor-ble-addresss" name="sensors[indoor][bleAddresss]" maxlength="17">
<small>ONLY for some ESP32 which support BLE</small>
</label>
</div>
<button type="submit">Save</button>
</form>
@@ -249,6 +292,39 @@
</form>
</div>
</article>
<article>
<div>
<hgroup>
<h2>System settings</h2>
<p></p>
</hgroup>
<div id="system-settings-busy" aria-busy="true"></div>
<form action="/api/settings" id="system-settings" class="hidden">
<fieldset>
<label for="system-debug">
<input type="checkbox" class="system-debug" name="system[debug]" value="true">
Debug mode
</label>
<label for="system-use-serial">
<input type="checkbox" class="system-use-serial" name="system[useSerial]" value="true">
Enable serial port
</label>
<label for="system-use-telnet">
<input type="checkbox" class="system-use-telnet" name="system[useTelnet]" value="true">
Enable telnet
</label>
</fieldset>
<fieldset>
<mark>After changing this settings, the ESP must be restarted for the changes to take effect.</mark>
</fieldset>
<button type="submit">Save</button>
</form>
</div>
</article>
</main>
<footer class="container">
@@ -270,8 +346,10 @@
setupForm('#portal-settings');
setupForm('#opentherm-settings');
setupForm('#mqtt-settings');
setupForm('#sensors-settings');
setupForm('#outdoor-sensor-settings');
setupForm('#indoor-sensor-settings');
setupForm('#extpump-settings');
setupForm('#system-settings');
};
</script>
</body>

Binary file not shown.

Binary file not shown.

View File

@@ -13,7 +13,7 @@
<header class="container">
<nav>
<ul>
<li><a href="/"><kbd>OpenTherm Gateway</kbd></a></li>
<li><a href="/"><div class="logo">OpenTherm Gateway</div></a></li>
</ul>
<ul>
<li><a href="https://github.com/Laxilef/OTGateway/wiki" role="button" class="secondary" target="_blank">Help</a></li>

View File

@@ -61,4 +61,17 @@ tr.network:hover {
border: 0.25em solid var(--pico-form-element-invalid-border-color);
padding: 1em;
margin-bottom: 1em;
}
.logo {
display: inline-block;
padding: .5rem;
vertical-align: baseline;
line-height: initial;
background-color: var(--pico-code-kbd-background-color);
border-radius: var(--pico-border-radius);
color: var(--pico-code-kbd-color);
font-weight: bolder;
font-size: 1.5em;
font-family: var(--pico-font-family-monospace);
}

View File

@@ -468,6 +468,11 @@ async function loadSettings() {
let response = await fetch('/api/settings', { cache: 'no-cache' });
let result = await response.json();
setCheckboxValue('.system-debug', result.system.debug);
setCheckboxValue('.system-use-serial', result.system.useSerial);
setCheckboxValue('.system-use-telnet', result.system.useTelnet);
setBusy('#system-settings-busy', '#system-settings', false);
setCheckboxValue('.portal-use-auth', result.portal.useAuth);
setInputValue('.portal-login', result.portal.login);
setInputValue('.portal-password', result.portal.password);
@@ -493,12 +498,16 @@ async function loadSettings() {
setInputValue('.mqtt-interval', result.mqtt.interval);
setBusy('#mqtt-settings-busy', '#mqtt-settings', false);
setInputValue('.sensors-outdoor-pin', result.sensors.outdoor.pin);
setInputValue('.sensors-outdoor-offset', result.sensors.outdoor.offset);
setInputValue('.sensors-indoor-pin', result.sensors.indoor.pin);
setInputValue('.sensors-indoor-offset', result.sensors.indoor.offset);
setInputValue('.sensors-indoor-ble-addresss', result.sensors.indoor.bleAddresss);
setBusy('#sensors-settings-busy', '#sensors-settings', false);
setRadioValue('.outdoor-sensor-type', result.sensors.outdoor.type);
setInputValue('.outdoor-sensor-pin', result.sensors.outdoor.pin);
setInputValue('.outdoor-sensor-offset', result.sensors.outdoor.offset);
setBusy('#outdoor-sensor-settings-busy', '#outdoor-sensor-settings', false);
setRadioValue('.indoor-sensor-type', result.sensors.indoor.type);
setInputValue('.indoor-sensor-pin', result.sensors.indoor.pin);
setInputValue('.indoor-sensor-offset', result.sensors.indoor.offset);
setInputValue('.indoor-sensor-ble-addresss', result.sensors.indoor.bleAddresss);
setBusy('#indoor-sensor-settings-busy', '#indoor-sensor-settings', false);
setCheckboxValue('.extpump-use', result.externalPump.use);
setInputValue('.extpump-pin', result.externalPump.pin);
@@ -594,6 +603,17 @@ function setCheckboxValue(selector, value) {
item.checked = value;
}
function setRadioValue(selector, value) {
let items = document.querySelectorAll(selector);
if (!items.length) {
return;
}
for (let item of items) {
item.checked = item.value == value;
}
}
function setInputValue(selector, value) {
let item = document.querySelector(selector);
if (!item) {

View File

@@ -52,7 +52,7 @@ monitor_speed = 115200
monitor_filters = direct
board_build.flash_mode = dio
board_build.filesystem = littlefs
version = 1.4.0-rc.9
version = 1.4.0-rc.10
; Defaults
[esp8266_defaults]

View File

@@ -5,56 +5,6 @@ class HaHelper : public HomeAssistantHelper {
public:
static const byte TEMP_SOURCE_HEATING = 0;
static const byte TEMP_SOURCE_INDOOR = 1;
bool publishSelectOutdoorSensorType(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set"));
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"sensors\": {\"outdoor\": {\"type\": {% if value == 'Boiler' %}0{% elif value == 'Manual' %}1{% elif value == 'External' %}2{% endif %}}}}");
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("outdoor_sensor_type"));
doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("outdoor_sensor_type"));
doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config");
doc[FPSTR(HA_NAME)] = F("Outdoor temperature source");
doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings"));
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{% if value_json.sensors.outdoor.type == 0 %}Boiler{% elif value_json.sensors.outdoor.type == 1 %}Manual{% elif value_json.sensors.outdoor.type == 2 %}External{% endif %}");
doc[FPSTR(HA_OPTIONS)][0] = F("Boiler");
doc[FPSTR(HA_OPTIONS)][1] = F("Manual");
doc[FPSTR(HA_OPTIONS)][2] = F("External");
doc[FPSTR(HA_EXPIRE_AFTER)] = 120;
doc.shrinkToFit();
return this->publish(this->getTopic(FPSTR(HA_ENTITY_SELECT), F("outdoor_sensor_type")).c_str(), doc);
}
bool publishSelectIndoorSensorType(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set"));
#if USE_BLE
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"sensors\": {\"indoor\": {\"type\": {% if value == 'Manual' %}1{% elif value == 'External' %}2{% elif value == 'Bluetooth' %}3{% endif %}}}}");
#else
doc[FPSTR(HA_COMMAND_TEMPLATE)] = F("{\"sensors\": {\"indoor\": {\"type\": {% if value == 'Manual' %}1{% elif value == 'External' %}2{% endif %}}}}");
#endif
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("indoor_sensor_type"));
doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("indoor_sensor_type"));
doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config");
doc[FPSTR(HA_NAME)] = F("Indoor temperature source");
doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings"));
#if USE_BLE
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{% if value_json.sensors.indoor.type == 1 %}Manual{% elif value_json.sensors.indoor.type == 2 %}External{% elif value_json.sensors.indoor.type == 3 %}Bluetooth{% endif %}");
#else
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{% if value_json.sensors.indoor.type == 1 %}Manual{% elif value_json.sensors.indoor.type == 2 %}External{% endif %}");
#endif
doc[FPSTR(HA_OPTIONS)][0] = F("Manual");
doc[FPSTR(HA_OPTIONS)][1] = F("External");
#if USE_BLE
doc[FPSTR(HA_OPTIONS)][2] = F("Bluetooth");
#endif
doc[FPSTR(HA_EXPIRE_AFTER)] = 120;
doc.shrinkToFit();
return this->publish(this->getTopic(FPSTR(HA_ENTITY_SELECT), F("indoor_sensor_type")).c_str(), doc);
}
bool publishNumberOutdoorSensorOffset(bool enabledByDefault = true) {
JsonDocument doc;
@@ -109,28 +59,6 @@ public:
}
bool publishSwitchDebug(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;
doc[FPSTR(HA_UNIQUE_ID)] = this->getObjectId(F("debug"));
doc[FPSTR(HA_OBJECT_ID)] = this->getObjectId(F("debug"));
doc[FPSTR(HA_ENTITY_CATEGORY)] = F("config");
doc[FPSTR(HA_NAME)] = F("Debug");
doc[FPSTR(HA_ICON)] = F("mdi:code-braces");
doc[FPSTR(HA_STATE_TOPIC)] = this->getDeviceTopic(F("settings"));
doc[FPSTR(HA_STATE_ON)] = true;
doc[FPSTR(HA_STATE_OFF)] = false;
doc[FPSTR(HA_VALUE_TEMPLATE)] = F("{{ value_json.debug }}");
doc[FPSTR(HA_COMMAND_TOPIC)] = this->getDeviceTopic(F("settings/set"));
doc[FPSTR(HA_PAYLOAD_ON)] = F("{\"debug\": true}");
doc[FPSTR(HA_PAYLOAD_OFF)] = F("{\"debug\": false}");
doc[FPSTR(HA_EXPIRE_AFTER)] = 120;
doc.shrinkToFit();
return this->publish(this->getTopic(FPSTR(HA_ENTITY_SWITCH), F("debug")).c_str(), doc);
}
bool publishSwitchEmergency(bool enabledByDefault = true) {
JsonDocument doc;
doc[FPSTR(HA_ENABLED_BY_DEFAULT)] = enabledByDefault;

View File

@@ -4,9 +4,7 @@ extern NetworkTask* tNetwork;
extern MqttTask* tMqtt;
extern OpenThermTask* tOt;
extern FileData fsSettings, fsNetworkSettings;
#if USE_TELNET
extern ESPTelnetStream TelnetStream;
#endif
extern ESPTelnetStream* telnetStream;
class MainTask : public Task {
@@ -36,9 +34,7 @@ protected:
unsigned long heatingDisabledTime = 0;
byte externalPumpStartReason;
unsigned long externalPumpStartTime = 0;
#if USE_TELNET
bool telnetStarted = false;
#endif
const char* getTaskName() {
return "Main";
@@ -76,11 +72,9 @@ protected:
Log.sinfoln(FPSTR(L_NETWORK_SETTINGS), F("Updated"));
}
#if USE_TELNET
if (this->telnetStarted) {
TelnetStream.loop();
telnetStream->loop();
}
#endif
if (vars.actions.restart) {
Log.sinfoln(FPSTR(L_MAIN), F("Restart signal received. Restart after 10 sec."));
@@ -95,12 +89,10 @@ protected:
}
if (tNetwork->isConnected()) {
#if USE_TELNET
if (!this->telnetStarted) {
TelnetStream.begin(23, false);
if (!this->telnetStarted && telnetStream != nullptr) {
telnetStream->begin(23, false);
this->telnetStarted = true;
}
#endif
vars.sensors.rssi = WiFi.RSSI();
@@ -112,20 +104,18 @@ protected:
this->firstFailConnect = 0;
}
if ( Log.getLevel() != TinyLogger::Level::INFO && !settings.debug ) {
if ( Log.getLevel() != TinyLogger::Level::INFO && !settings.system.debug ) {
Log.setLevel(TinyLogger::Level::INFO);
} else if ( Log.getLevel() != TinyLogger::Level::VERBOSE && settings.debug ) {
} else if ( Log.getLevel() != TinyLogger::Level::VERBOSE && settings.system.debug ) {
Log.setLevel(TinyLogger::Level::VERBOSE);
}
} else {
#if USE_TELNET
if (this->telnetStarted) {
TelnetStream.stop();
telnetStream->stop();
this->telnetStarted = false;
}
#endif
if (tMqtt->isEnabled()) {
tMqtt->disable();
@@ -178,7 +168,7 @@ protected:
return;
}
if (!settings.debug) {
if (!settings.system.debug) {
return;
}

View File

@@ -257,7 +257,7 @@ protected:
return;
}
if (settings.debug) {
if (settings.system.debug) {
Log.strace(FPSTR(L_MQTT_MSG), F("Topic: %s\r\n> "), topic);
if (Log.lock()) {
for (size_t i = 0; i < length; i++) {
@@ -328,11 +328,8 @@ protected:
void publishHaEntities() {
// main
this->haHelper->publishSelectOutdoorSensorType();
this->haHelper->publishSelectIndoorSensorType();
this->haHelper->publishNumberOutdoorSensorOffset(false);
this->haHelper->publishNumberIndoorSensorOffset(false);
this->haHelper->publishSwitchDebug(false);
// emergency
this->haHelper->publishSwitchEmergency();

View File

@@ -1,5 +1,5 @@
#define PORTAL_CACHE_TIME "" //"max-age=86400"
#define PORTAL_CACHE settings.debug ? nullptr : PORTAL_CACHE_TIME
#define PORTAL_CACHE settings.system.debug ? nullptr : PORTAL_CACHE_TIME
#ifdef ARDUINO_ARCH_ESP8266
#include <ESP8266WebServer.h>
#include <Updater.h>

View File

@@ -23,7 +23,11 @@ struct NetworkSettings {
} networkSettings;
struct Settings {
bool debug = DEBUG_BY_DEFAULT;
struct {
bool debug = DEBUG_BY_DEFAULT;
bool useSerial = USE_SERIAL;
bool useTelnet = USE_TELNET;
} system;
struct {
bool useAuth = false;

View File

@@ -1,5 +1,5 @@
#define PROJECT_NAME "OpenTherm Gateway"
#define PROJECT_VERSION "1.4.0-rc.9"
#define PROJECT_VERSION "1.4.0-rc.10"
#define PROJECT_REPO "https://github.com/Laxilef/OTGateway"
#define EMERGENCY_TIME_TRESHOLD 120000

View File

@@ -4,14 +4,11 @@
#include <ArduinoJson.h>
#include <FileData.h>
#include <LittleFS.h>
#include "ESPTelnetStream.h"
#include <TinyLogger.h>
#include "Settings.h"
#include <utils.h>
#if USE_TELNET
#include "ESPTelnetStream.h"
#endif
#if defined(ARDUINO_ARCH_ESP32)
#include <ESP32Scheduler.h>
#elif defined(ARDUINO_ARCH_ESP8266)
@@ -33,9 +30,7 @@
// Vars
FileData fsNetworkSettings(&LittleFS, "/network.conf", 'n', &networkSettings, sizeof(networkSettings), 1000);
FileData fsSettings(&LittleFS, "/settings.conf", 's', &settings, sizeof(settings), 60000);
#if USE_TELNET
ESPTelnetStream TelnetStream;
#endif
ESPTelnetStream* telnetStream = nullptr;
// Tasks
NetworkTask* tNetwork;
@@ -63,17 +58,9 @@ void setup() {
return tm{sec, min, hour};
});
#if USE_SERIAL
Serial.begin(115200);
Log.addStream(&Serial);
#endif
#if USE_TELNET
TelnetStream.setKeepAliveInterval(500);
Log.addStream(&TelnetStream);
#endif
Log.print("\n\n\r");
// network settings
@@ -121,8 +108,21 @@ void setup() {
break;
}
Log.setLevel(settings.debug ? TinyLogger::Level::VERBOSE : TinyLogger::Level::INFO);
// logs
if (!settings.system.useSerial) {
Log.clearStreams();
Serial.end();
}
if (settings.system.useTelnet) {
telnetStream = new ESPTelnetStream;
telnetStream->setKeepAliveInterval(500);
Log.addStream(telnetStream);
}
Log.setLevel(settings.system.debug ? TinyLogger::Level::VERBOSE : TinyLogger::Level::INFO);
// tasks
tNetwork = (new NetworkTask(true, 500))
->setHostname(networkSettings.hostname)
->setStaCredentials(

View File

@@ -234,9 +234,11 @@ bool jsonToNetworkSettings(const JsonVariantConst src, NetworkSettings& dst) {
}
void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
dst["debug"] = src.debug;
if (!safe) {
dst["system"]["debug"] = src.system.debug;
dst["system"]["useSerial"] = src.system.useSerial;
dst["system"]["useTelnet"] = src.system.useTelnet;
dst["portal"]["useAuth"] = src.portal.useAuth;
dst["portal"]["login"] = src.portal.login;
dst["portal"]["password"] = src.portal.password;
@@ -318,13 +320,24 @@ void safeSettingsToJson(const Settings& src, JsonVariant dst) {
bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false) {
bool changed = false;
if (src["debug"].is<bool>()) {
dst.debug = src["debug"].as<bool>();
changed = true;
}
if (!safe) {
// system
if (src["system"]["debug"].is<bool>()) {
dst.system.debug = src["system"]["debug"].as<bool>();
changed = true;
}
if (src["system"]["useSerial"].is<bool>()) {
dst.system.useSerial = src["system"]["useSerial"].as<bool>();
changed = true;
}
if (src["system"]["useTelnet"].is<bool>()) {
dst.system.useTelnet = src["system"]["useTelnet"].as<bool>();
changed = true;
}
// portal
if (src["portal"]["useAuth"].is<bool>()) {
dst.portal.useAuth = src["portal"]["useAuth"].as<bool>();