Skip to content

Commit

Permalink
settings: switch to preact, aupdate mqtt preset
Browse files Browse the repository at this point in the history
  • Loading branch information
dentra committed Aug 9, 2024
1 parent 0c4fd8b commit cdfe216
Show file tree
Hide file tree
Showing 8 changed files with 618 additions and 390 deletions.
78 changes: 63 additions & 15 deletions components/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@
import esphome.config_validation as cv
from esphome import core
from esphome.components import web_server, web_server_base
from esphome.const import CONF_ID, CONF_LAMBDA
from esphome.const import (
CONF_AUTH,
CONF_ID,
CONF_LAMBDA,
CONF_PASSWORD,
CONF_USERNAME,
)

from . import const, cpp, presets, var

CODEOWNERS = ["@dentra"]
AUTO_LOAD = ["web_server_base", "json", "nvs"]
AUTO_LOAD = ["web_server_base", "json", "nvs", "dtu"]
DEPENDENCIES = ["wifi"]

CONF_BASE_URL = "base_url"

CONF_SETTINGS_VARIABLES = "settings_variables"
CONF_VARIABLES = "variables"
Expand Down Expand Up @@ -162,13 +169,46 @@ def variable_shorthand(config):
cv.All(VARIABLE_DATA, variable_shorthand),
)

CONFIG_SCHEMA = cv.Schema(

def _web_menu_schema(config):
schema = cv.Schema(config)
try:
from esphome.components import web_menu

schema = schema.extend(
{cv.GenerateID(web_menu.CONF_WEB_MENU_ID): cv.use_id(web_menu.WebMenu)}
)
except ImportError:
pass

return schema


async def _web_menu_add_item(config, var, name):
try:
from esphome.components import web_menu

if menu := await web_menu.get_web_menu_variable(config):
cg.add(menu.add_item(var.get_base_url(), name))
cg.add(var.set_menu_url(menu.get_base_url()))
except ImportError:
pass


CONFIG_SCHEMA = _web_menu_schema(
{
cv.GenerateID(): cv.declare_id(cpp.Settings),
cv.GenerateID(web_server_base.CONF_WEB_SERVER_BASE_ID): cv.use_id(
web_server_base.WebServerBase,
),
cv.Required(CONF_VARIABLES): cv.Schema(
cv.Optional(CONF_BASE_URL): cv.string_strict,
cv.Optional(CONF_AUTH): cv.Schema(
{
cv.Required(CONF_USERNAME): cv.All(cv.string_strict, cv.Length(min=1)),
cv.Required(CONF_PASSWORD): cv.All(cv.string_strict, cv.Length(min=1)),
}
),
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{
cv.valid_name: cv.All(
VARIABLE_SCHEMA,
Expand All @@ -184,14 +224,17 @@ def variable_shorthand(config):
)


def _add_resource(filename: str, resurce_name: str = ""):
if not resurce_name:
resurce_name = filename.replace(".", "_").upper()
path = f"{os.path.dirname(__file__)}/{filename}"
with open(file=path, encoding="utf-8") as file:
web_server.add_resource_as_progmem(resurce_name, file.read())


# Run with low priority so that all initilization be doing first
@core.coroutine_with_priority(-200.0)
@core.coroutine_with_priority(-1000.0)
async def to_code(config):
# for key, val in CORE.config["mqtt"].items():
# print("to_code", key, val)
# # for k, v in val.items():
# # print(" ", k, v)

presets.presets_init(config.get(CONF_PRESETS), config[CONF_VARIABLES])

web = await cg.get_variable(config[web_server_base.CONF_WEB_SERVER_BASE_ID])
Expand All @@ -204,9 +247,14 @@ async def to_code(config):
# last step, loading settings
await cpp.add_on_load(settings, vars, config.get(CONF_LAMBDA, None))

path = f"{os.path.dirname(__file__)}/settings.html"
with open(file=path, encoding="utf-8") as html_file:
web_server.add_resource_as_progmem("SETTINGS_HTML", html_file.read())
_add_resource("settings.html")
_add_resource("settings.js")

if CONF_BASE_URL in config:
cg.add(settings.set_base_url(config[CONF_BASE_URL]))

await _web_menu_add_item(config, settings, "Settings")

if 'web_server' not in core.CORE.config:
cg.add(settings.set_base_url("/"))
if CONF_AUTH in config:
cg.add(settings.set_username(config[CONF_AUTH][CONF_USERNAME]))
cg.add(settings.set_password(config[CONF_AUTH][CONF_PASSWORD]))
4 changes: 3 additions & 1 deletion components/settings/cpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ async def _setter(v: var.Var, nvs: cg.MockObj):
sav = cg.MockObj("sav")

if not callable(setter):
lam: core.Lambda = await cg.process_lambda(setter, [])
lam: cpp.LambdaExpression = await cg.process_lambda(
setter, parameters=[], capture=""
)
if len(lam.content.strip()) == 0:
return None
setter = cg.RawStatement(lam.content)
Expand Down
107 changes: 107 additions & 0 deletions components/settings/mqtt_component_accessor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#pragma once

#include "esphome/core/defines.h"
#ifdef USE_MQTT

#include "esphome/core/application.h"
#include "esphome/components/mqtt/mqtt_client.h"

#include "esphome/components/dtu/dtu.h"

namespace esphome {
namespace settings {
class MQTTClientComponentAccessor : public mqtt::MQTTClientComponent {
public:
MQTTClientComponentAccessor() = delete;

static MQTTClientComponentAccessor *make(mqtt::MQTTClientComponent *ref) {
return reinterpret_cast<MQTTClientComponentAccessor *>(ref);
}

const std::string topic_prefix() const {
auto s = this->get_topic_prefix();
if (!s.empty() && App.is_name_add_mac_suffix_enabled()) {
auto suffix = dtu::get_mac_suffix();
if (str_endswith(s, suffix)) {
s.erase(s.size() - suffix.size());
}
}
return s;
}
void topic_prefix(const std::string &topic_prefix) {
auto s = dtu::str_trim(topic_prefix, trim_pred_);
if (!s.empty() && App.is_name_add_mac_suffix_enabled()) {
auto suffix = dtu::get_mac_suffix();
if (!str_endswith(s, suffix)) {
s.append(suffix);
}
}
this->set_topic_prefix(s);
}

const std::string log_message_topic() const { return this->unprefix_(this->log_message_.topic); }
void log_message_topic(const std::string &topic) {
this->set_log_message_template(this->mk_topic_(topic, this->log_message_));
}

const std::string last_will_topic() const { return this->unprefix_(this->last_will_.topic); }
void last_will_topic(const std::string &topic) { this->set_last_will(this->mk_topic_(topic, this->last_will_)); }

const std::string birth_message_topic() const { return this->unprefix_(this->birth_message_.topic); }
void birth_message_topic(const std::string &topic) {
this->set_birth_message(this->mk_topic_(topic, this->birth_message_));
}

const std::string shutdown_message_topic() const { return this->unprefix_(this->shutdown_message_.topic); }
void shutdown_message_topic(const std::string &topic) {
this->set_shutdown_message(this->mk_topic_(topic, this->shutdown_message_));
}

protected:
inline static bool trim_pred_(const char c) { return std::isspace(c) || c == '/'; }

const std::string unprefix_(const std::string &topic) const {
auto s = topic;
if (!s.empty()) {
auto prefix = this->get_topic_prefix();
prefix.append(1, '/');
if (str_startswith(s, prefix)) {
s.erase(0, prefix.length());
} else {
s.insert(s.cbegin(), '/');
}
}
return s;
}

// make topic with appending topic prefix.
mqtt::MQTTMessage mk_topic_(const std::string &topic, const mqtt::MQTTMessage &src) {
auto s = dtu::str_rtrim(topic, trim_pred_);
dtu::str_ltrim_ref(s); // trim only spaces to check slash later
if (!s.empty()) {
// check that user wants his own prefix
if (s.front() == '/') {
dtu::str_ltrim_ref(s, trim_pred_);
} else {
s.insert(s.cbegin(), '/');
s.insert(0, this->get_topic_prefix());
}
}

return mqtt::MQTTMessage{
.topic = s,
.payload = src.payload,
.qos = src.qos,
.retain = src.retain,
};
}
};

inline MQTTClientComponentAccessor *mqtt_access(mqtt::MQTTClientComponent *ref) {
return MQTTClientComponentAccessor::make(ref);
}

} // namespace settings
} // namespace esphome

#endif
47 changes: 45 additions & 2 deletions components/settings/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,47 @@
},
"mqtt_topic_prefix": {
const.CONF_VAR_TYPE: var.VT_STR,
const.CONF_VAR_GETTER: lambda _, o: o.get_topic_prefix(),
const.CONF_VAR_SETTER: lambda _, o: o.set_topic_prefix,
const.CONF_VAR_HELP: "The prefix used for all MQTT messages. Set to empty to disable publishing or subscribing of any MQTT topic unless it is explicitly configured",
const.CONF_VAR_GETTER: lambda _, o: _mqtt_access(o).topic_prefix(),
const.CONF_VAR_SETTER: lambda _, o: _mqtt_access(o).topic_prefix,
},
"mqtt_birth_message": {
const.CONF_VAR_TYPE: var.VT_STR,
const.CONF_VAR_HELP: "The message to send when a connection to the broker is established",
const.CONF_VAR_GETTER: lambda _, o: _mqtt_access(
o
).birth_message_topic(),
const.CONF_VAR_SETTER: lambda _, o: _mqtt_access(
o
).birth_message_topic,
},
"mqtt_will_message": {
const.CONF_VAR_TYPE: var.VT_STR,
const.CONF_VAR_HELP: "The message to send when the MQTT connection is dropped",
const.CONF_VAR_GETTER: lambda _, o: _mqtt_access(
o
).last_will_topic(),
const.CONF_VAR_SETTER: lambda _, o: _mqtt_access(o).last_will_topic,
},
"mqtt_shutdown_message": {
const.CONF_VAR_TYPE: var.VT_STR,
const.CONF_VAR_HELP: "The message to send when the node shuts down and the connection is closed cleanly",
const.CONF_VAR_GETTER: lambda _, o: _mqtt_access(
o,
).shutdown_message_topic(),
const.CONF_VAR_SETTER: lambda _, o: _mqtt_access(
o
).shutdown_message_topic,
},
"mqtt_log_topic": {
const.CONF_VAR_TYPE: var.VT_STR,
const.CONF_VAR_HELP: " The topic to send MQTT log messages to. Set to empty for disable logging",
const.CONF_VAR_GETTER: lambda _, o: _mqtt_access(
o
).log_message_topic(),
const.CONF_VAR_SETTER: lambda _, o: _mqtt_access(
o
).log_message_topic,
},
"mqtt_reboot_timeout": {
const.CONF_VAR_TYPE: var.VT_TIMEOUT,
Expand Down Expand Up @@ -344,6 +383,10 @@ def _wifi_network_setter(index: int, c: dict, wifi_comp: cg.MockObj) -> cg.State
return cgp.CodeBlock(None, None, ss)


def _mqtt_access(mqtt_comp: cg.MockObj):
return cg.MockObj("settings::mqtt_access", "->")(mqtt_comp)


def _get_presets(cpresets: list[str]) -> OrderedDict[str, Any]:
presets = OrderedDict()

Expand Down
39 changes: 31 additions & 8 deletions components/settings/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ bool Settings::canHandle(AsyncWebServerRequest *request) {
void Settings::handleRequest(AsyncWebServerRequest *request) { // NOLINT(readability-non-const-parameter)
SETTINGS_TRACE(TAG, "Handle request method %u, url: %s", request->method(), request->url().c_str());

if (!request->authenticate(this->username_, this->password_)) {
return request->requestAuthentication();
}

if (request->method() == HTTP_POST) {
if (request->url() == this->url_("reset")) {
this->handle_reset_(request);
Expand All @@ -66,9 +70,15 @@ void Settings::handleRequest(AsyncWebServerRequest *request) { // NOLINT(readab
}

if (request->url() == this->url_("settings.json")) {
this->handle_load_(request);
this->handle_json_(request);
return;
}

if (request->url() == this->url_("settings.js")) {
this->handle_js_(request);
return;
}

#ifdef USE_ARDUINO
// arduino returns String but not std::string
if (!request->url().endsWith("/"))
Expand All @@ -80,10 +90,17 @@ void Settings::handleRequest(AsyncWebServerRequest *request) { // NOLINT(readab
return;
}

this->handle_base_(request);
this->handle_html_(request);
}

void Settings::handle_js_(AsyncWebServerRequest *request) { // NOLINT(readability-non-const-parameter)
auto *response = request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_SETTINGS_JS,
ESPHOME_WEBSERVER_SETTINGS_JS_SIZE);
response->addHeader("Content-Encoding", "gzip");
request->send(response);
}

void Settings::handle_base_(AsyncWebServerRequest *request) { // NOLINT(readability-non-const-parameter)
void Settings::handle_html_(AsyncWebServerRequest *request) { // NOLINT(readability-non-const-parameter)
auto *response =
request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_SETTINGS_HTML, ESPHOME_WEBSERVER_SETTINGS_HTML_SIZE);
response->addHeader("Content-Encoding", "gzip");
Expand Down Expand Up @@ -144,12 +161,13 @@ std::string Settings::get_json_value_(const VarInfo &v) const {
}
}

void Settings::handle_load_(AsyncWebServerRequest *request) { // NOLINT(readability-non-const-parameter)
void Settings::handle_json_(AsyncWebServerRequest *request) { // NOLINT(readability-non-const-parameter)
SETTINGS_TRACE(TAG, "Handle json...");
this->nvs_.open(NVS_NS);
auto s = JsonWriter(request->beginResponseStream("application/json"));
s.start_object();
s.add_kv("t", App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name());
s.add_kv("m", this->menu_url_);
s.start_array("v");
bool is_first = true; // NOLINT(misc-const-correctness)
for (auto const &x : this->items_) {
Expand Down Expand Up @@ -262,7 +280,7 @@ bool Settings::save_pv_(const VarInfo &x, const char *param) const {
}

void Settings::handle_save_(AsyncWebServerRequest *request) { // NOLINT(readability-non-const-parameter)
SETTINGS_TRACE(TAG, "Saving changes...");
ESP_LOGD(TAG, "Saving changes...");
this->nvs_.open(NVS_NS, true);

size_t changes = 0;
Expand Down Expand Up @@ -290,13 +308,18 @@ void Settings::handle_save_(AsyncWebServerRequest *request) { // NOLINT(readabi
}

void Settings::handle_reset_(AsyncWebServerRequest *request) { // NOLINT(readability-non-const-parameter)
global_preferences->reset();
this->reboot_();
ESP_LOGD(TAG, "Resetting device to factory...");

this->redirect_home_(request);

if (request->arg("confirm") == "on") {
global_preferences->reset();
this->reboot_();
}
}

void Settings::reboot_() {
this->set_timeout(1000, []() { App.safe_reboot(); });
this->set_timeout(100, []() { App.safe_reboot(); });
}

void Settings::load(void (*on_load)(const nvs_flash::NvsFlash &nvs)) {
Expand Down
Loading

0 comments on commit cdfe216

Please sign in to comment.