mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[emontx] Read the emonTx firmware version (#20619)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
25843c5242
commit
77c9fa2fa4
@@ -1,19 +1,32 @@
|
||||
#include "emontx.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/components/json/json_util.h"
|
||||
|
||||
namespace esphome::emontx {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "emontx");
|
||||
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
static constexpr uint32_t VERSION_REQUEST_ID = 0;
|
||||
// The emonTx joins bytes received while a command is pending into that command, so 'v' waits
|
||||
static constexpr uint32_t VERSION_REQUEST_DELAY_MS = 250;
|
||||
// Line of the 'v' reply (and of the boot banner) carrying the release, e.g. " - Version: v1.1.1"
|
||||
static const char VERSION_PREFIX[] PROGMEM = " - Version:";
|
||||
#endif
|
||||
|
||||
void EmonTx::setup() {
|
||||
this->buffer_pos_ = 0;
|
||||
if (!this->lock_on_reboot_)
|
||||
return;
|
||||
// The line feed ends any boot noise first
|
||||
ESP_LOGI(TAG, "Unlocking emonTx after boot");
|
||||
this->write_byte('\n');
|
||||
this->send_command("emonunlock");
|
||||
if (this->lock_on_reboot_) {
|
||||
// The line feed ends any boot noise first
|
||||
ESP_LOGI(TAG, "Unlocking emonTx after boot");
|
||||
this->write_byte('\n');
|
||||
this->send_command("emonunlock");
|
||||
}
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
this->request_version_();
|
||||
#endif
|
||||
}
|
||||
|
||||
void EmonTx::on_shutdown() {
|
||||
@@ -48,6 +61,11 @@ void EmonTx::loop() {
|
||||
uint8_t dropped;
|
||||
while (this->available() > 0 && this->read_byte(&dropped))
|
||||
this->discarding_line_ = dropped != '\n';
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
// The emonTx may have been flashed, or a request dropped, during the pause
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
this->request_version_();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Read all available data to prevent UART buffer overflow
|
||||
@@ -77,6 +95,10 @@ void EmonTx::loop() {
|
||||
ESP_LOGV(TAG, "Line is JSON, parsing...");
|
||||
this->parse_json_(this->buffer_.data(), len);
|
||||
}
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
this->parse_version_line_(this->buffer_.data(), len);
|
||||
#endif
|
||||
}
|
||||
} else if (received == '\r' || this->discarding_line_) {
|
||||
// Ignore CR, and the rest of a line that overflowed or was interrupted by a pause
|
||||
@@ -102,6 +124,16 @@ void EmonTx::parse_json_(const char *data, size_t len) {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
auto msg = root["MSG"];
|
||||
if (this->version_text_sensor_ != nullptr && msg.is<uint32_t>()) {
|
||||
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
|
||||
uint32_t msg_number = msg;
|
||||
if (msg_number <= this->last_msg_number_)
|
||||
this->request_version_();
|
||||
this->last_msg_number_ = msg_number;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->json_callbacks_.call(root, StringRef(data, len));
|
||||
return true;
|
||||
@@ -112,6 +144,30 @@ void EmonTx::parse_json_(const char *data, size_t len) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
|
||||
void EmonTx::request_version_() {
|
||||
if (this->is_paused())
|
||||
return;
|
||||
this->write_byte('\n');
|
||||
this->set_timeout(VERSION_REQUEST_ID, VERSION_REQUEST_DELAY_MS, [this]() { this->send_command("v"); });
|
||||
}
|
||||
|
||||
void EmonTx::parse_version_line_(const char *line, size_t len) {
|
||||
constexpr size_t prefix_len = sizeof(VERSION_PREFIX) - 1;
|
||||
if (len <= prefix_len || ESPHOME_strncmp_P(line, VERSION_PREFIX, prefix_len) != 0)
|
||||
return;
|
||||
const char *version = line + prefix_len;
|
||||
const char *end = line + len;
|
||||
while (version < end && *version == ' ')
|
||||
version++;
|
||||
if (version == end)
|
||||
return;
|
||||
ESP_LOGI(TAG, "emonTx firmware version: %.*s", static_cast<int>(end - version), version);
|
||||
this->version_text_sensor_->publish_state(version, end - version);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Logs the EmonTx component configuration details.
|
||||
*/
|
||||
@@ -127,6 +183,9 @@ void EmonTx::dump_config() {
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, " Sensor support: DISABLED");
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
LOG_TEXT_SENSOR(" ", "Firmware version", this->version_text_sensor_);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::emontx {
|
||||
|
||||
@@ -27,6 +30,9 @@ static constexpr size_t MAX_LINE_LENGTH = 1024;
|
||||
* extracts tags and values, and publishes them to registered sensors.
|
||||
*/
|
||||
class EmonTx final : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
SUB_TEXT_SENSOR(version)
|
||||
#endif
|
||||
public:
|
||||
EmonTx() = default;
|
||||
|
||||
@@ -60,6 +66,10 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
|
||||
protected:
|
||||
void parse_json_(const char *data, size_t len);
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
void request_version_();
|
||||
void parse_version_line_(const char *line, size_t len);
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
FixedVector<std::pair<const char *, sensor::Sensor *>> sensors_{};
|
||||
@@ -71,6 +81,9 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
bool was_paused_{false};
|
||||
bool lock_on_reboot_{false};
|
||||
bool discarding_line_{false}; // Set after an overflow or a pause until the end of that line
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
uint32_t last_msg_number_{0};
|
||||
#endif
|
||||
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_VERSION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_EMONTX_ID, EmonTx
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_EMONTX_ID): cv.use_id(EmonTx),
|
||||
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_EMONTX_ID])
|
||||
text_sensors = text_sensor.sub_text_sensors(config)
|
||||
if await text_sensors(CONF_VERSION, hub.set_version_text_sensor) is not None:
|
||||
cg.add_define("USE_EMONTX_VERSION")
|
||||
@@ -68,6 +68,7 @@
|
||||
#define USE_DEVICES
|
||||
#define USE_DISPLAY
|
||||
#define DISPLAY_PAGE_CHANGE_CALLBACK_COUNT 1
|
||||
#define USE_EMONTX_VERSION
|
||||
#define USE_ENTITY_DEVICE_CLASS
|
||||
#define USE_ENTITY_ICON
|
||||
#define USE_ENTITY_UNIT_OF_MEASUREMENT
|
||||
|
||||
@@ -24,3 +24,9 @@ sensor:
|
||||
tag_name: P1
|
||||
emontx_id: test_emontx
|
||||
unit_of_measurement: W
|
||||
|
||||
text_sensor:
|
||||
- platform: emontx
|
||||
emontx_id: test_emontx
|
||||
version:
|
||||
name: emonTx firmware version
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The platform's own to_code needs a configured hub; only its define is wanted here.
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
cg.add_define("USE_EMONTX_VERSION")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -0,0 +1,116 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "esphome/components/emontx/emontx.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::emontx::testing {
|
||||
|
||||
namespace {
|
||||
class EmonTxVersionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// The test main does not construct App as generated code does
|
||||
static const bool APP_CONSTRUCTED = (new (&App) Application(), true);
|
||||
(void) APP_CONSTRUCTED;
|
||||
this->sut_.set_uart_parent(&this->uart_);
|
||||
this->sut_.set_version_text_sensor(&this->version_);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
App.scheduler.cancel_timeout(&this->sut_, uint32_t{0});
|
||||
App.scheduler.call(millis());
|
||||
}
|
||||
|
||||
// Lets the delayed 'v' go out
|
||||
static void run_timers() {
|
||||
App.scheduler.call(millis());
|
||||
delay(300); // NOLINT - test only, the scheduler reads the real clock
|
||||
App.scheduler.call(millis());
|
||||
}
|
||||
|
||||
std::string tx_() const { return std::string(this->uart_.tx.begin(), this->uart_.tx.end()); }
|
||||
|
||||
MockUARTComponent uart_;
|
||||
EmonTx sut_;
|
||||
text_sensor::TextSensor version_;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
// 'v' must not reach the emonTx in the same burst as the line feed, or it joins the pending command
|
||||
TEST_F(EmonTxVersionTest, SetupRequestsVersionAfterDelay) {
|
||||
this->sut_.setup();
|
||||
EXPECT_EQ(this->tx_(), "\n");
|
||||
|
||||
run_timers();
|
||||
EXPECT_EQ(this->tx_(), "\nv\n");
|
||||
}
|
||||
|
||||
TEST_F(EmonTxVersionTest, SetupUnlocksBeforeRequestingVersion) {
|
||||
this->sut_.set_lock_on_reboot(true);
|
||||
this->sut_.setup();
|
||||
EXPECT_EQ(this->tx_(), "\nemonunlock\n\n");
|
||||
|
||||
run_timers();
|
||||
EXPECT_EQ(this->tx_(), "\nemonunlock\n\nv\n");
|
||||
}
|
||||
|
||||
TEST_F(EmonTxVersionTest, VersionLinePublished) {
|
||||
this->uart_.push_rx("> Firmware:\r\n - Bootloader: UART (.bin)\r\n - Version: v1.1.1\r\n");
|
||||
this->sut_.loop();
|
||||
|
||||
ASSERT_TRUE(this->version_.has_state());
|
||||
EXPECT_EQ(this->version_.get_state(), "v1.1.1");
|
||||
}
|
||||
|
||||
TEST_F(EmonTxVersionTest, OtherLinesNotPublished) {
|
||||
this->uart_.push_rx(" - Build: 768be1b (Default)\r\n{\"P1\":100}\r\n - Version:\r\n - Version: \r\n");
|
||||
this->sut_.loop();
|
||||
|
||||
EXPECT_FALSE(this->version_.has_state());
|
||||
}
|
||||
|
||||
// MSG restarts at 1 after a reboot, e.g. a firmware update
|
||||
TEST_F(EmonTxVersionTest, MsgCounterResetRequestsVersion) {
|
||||
this->uart_.push_rx("{\"MSG\":41}\n{\"MSG\":42}\n");
|
||||
this->sut_.loop();
|
||||
EXPECT_TRUE(this->uart_.tx.empty());
|
||||
|
||||
this->uart_.push_rx("{\"MSG\":1}\n");
|
||||
this->sut_.loop();
|
||||
EXPECT_EQ(this->tx_(), "\n");
|
||||
|
||||
run_timers();
|
||||
EXPECT_EQ(this->tx_(), "\nv\n");
|
||||
}
|
||||
|
||||
// The emonTx may have been flashed during the pause, with MSG already past its old value
|
||||
TEST_F(EmonTxVersionTest, ResumeRequestsVersion) {
|
||||
this->sut_.set_paused(true);
|
||||
this->sut_.loop();
|
||||
this->sut_.set_paused(false);
|
||||
this->sut_.loop();
|
||||
EXPECT_EQ(this->tx_(), "\n");
|
||||
|
||||
run_timers();
|
||||
EXPECT_EQ(this->tx_(), "\nv\n");
|
||||
}
|
||||
|
||||
TEST_F(EmonTxVersionTest, RequestDroppedByPauseSentAgainOnResume) {
|
||||
this->sut_.setup();
|
||||
this->sut_.set_paused(true);
|
||||
this->sut_.loop();
|
||||
run_timers();
|
||||
EXPECT_EQ(this->tx_(), "\n");
|
||||
|
||||
this->sut_.set_paused(false);
|
||||
this->sut_.loop();
|
||||
run_timers();
|
||||
EXPECT_EQ(this->tx_(), "\n\nv\n");
|
||||
}
|
||||
|
||||
} // namespace esphome::emontx::testing
|
||||
Reference in New Issue
Block a user