ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
emontx.cpp
Go to the documentation of this file.
1#include "emontx.h"
2#include "esphome/core/log.h"
5
6namespace esphome::emontx {
7
8ESPHOME_LOG_TAG(TAG, "emontx");
9
10static constexpr uint32_t UNLOCK_ID = 0;
11// The emonTx joins bytes received while a command is pending into that command, so the next one waits
12static constexpr uint32_t COMMAND_GAP_MS = 250;
13
14#ifdef USE_EMONTX_VERSION
15static constexpr uint32_t VERSION_REQUEST_ID = 1;
16// Without a reply, 'v' is sent again every VERSION_RETRY_FRAMES data lines, VERSION_MAX_ATTEMPTS times in all
17static constexpr uint8_t VERSION_RETRY_FRAMES = 2;
18static constexpr uint8_t VERSION_MAX_ATTEMPTS = 3;
19// Line of the 'v' reply (and of the boot banner) carrying the release, e.g. " - Version: v1.1.1"
20static const char VERSION_PREFIX[] PROGMEM = " - Version:";
21// Every data line starts with the message counter, in JSON or text format
22static const char MSG_JSON_PREFIX[] PROGMEM = "{\"MSG\":";
23static const char MSG_TEXT_PREFIX[] PROGMEM = "MSG:";
24// First firmware with 'emonlock' and 'emonunlock'; older firmware takes them for the bootloader command
25static constexpr uint32_t LOCK_MIN_MAJOR = 1;
26static constexpr uint32_t LOCK_MIN_MINOR = 2;
27
28// True if line starts with the PROGMEM prefix and has more after it
29static bool starts_with(const char *line, size_t len, const char *prefix, size_t prefix_len) {
30 return len > prefix_len && ESPHOME_strncmp_P(line, prefix, prefix_len) == 0;
31}
32
33// Reads the decimal number (up to 9 digits) at pos and moves pos past it; false if there is none
34static bool read_number(const char *&pos, const char *end, uint32_t &value) {
35 const char *start = pos;
36 value = 0;
37 while (pos < end && pos - start < 9 && *pos >= '0' && *pos <= '9')
38 value = value * 10 + (*pos++ - '0');
39 return pos != start;
40}
41
42// True for a release such as "v1.2.0" or "v1.3.0-rc1" from LOCK_MIN_MAJOR.LOCK_MIN_MINOR on
43static bool supports_lock(const char *version, const char *end) {
44 if (version < end && (*version == 'v' || *version == 'V'))
45 version++;
46 uint32_t major;
47 if (!read_number(version, end, major) || version == end || *version != '.')
48 return false;
49 version++;
50 uint32_t minor;
51 if (!read_number(version, end, minor))
52 return false;
53 return major > LOCK_MIN_MAJOR || (major == LOCK_MIN_MAJOR && minor >= LOCK_MIN_MINOR);
54}
55#endif
56
57void EmonTx::setup() {
59 // The line feed ends any boot noise
60 this->write_byte('\n');
61 this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
62 this->unlock_();
63#ifdef USE_EMONTX_VERSION
64 if (this->wants_version_())
65 this->request_version_();
66#endif
67 });
68 return;
69 }
70#ifdef USE_EMONTX_VERSION
71 // With AUTO, the reply decides whether to unlock
72 if (this->wants_version_())
73 this->request_version_();
74#endif
75}
76
78 ESP_LOGI(TAG, "Unlocking emonTx");
79 this->send_command("emonunlock");
80}
81
82void EmonTx::on_shutdown() {
84#ifdef USE_EMONTX_VERSION
86#endif
87 if (!lock)
88 return;
89 if (this->is_paused()) {
90 ESP_LOGW(TAG, "Not locking emonTx before shutdown, UART is paused");
91 return;
92 }
93 ESP_LOGI(TAG, "Locking emonTx before shutdown");
94 this->send_command("emonlock");
95 this->flush();
96}
97
105void EmonTx::loop() {
106 if (this->is_paused()) {
107 this->was_paused_ = true;
108 return;
109 }
110 if (this->was_paused_) {
111 // Drop what arrived during the pause and resync at the next line start
112 this->was_paused_ = false;
113 uint8_t dropped = 0;
114 this->read_available([&dropped](uint8_t byte) { dropped = byte; });
115 if (dropped == '\n') {
116 this->line_reader_.reset();
117 } else {
119 }
120#ifdef USE_EMONTX_VERSION
121 // The emonTx may have been flashed, or a request dropped, during the pause
122 if (this->wants_version_())
123 this->request_version_();
124#endif
125 }
126
127 // Read all available data to prevent UART buffer overflow
128 this->read_available([this](uint8_t received) {
129 size_t len = 0;
130 char *line = this->line_reader_.feed(received, len);
132 if (line == nullptr || len == 0) {
133 return;
134 }
135 ESP_LOGD(TAG, "Received line: %s", line);
136 this->data_callbacks_.call(StringRef(line, len));
137 if (line[0] == '{') {
138 ESP_LOGV(TAG, "Line is JSON, parsing...");
139 this->parse_json_(line, len);
140 }
141#ifdef USE_EMONTX_VERSION
142 if (this->wants_version_()) {
143 this->parse_msg_number_(line, len);
144 this->parse_version_line_(line, len);
145 }
146#endif
147 });
148}
149
150void EmonTx::parse_json_(const char *data, size_t len) {
151 bool success = json::parse_json(reinterpret_cast<const uint8_t *>(data), len, [this, data, len](JsonObject root) {
152#ifdef USE_SENSOR
153 for (auto &sensor_pair : this->sensors_) {
154 auto val = root[sensor_pair.first];
155 if (val.is<JsonVariant>()) {
156 float value = val;
157 ESP_LOGV(TAG, "Updating sensor '%s' with value: %.2f", sensor_pair.first, value);
158 sensor_pair.second->publish_state(value);
159 }
160 }
161#endif
162
163 this->json_callbacks_.call(root, StringRef(data, len));
164 return true;
165 });
166
167 if (!success) {
168 ESP_LOGW(TAG, "Failed to parse JSON");
169 }
170}
171
172#ifdef USE_EMONTX_VERSION
174 // The firmware may have changed; until the reply, an unknown version gets no lock commands
175 this->lock_supported_ = false;
176 this->version_frames_left_ = VERSION_RETRY_FRAMES * VERSION_MAX_ATTEMPTS;
177 this->send_version_request_();
178}
179
180// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
182 if (this->is_paused())
183 return;
184 this->write_byte('\n');
185 this->set_timeout(VERSION_REQUEST_ID, COMMAND_GAP_MS, [this]() { this->send_command("v"); });
186}
187
188void EmonTx::parse_msg_number_(const char *line, size_t len) {
189 const char *pos;
190 if (starts_with(line, len, MSG_JSON_PREFIX, sizeof(MSG_JSON_PREFIX) - 1)) {
191 pos = line + sizeof(MSG_JSON_PREFIX) - 1;
192 } else if (starts_with(line, len, MSG_TEXT_PREFIX, sizeof(MSG_TEXT_PREFIX) - 1)) {
193 pos = line + sizeof(MSG_TEXT_PREFIX) - 1;
194 } else {
195 return;
196 }
197 uint32_t msg_number;
198 if (!read_number(pos, line + len, msg_number))
199 return;
200
201 // MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
202 if (msg_number <= this->last_msg_number_) {
203 this->request_version_();
204 } else if (this->version_frames_left_ > 0) {
205 if (--this->version_frames_left_ == 0) {
206 ESP_LOGW(TAG, "No firmware version from the emonTx, it may still be locked");
207 } else if (this->version_frames_left_ % VERSION_RETRY_FRAMES == 0) {
208 this->send_version_request_();
209 }
210 }
211 this->last_msg_number_ = msg_number;
212}
213
214void EmonTx::parse_version_line_(const char *line, size_t len) {
215 constexpr size_t prefix_len = sizeof(VERSION_PREFIX) - 1;
216 if (!starts_with(line, len, VERSION_PREFIX, prefix_len))
217 return;
218 const char *version = line + prefix_len;
219 const char *end = line + len;
220 while (version < end && *version == ' ')
221 version++;
222 if (version == end)
223 return;
224 ESP_LOGI(TAG, "emonTx firmware version: %.*s", static_cast<int>(end - version), version);
225 this->version_frames_left_ = 0;
226#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
227 if (this->version_text_sensor_ != nullptr)
228 this->version_text_sensor_->publish_state(version, end - version);
229#endif
230
231 bool supported = supports_lock(version, end);
232 if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && supported && !this->unlock_sent_) {
233 // Once the rest of the reply has gone out; a pause drops it, and the version request on resume retries
234 this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
235 if (this->is_paused())
236 return;
237 this->unlock_();
238 this->unlock_sent_ = true;
239 });
240 }
241 this->lock_supported_ = supported;
242}
243#endif
244
248void EmonTx::dump_config() {
249 ESP_LOGCONFIG(TAG, "EmonTx:");
250 const char *lock = LOG_STR_LITERAL("no");
252 lock = LOG_STR_LITERAL("yes");
254 lock = LOG_STR_LITERAL("auto");
255 }
256 ESP_LOGCONFIG(TAG, " Lock on reboot: %s", lock);
257
258#ifdef USE_SENSOR
259 ESP_LOGCONFIG(TAG, " Registered sensors: %zu", this->sensors_.size());
260 for (const auto &sensor_pair : this->sensors_) {
261 ESP_LOGCONFIG(TAG, " Sensor: %s", sensor_pair.first);
262 }
263#else
264 ESP_LOGCONFIG(TAG, " Sensor support: DISABLED");
265#endif
266#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
267 LOG_TEXT_SENSOR(" ", "Firmware version", this->version_text_sensor_);
268#endif
269}
270
276void EmonTx::send_command(const char *command) {
277 if (this->is_paused()) {
278 ESP_LOGW(TAG, "Not sending command, UART is paused: %s", command);
279 return;
280 }
281 ESP_LOGD(TAG, "Sending command to emonTx: %s", command);
282 this->write_str(command);
283 this->write_byte('\n');
284}
285
286#ifdef USE_SENSOR
293void EmonTx::register_sensor(const char *tag_name, sensor::Sensor *sensor) {
294 ESP_LOGCONFIG(TAG, "Registering sensor for tag: %s", tag_name);
295 this->sensors_.emplace_back(tag_name, sensor);
296}
297#endif
298
299} // namespace esphome::emontx
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
char * feed(uint8_t c)
Feed one byte; returns the completed line (empty lines included), valid until the next call,...
Definition line_reader.h:23
void reset()
Forget the partial line and start a new one with the next byte.
Definition line_reader.h:76
void log_if_overflowed(const char *tag) const
Log the shared "line dropped" warning when the last feed() overflowed.
Definition line_reader.h:69
void discard_line()
Drop everything up to and including the next terminator.
Definition line_reader.h:85
StringRef is a reference to a string owned by something else.
Definition string_ref.h:31
FixedVector< std::pair< const char *, sensor::Sensor * > > sensors_
Definition emontx.h:91
LineReader< MAX_LINE_LENGTH+1 > line_reader_
Definition emontx.h:104
LockOnReboot lock_on_reboot_
Definition emontx.h:97
bool wants_version_() const
Definition emontx.h:77
void parse_msg_number_(const char *line, size_t len)
Definition emontx.cpp:188
LazyCallbackManager< void(JsonObject, StringRef)> json_callbacks_
Definition emontx.h:93
uint32_t last_msg_number_
Definition emontx.h:102
LazyCallbackManager< void(StringRef)> data_callbacks_
Definition emontx.h:94
uint8_t version_frames_left_
Definition emontx.h:101
void parse_json_(const char *data, size_t len)
Definition emontx.cpp:150
void parse_version_line_(const char *line, size_t len)
Definition emontx.cpp:214
UARTFlushResult flush()
Definition uart.h:96
void write_str(const char *str)
Definition uart.h:37
void write_byte(uint8_t data)
Definition uart.h:22
bool read_available(F &&on_byte)
Same as read_available_chunks(), calling on_byte(uint8_t) once per byte.
Definition uart.h:77
mopeka_std_values val[3]
ESPHOME_LOG_TAG(TAG, "emontx")
bool parse_json(const std::string &data, const json_parse_t &f)
Parse a JSON string and run the provided json parse function if it's valid.
Definition json_util.cpp:28
const void size_t len
Definition hal.h:68
size_t size_t pos
Definition helpers.h:1255
static void uint32_t
uint8_t end[39]
Definition sun_gtil2.cpp:17
SemaphoreHandle_t lock