[emontx] Read the emonTx firmware version (#20619)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Frédéric Metrich
2026-10-12 22:43:02 -10:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 25843c5242
commit 77c9fa2fa4
7 changed files with 235 additions and 6 deletions
+65 -6
View File
@@ -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
View File
@@ -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")
+1
View File
@@ -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
+6
View File
@@ -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