Merge remote-tracking branch 'upstream/fix-ble-client-disconnect-race' into integration

This commit is contained in:
J. Nick Koston
2026-03-10 21:30:51 -10:00
32 changed files with 442 additions and 227 deletions
+2 -3
View File
@@ -91,11 +91,10 @@ void DaikinArcClimate::transmit_state() {
remote_state[5] = this->operation_mode_() | 0x08;
remote_state[6] = this->temperature_();
remote_state[7] = this->humidity_();
static uint8_t last_humidity = 0x66;
if (remote_state[7] != last_humidity && this->mode != climate::CLIMATE_MODE_OFF) {
if (remote_state[7] != this->last_humidity_ && this->mode != climate::CLIMATE_MODE_OFF) {
ESP_LOGD(TAG, "Set Humditiy: %d, %d\n", (int) this->target_humidity, (int) remote_state[7]);
remote_header[9] |= 0x10;
last_humidity = remote_state[7];
this->last_humidity_ = remote_state[7];
}
uint16_t fan_speed = this->fan_speed_();
remote_state[8] = fan_speed >> 8;
@@ -70,6 +70,7 @@ class DaikinArcClimate : public climate_ir::ClimateIR {
// Handle received IR Buffer
bool on_receive(remote_base::RemoteReceiveData data) override;
bool parse_state_frame_(const uint8_t frame[]);
uint8_t last_humidity_{0x66};
};
} // namespace daikin_arc
+2 -3
View File
@@ -1375,9 +1375,8 @@ void HonClimate::process_protocol_reset() {
bool HonClimate::should_get_big_data_() {
if (this->big_data_sensors_ > 0) {
static uint8_t counter = 0;
counter = (counter + 1) % 3;
return counter == 1;
this->big_data_counter_ = (this->big_data_counter_ + 1) % 3;
return this->big_data_counter_ == 1;
}
return false;
}
+1
View File
@@ -188,6 +188,7 @@ class HonClimate : public HaierClimateBase {
float active_alarm_count_{NAN};
std::chrono::steady_clock::time_point last_alarm_request_;
int big_data_sensors_{0};
uint8_t big_data_counter_{0};
esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{};
esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{};
HonSettings settings_{};
@@ -572,9 +572,8 @@ bool INA2XX::write_unsigned_16_(uint8_t reg, uint16_t val) {
}
bool INA2XX::read_unsigned_(uint8_t reg, uint8_t reg_size, uint64_t &data_out) {
static uint8_t rx_buf[5] = {0}; // max buffer size
if (reg_size > 5) {
uint8_t rx_buf[5]{};
if (reg_size > sizeof(rx_buf)) {
return false;
}
+10 -11
View File
@@ -137,7 +137,6 @@ void LTRAlsPsComponent::update() {
void LTRAlsPsComponent::loop() {
ErrorCode err = i2c::ERROR_OK;
static uint8_t tries{0};
switch (this->state_) {
case State::DELAYED_SETUP:
@@ -166,20 +165,20 @@ void LTRAlsPsComponent::loop() {
case State::WAITING_FOR_DATA:
if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) {
tries = 0;
this->read_data_tries_ = 0;
ESP_LOGV(TAG, "Reading sensor data having gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain),
get_itime_ms(this->als_readings_.integration_time));
this->read_sensor_data_(this->als_readings_);
this->state_ = State::DATA_COLLECTED;
this->apply_lux_calculation_(this->als_readings_);
} else if (tries >= MAX_TRIES) {
} else if (this->read_data_tries_ >= MAX_TRIES) {
ESP_LOGW(TAG, "Can't get data after several tries.");
tries = 0;
this->read_data_tries_ = 0;
this->status_set_warning();
this->state_ = State::IDLE;
return;
} else {
tries++;
this->read_data_tries_++;
}
break;
@@ -221,21 +220,21 @@ void LTRAlsPsComponent::loop() {
}
void LTRAlsPsComponent::check_and_trigger_ps_() {
static uint32_t last_high_trigger_time{0};
static uint32_t last_low_trigger_time{0};
uint16_t ps_data = this->read_ps_data_();
uint32_t now = millis();
if (ps_data != this->ps_readings_) {
this->ps_readings_ = ps_data;
// Higher values - object is closer to sensor
if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) {
last_high_trigger_time = now;
if (ps_data > this->ps_threshold_high_ &&
now - this->last_ps_high_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) {
this->last_ps_high_trigger_time_ = now;
ESP_LOGV(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data,
this->ps_threshold_high_);
this->on_ps_high_trigger_callback_.call();
} else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) {
last_low_trigger_time = now;
} else if (ps_data < this->ps_threshold_low_ &&
now - this->last_ps_low_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) {
this->last_ps_low_trigger_time_ = now;
ESP_LOGV(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data,
this->ps_threshold_low_);
this->on_ps_low_trigger_callback_.call();
+4 -1
View File
@@ -126,10 +126,13 @@ class LTRAlsPsComponent : public PollingComponent, public i2c::I2CDevice {
MeasurementRepeatRate repeat_rate_{MeasurementRepeatRate::REPEAT_RATE_500MS};
float glass_attenuation_factor_{1.0};
uint32_t last_ps_high_trigger_time_{0};
uint32_t last_ps_low_trigger_time_{0};
uint16_t ps_cooldown_time_s_{5};
PsGain ps_gain_{PsGain::PS_GAIN_16};
uint16_t ps_threshold_high_{0xffff};
uint16_t ps_threshold_low_{0x0000};
uint8_t read_data_tries_{0};
PsGain ps_gain_{PsGain::PS_GAIN_16};
//
// Sensors for publishing data
@@ -27,8 +27,6 @@ void MatrixKeypad::setup() {
}
void MatrixKeypad::loop() {
static uint32_t active_start = 0;
static int active_key = -1;
uint32_t now = App.get_loop_component_start_time();
int key = -1;
bool error = false;
@@ -54,8 +52,8 @@ void MatrixKeypad::loop() {
if (error)
return;
if (key != active_key) {
if ((active_key != -1) && (this->pressed_key_ == active_key)) {
if (key != this->active_key_) {
if ((this->active_key_ != -1) && (this->pressed_key_ == this->active_key_)) {
row = this->pressed_key_ / this->columns_.size();
col = this->pressed_key_ % this->columns_.size();
ESP_LOGD(TAG, "key @ row %d, col %d released", row, col);
@@ -70,13 +68,13 @@ void MatrixKeypad::loop() {
this->pressed_key_ = -1;
}
active_key = key;
this->active_key_ = key;
if (key == -1)
return;
active_start = now;
this->active_start_ = now;
}
if ((this->pressed_key_ == key) || (now - active_start < this->debounce_time_))
if ((this->pressed_key_ == key) || (now - this->active_start_ < this->debounce_time_))
return;
row = key / this->columns_.size();
@@ -44,6 +44,8 @@ class MatrixKeypad : public key_provider::KeyProvider, public Component {
bool has_diodes_{false};
bool has_pulldowns_{false};
int pressed_key_ = -1;
uint32_t active_start_{0};
int active_key_{-1};
std::vector<MatrixKeypadListener *> listeners_{};
std::vector<MatrixKeyTrigger *> key_triggers_;
+50 -4
View File
@@ -1,14 +1,34 @@
from dataclasses import dataclass
from esphome import pins
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
CODEOWNERS = ["@jorre05", "@edenhaus"]
DEPENDENCIES = ["uart"]
DOMAIN = "micronova"
@dataclass
class MicronovaData:
"""Track micronova component state during code generation."""
listener_count: int = 0
has_writer: bool = False
def _get_data() -> MicronovaData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = MicronovaData()
return CORE.data[DOMAIN]
CONF_MICRONOVA_ID = f"{DOMAIN}_id"
CONF_ENABLE_RX_PIN = "enable_rx_pin"
CONF_MEMORY_LOCATION = "memory_location"
@@ -66,16 +86,42 @@ def MICRONOVA_ADDRESS_SCHEMA(
return schema
def register_micronova_writer() -> None:
"""Register a component that can write to the stove (button, switch, number)."""
_get_data().has_writer = True
async def to_code_micronova_listener(mv, var, config):
_get_data().listener_count += 1
await cg.register_component(var, config)
cg.add(mv.register_micronova_listener(var))
cg.add(var.set_memory_location(config[CONF_MEMORY_LOCATION]))
cg.add(var.set_memory_address(config[CONF_MEMORY_ADDRESS]))
# Register listener as last step as we need all properties set before registering
cg.add(mv.register_micronova_listener(var))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
enable_rx_pin = await cg.gpio_pin_expression(config[CONF_ENABLE_RX_PIN])
var = cg.new_Pvariable(config[CONF_ID], enable_rx_pin)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
enable_rx_pin = await cg.gpio_pin_expression(config[CONF_ENABLE_RX_PIN])
cg.add(var.set_enable_rx_pin(enable_rx_pin))
CORE.add_job(_final_step)
@coroutine_with_priority(CoroPriority.FINAL)
async def _final_step() -> None:
"""Add defines for listener and writer counts after all are registered."""
data = _get_data()
if data.listener_count == 0 and not data.has_writer:
raise cv.Invalid(
"No micronova entities configured. Add at least one micronova entity."
)
if data.listener_count > 255:
raise cv.Invalid(
f"Too many micronova reading entities ({data.listener_count}). Maximum is 255."
)
if data.listener_count > 0:
cg.add_define("MICRONOVA_LISTENER_COUNT", data.listener_count)
if data.has_writer:
cg.add_define("USE_MICRONOVA_WRITER")
@@ -9,6 +9,7 @@ from .. import (
MICRONOVA_ADDRESS_SCHEMA,
MicroNova,
micronova_ns,
register_micronova_writer,
)
MicroNovaButton = micronova_ns.class_("MicroNovaButton", button.Button, cg.Component)
@@ -36,6 +37,7 @@ async def to_code(config):
mv = await cg.get_variable(config[CONF_MICRONOVA_ID])
if custom_button_config := config.get(CONF_CUSTOM_BUTTON):
register_micronova_writer()
bt = await button.new_button(custom_button_config, mv)
cg.add(bt.set_memory_location(custom_button_config[CONF_MEMORY_LOCATION]))
cg.add(bt.set_memory_address(custom_button_config[CONF_MEMORY_ADDRESS]))
@@ -2,9 +2,15 @@
namespace esphome::micronova {
static const char *const TAG = "micronova.button";
void MicroNovaButton::dump_config() {
LOG_BUTTON("", "Micronova button", this);
this->dump_base_config();
}
void MicroNovaButton::press_action() {
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_);
this->micronova_->request_update_listeners();
this->micronova_->queue_write_command(this->memory_location_, this->memory_address_, this->memory_data_);
}
} // namespace esphome::micronova
@@ -6,19 +6,18 @@
namespace esphome::micronova {
class MicroNovaButton : public Component, public button::Button, public MicroNovaButtonListener {
class MicroNovaButton : public Component, public button::Button, public MicroNovaBaseListener {
public:
MicroNovaButton(MicroNova *m) : MicroNovaButtonListener(m) {}
void dump_config() override {
LOG_BUTTON("", "Micronova button", this);
this->dump_base_config();
}
MicroNovaButton(MicroNova *m) : MicroNovaBaseListener(m) {}
void dump_config() override;
void set_memory_data(uint8_t f) { this->memory_data_ = f; }
uint8_t get_memory_data() { return this->memory_data_; }
protected:
void press_action() override;
uint8_t memory_data_ = 0;
};
} // namespace esphome::micronova
+157 -99
View File
@@ -3,8 +3,12 @@
namespace esphome::micronova {
static const int STOVE_REPLY_DELAY = 60;
static const uint8_t WRITE_BIT = 1 << 7; // 0x80
static const char *const TAG = "micronova";
static constexpr uint8_t STOVE_REPLY_SIZE = 2;
static constexpr uint32_t STOVE_REPLY_TIMEOUT = 200; // ms
static constexpr uint8_t WRITE_BIT = 1 << 7; // 0x80
bool MicroNovaCommand::is_write() const { return this->memory_location & WRITE_BIT; }
void MicroNovaBaseListener::dump_base_config() {
ESP_LOGCONFIG(TAG,
@@ -18,139 +22,193 @@ void MicroNovaListener::dump_base_config() {
LOG_UPDATE_INTERVAL(this);
}
void MicroNovaListener::request_value_from_stove_() {
this->micronova_->queue_read_request(this->memory_location_, this->memory_address_);
}
void MicroNova::setup() {
if (this->enable_rx_pin_ != nullptr) {
this->enable_rx_pin_->setup();
this->enable_rx_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->enable_rx_pin_->digital_write(false);
}
this->current_transmission_.request_transmission_time = millis();
this->current_transmission_.memory_location = 0;
this->current_transmission_.memory_address = 0;
this->current_transmission_.reply_pending = false;
this->current_transmission_.initiating_listener = nullptr;
this->enable_rx_pin_->setup();
this->enable_rx_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->enable_rx_pin_->digital_write(false);
}
void MicroNova::dump_config() {
ESP_LOGCONFIG(TAG, "MicroNova:");
if (this->enable_rx_pin_ != nullptr) {
LOG_PIN(" Enable RX Pin: ", this->enable_rx_pin_);
}
LOG_PIN(" Enable RX Pin: ", this->enable_rx_pin_);
}
void MicroNova::request_update_listeners() {
ESP_LOGD(TAG, "Schedule listener update");
for (auto &mv_listener : this->micronova_listeners_) {
mv_listener->set_needs_update(true);
#ifdef MICRONOVA_LISTENER_COUNT
void MicroNova::register_micronova_listener(MicroNovaListener *listener) {
this->listeners_.push_back(listener);
// Request initial value
this->queue_read_request(listener->get_memory_location(), listener->get_memory_address());
}
void MicroNova::request_update_listeners_() {
ESP_LOGD(TAG, "Requesting update from all listeners");
for (auto *listener : this->listeners_) {
this->queue_read_request(listener->get_memory_location(), listener->get_memory_address());
}
}
#endif
void MicroNova::loop() {
// Only read one sensor that needs update per loop
// If STOVE_REPLY_DELAY time has passed since last loop()
// check for a reply from the stove
if ((this->current_transmission_.reply_pending) &&
(millis() - this->current_transmission_.request_transmission_time > STOVE_REPLY_DELAY)) {
int stove_reply_value = this->read_stove_reply();
if (this->current_transmission_.initiating_listener != nullptr) {
this->current_transmission_.initiating_listener->process_value_from_stove(stove_reply_value);
this->current_transmission_.initiating_listener = nullptr;
}
this->current_transmission_.reply_pending = false;
return;
} else if (!this->current_transmission_.reply_pending) {
for (auto &mv_listener : this->micronova_listeners_) {
if (mv_listener->get_needs_update()) {
mv_listener->set_needs_update(false);
this->current_transmission_.initiating_listener = mv_listener;
mv_listener->request_value_from_stove();
return;
// Check if we're processing a command and waiting for reply
if (this->reply_pending_) {
// Check if all reply bytes have arrived
if (this->available() >= STOVE_REPLY_SIZE) {
#ifdef MICRONOVA_LISTENER_COUNT
int stove_reply_value = this->read_stove_reply_();
if (this->current_command_.is_write()) {
if (stove_reply_value == -1) {
ESP_LOGW(TAG, "Write to [0x%02X:0x%02X] may have failed (checksum mismatch in reply)",
this->current_command_.memory_location & ~WRITE_BIT, this->current_command_.memory_address);
}
} else {
// For READ commands, notify all listeners registered for this address
uint8_t loc = this->current_command_.memory_location;
uint8_t addr = this->current_command_.memory_address;
for (auto *listener : this->listeners_) {
if (listener->get_memory_location() == loc && listener->get_memory_address() == addr) {
listener->process_value_from_stove(stove_reply_value);
}
}
}
#else
this->read_stove_reply_();
#endif
this->reply_pending_ = false;
} else if (millis() - this->transmission_time_ > STOVE_REPLY_TIMEOUT) {
// Timeout - no reply received (buffer cleared before next command)
ESP_LOGW(TAG, "Timeout waiting for reply from [0x%02X:0x%02X], available: %d",
this->current_command_.memory_location, this->current_command_.memory_address, this->available());
this->reply_pending_ = false;
}
return;
}
// No reply pending - process next command (writes have priority over reads)
#ifdef USE_MICRONOVA_WRITER
if (!this->write_queue_.empty()) {
this->current_command_ = this->write_queue_.front();
this->write_queue_.pop();
this->send_current_command_();
return;
}
#endif
#ifdef MICRONOVA_LISTENER_COUNT
if (!this->read_queue_.empty()) {
this->current_command_ = this->read_queue_.front();
this->read_queue_.pop();
this->send_current_command_();
}
#endif
}
void MicroNova::request_address(uint8_t location, uint8_t address, MicroNovaListener *listener) {
uint8_t write_data[2] = {0, 0};
#ifdef MICRONOVA_LISTENER_COUNT
void MicroNova::queue_read_request(uint8_t location, uint8_t address) {
// Check if this read is already queued
for (const auto &queued : this->read_queue_) {
if (queued.memory_location == location && queued.memory_address == address) {
ESP_LOGV(TAG, "Read [%02X,%02X] already queued, skipping", location, address);
return;
}
}
MicroNovaCommand cmd;
cmd.memory_location = location;
cmd.memory_address = address;
cmd.data = 0;
if (!this->read_queue_.push(cmd)) {
ESP_LOGW(TAG, "Read queue full, dropping read [%02X,%02X]", location, address);
return;
}
ESP_LOGV(TAG, "Queued read [%02X,%02X] (queue size: %u)", location, address, this->read_queue_.size());
}
#endif
void MicroNova::send_current_command_() {
uint8_t trash_rx;
if (this->reply_pending_mutex_.try_lock()) {
// clear rx buffer.
// Stove hickups may cause late replies in the rx
while (this->available()) {
this->read_byte(&trash_rx);
ESP_LOGW(TAG, "Reading excess byte 0x%02X", trash_rx);
}
write_data[0] = location;
write_data[1] = address;
ESP_LOGV(TAG, "Request from stove [%02X,%02X]", write_data[0], write_data[1]);
this->enable_rx_pin_->digital_write(true);
this->write_array(write_data, 2);
this->flush();
this->enable_rx_pin_->digital_write(false);
this->current_transmission_.request_transmission_time = millis();
this->current_transmission_.memory_location = location;
this->current_transmission_.memory_address = address;
this->current_transmission_.reply_pending = true;
this->current_transmission_.initiating_listener = listener;
} else {
ESP_LOGE(TAG, "Reply is pending, skipping read request");
// Clear rx buffer - stove hiccups may cause late replies in the rx
while (this->available()) {
this->read_byte(&trash_rx);
ESP_LOGW(TAG, "Reading excess byte 0x%02X", trash_rx);
}
uint8_t write_data[4] = {this->current_command_.memory_location, this->current_command_.memory_address, 0, 0};
size_t write_len;
if (this->current_command_.is_write()) {
write_len = 4;
write_data[2] = this->current_command_.data;
// calculate checksum
write_data[3] = write_data[0] + write_data[1] + write_data[2];
ESP_LOGV(TAG, "Sending write request [%02X,%02X,%02X,%02X]", write_data[0], write_data[1], write_data[2],
write_data[3]);
} else {
write_len = 2;
ESP_LOGV(TAG, "Sending read request [%02X,%02X]", write_data[0], write_data[1]);
}
this->enable_rx_pin_->digital_write(true);
this->write_array(write_data, write_len);
this->flush();
this->enable_rx_pin_->digital_write(false);
this->transmission_time_ = millis();
this->reply_pending_ = true;
}
int MicroNova::read_stove_reply() {
int MicroNova::read_stove_reply_() {
uint8_t reply_data[2] = {0, 0};
uint8_t checksum = 0;
// assert enable_rx_pin is false
this->read_array(reply_data, 2);
this->reply_pending_mutex_.unlock();
ESP_LOGV(TAG, "Reply from stove [%02X,%02X]", reply_data[0], reply_data[1]);
checksum = ((uint16_t) this->current_transmission_.memory_location +
(uint16_t) this->current_transmission_.memory_address + (uint16_t) reply_data[1]) &
0xFF;
uint8_t checksum = this->current_command_.memory_location + this->current_command_.memory_address + reply_data[1];
if (reply_data[0] != checksum) {
ESP_LOGE(TAG, "Checksum missmatch! From [0x%02X:0x%02X] received [0x%02X,0x%02X]. Expected 0x%02X, got 0x%02X",
this->current_transmission_.memory_location, this->current_transmission_.memory_address, reply_data[0],
ESP_LOGE(TAG, "Checksum mismatch! From [0x%02X:0x%02X] received [0x%02X,0x%02X]. Expected 0x%02X, got 0x%02X",
this->current_command_.memory_location, this->current_command_.memory_address, reply_data[0],
reply_data[1], checksum, reply_data[0]);
return -1;
}
return ((int) reply_data[1]);
}
void MicroNova::write_address(uint8_t location, uint8_t address, uint8_t data) {
uint8_t write_data[4] = {0, 0, 0, 0};
uint16_t checksum = 0;
#ifdef USE_MICRONOVA_WRITER
bool MicroNova::queue_write_command(uint8_t location, uint8_t address, uint8_t data) {
MicroNovaCommand cmd;
cmd.memory_location = location | WRITE_BIT;
cmd.memory_address = address;
cmd.data = data;
if (this->reply_pending_mutex_.try_lock()) {
uint8_t write_location = location | WRITE_BIT;
write_data[0] = write_location;
write_data[1] = address;
write_data[2] = data;
checksum = ((uint16_t) write_data[0] + (uint16_t) write_data[1] + (uint16_t) write_data[2]) & 0xFF;
write_data[3] = checksum;
ESP_LOGV(TAG, "Write 4 bytes [%02X,%02X,%02X,%02X]", write_data[0], write_data[1], write_data[2], write_data[3]);
this->enable_rx_pin_->digital_write(true);
this->write_array(write_data, 4);
this->flush();
this->enable_rx_pin_->digital_write(false);
this->current_transmission_.request_transmission_time = millis();
this->current_transmission_.memory_location = write_location;
this->current_transmission_.memory_address = address;
this->current_transmission_.reply_pending = true;
this->current_transmission_.initiating_listener = nullptr;
} else {
ESP_LOGE(TAG, "Reply is pending, skipping write");
// Check if a write to the same address is already queued - update data in-place
for (auto &queued : this->write_queue_) {
if (queued.memory_location == cmd.memory_location && queued.memory_address == cmd.memory_address) {
if (queued.data != cmd.data) {
ESP_LOGD(TAG, "Updating queued write [%02X,%02X] data 0x%02X -> 0x%02X", location, address, queued.data, data);
queued.data = cmd.data;
} else {
ESP_LOGV(TAG, "Write [%02X,%02X] with data 0x%02X already queued, skipping", location, address, data);
}
return true;
}
}
if (!this->write_queue_.push(cmd)) {
ESP_LOGW(TAG, "Write queue full, dropping command");
return false;
}
ESP_LOGD(TAG, "Queued write [%02X,%02X] (queue size: %u)", location, address, this->write_queue_.size());
#ifdef MICRONOVA_LISTENER_COUNT
// Automatically queue sensor updates after write commands
this->request_update_listeners_();
#endif
return true;
}
#endif
} // namespace esphome::micronova
+52 -41
View File
@@ -6,11 +6,19 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <vector>
namespace esphome::micronova {
static const char *const TAG = "micronova";
static constexpr uint8_t WRITE_QUEUE_SIZE = 10;
/// Represents a command to be sent to the stove
/// Write commands have the high bit (0x80) set in memory_location
struct MicroNovaCommand {
uint8_t memory_location;
uint8_t memory_address;
uint8_t data; ///< Only used for write commands
bool is_write() const;
};
class MicroNova;
@@ -18,11 +26,8 @@ class MicroNova;
// Interface classes.
class MicroNovaBaseListener {
public:
MicroNovaBaseListener() {}
MicroNovaBaseListener(MicroNova *m) { this->micronova_ = m; }
void set_micronova_object(MicroNova *m) { this->micronova_ = m; }
void set_memory_location(uint8_t l) { this->memory_location_ = l; }
uint8_t get_memory_location() { return this->memory_location_; }
@@ -32,70 +37,76 @@ class MicroNovaBaseListener {
void dump_base_config();
protected:
MicroNova *micronova_{nullptr};
MicroNova *micronova_;
uint8_t memory_location_ = 0;
uint8_t memory_address_ = 0;
};
class MicroNovaListener : public MicroNovaBaseListener, public PollingComponent {
public:
MicroNovaListener() {}
MicroNovaListener(MicroNova *m) : MicroNovaBaseListener(m) {}
virtual void request_value_from_stove() = 0;
void update() override { this->request_value_from_stove_(); }
virtual void process_value_from_stove(int value_from_stove) = 0;
void set_needs_update(bool u) { this->needs_update_ = u; }
bool get_needs_update() { return this->needs_update_; }
void update() override { this->set_needs_update(true); }
void dump_base_config();
protected:
bool needs_update_ = false;
};
class MicroNovaButtonListener : public MicroNovaBaseListener {
public:
MicroNovaButtonListener(MicroNova *m) : MicroNovaBaseListener(m) {}
protected:
uint8_t memory_data_ = 0;
void request_value_from_stove_();
};
/////////////////////////////////////////////////////////////////////
// Main component class
class MicroNova : public Component, public uart::UARTDevice {
public:
MicroNova() {}
MicroNova(GPIOPin *enable_rx_pin) : enable_rx_pin_(enable_rx_pin) {}
void setup() override;
void loop() override;
void dump_config() override;
void register_micronova_listener(MicroNovaListener *l) { this->micronova_listeners_.push_back(l); }
void request_update_listeners();
void request_address(uint8_t location, uint8_t address, MicroNovaListener *listener);
void write_address(uint8_t location, uint8_t address, uint8_t data);
int read_stove_reply();
#ifdef MICRONOVA_LISTENER_COUNT
void register_micronova_listener(MicroNovaListener *listener);
void set_enable_rx_pin(GPIOPin *enable_rx_pin) { this->enable_rx_pin_ = enable_rx_pin; }
/// Queue a read request to the stove (low priority - added at back)
/// All listeners registered for this address will be notified with the result
/// @param location Memory location on the stove
/// @param address Memory address on the stove
void queue_read_request(uint8_t location, uint8_t address);
#endif
#ifdef USE_MICRONOVA_WRITER
/// Queue a write command to the stove (processed before reads)
/// @param location Memory location on the stove
/// @param address Memory address on the stove
/// @param data Data to write
/// @return true if command was queued, false if queue was full
bool queue_write_command(uint8_t location, uint8_t address, uint8_t data);
#endif
protected:
GPIOPin *enable_rx_pin_{nullptr};
void send_current_command_();
int read_stove_reply_();
#ifdef MICRONOVA_LISTENER_COUNT
void request_update_listeners_();
#endif
struct MicroNovaSerialTransmission {
uint32_t request_transmission_time;
uint8_t memory_location;
uint8_t memory_address;
bool reply_pending;
MicroNovaListener *initiating_listener;
};
GPIOPin *enable_rx_pin_;
Mutex reply_pending_mutex_;
MicroNovaSerialTransmission current_transmission_;
#ifdef USE_MICRONOVA_WRITER
StaticRingBuffer<MicroNovaCommand, WRITE_QUEUE_SIZE> write_queue_;
#endif
#ifdef MICRONOVA_LISTENER_COUNT
StaticRingBuffer<MicroNovaCommand, MICRONOVA_LISTENER_COUNT> read_queue_;
#endif
MicroNovaCommand current_command_{};
uint32_t transmission_time_{0}; ///< Time when current command was sent
bool reply_pending_{false}; ///< True if we are waiting for a reply from the stove
std::vector<MicroNovaListener *> micronova_listeners_{};
#ifdef MICRONOVA_LISTENER_COUNT
StaticVector<MicroNovaListener *, MICRONOVA_LISTENER_COUNT> listeners_;
#endif
};
} // namespace esphome::micronova
@@ -9,6 +9,7 @@ from .. import (
MicroNova,
MicroNovaListener,
micronova_ns,
register_micronova_writer,
to_code_micronova_listener,
)
@@ -59,22 +60,24 @@ async def to_code(config):
mv = await cg.get_variable(config[CONF_MICRONOVA_ID])
if thermostat_temperature_config := config.get(CONF_THERMOSTAT_TEMPERATURE):
register_micronova_writer()
numb = await number.new_number(
thermostat_temperature_config,
mv,
min_value=0,
max_value=40,
step=thermostat_temperature_config.get(CONF_STEP),
)
await to_code_micronova_listener(mv, numb, thermostat_temperature_config)
cg.add(numb.set_micronova_object(mv))
cg.add(numb.set_use_step_scaling(True))
if power_level_config := config.get(CONF_POWER_LEVEL):
register_micronova_writer()
numb = await number.new_number(
power_level_config,
mv,
min_value=1,
max_value=5,
step=1,
)
await to_code_micronova_listener(mv, numb, power_level_config)
cg.add(numb.set_micronova_object(mv))
@@ -2,6 +2,13 @@
namespace esphome::micronova {
static const char *const TAG = "micronova.number";
void MicroNovaNumber::dump_config() {
LOG_NUMBER("", "Micronova number", this);
this->dump_base_config();
}
void MicroNovaNumber::process_value_from_stove(int value_from_stove) {
if (value_from_stove == -1) {
this->publish_state(NAN);
@@ -22,8 +29,7 @@ void MicroNovaNumber::control(float value) {
} else {
new_number = static_cast<uint8_t>(value);
}
this->micronova_->write_address(this->memory_location_, this->memory_address_, new_number);
this->micronova_->request_update_listeners();
this->micronova_->queue_write_command(this->memory_location_, this->memory_address_, new_number);
}
} // namespace esphome::micronova
@@ -7,16 +7,9 @@ namespace esphome::micronova {
class MicroNovaNumber : public number::Number, public MicroNovaListener {
public:
MicroNovaNumber() {}
MicroNovaNumber(MicroNova *m) : MicroNovaListener(m) {}
void dump_config() override {
LOG_NUMBER("", "Micronova number", this);
this->dump_base_config();
}
void dump_config() override;
void control(float value) override;
void request_value_from_stove() override {
this->micronova_->request_address(this->memory_location_, this->memory_address_, this);
}
void process_value_from_stove(int value_from_stove) override;
void set_use_step_scaling(bool v) { this->use_step_scaling_ = v; }
@@ -2,6 +2,13 @@
namespace esphome::micronova {
static const char *const TAG = "micronova.sensor";
void MicroNovaSensor::dump_config() {
LOG_SENSOR("", "Micronova sensor", this);
this->dump_base_config();
}
void MicroNovaSensor::process_value_from_stove(int value_from_stove) {
if (value_from_stove == -1) {
this->publish_state(NAN);
@@ -8,14 +8,8 @@ namespace esphome::micronova {
class MicroNovaSensor : public sensor::Sensor, public MicroNovaListener {
public:
MicroNovaSensor(MicroNova *m) : MicroNovaListener(m) {}
void dump_config() override {
LOG_SENSOR("", "Micronova sensor", this);
this->dump_base_config();
}
void dump_config() override;
void request_value_from_stove() override {
this->micronova_->request_address(this->memory_location_, this->memory_address_, this);
}
void process_value_from_stove(int value_from_stove) override;
void set_divisor(uint8_t d) { this->divisor_ = d; }
@@ -9,6 +9,7 @@ from .. import (
MicroNova,
MicroNovaListener,
micronova_ns,
register_micronova_writer,
to_code_micronova_listener,
)
@@ -48,6 +49,7 @@ async def to_code(config):
mv = await cg.get_variable(config[CONF_MICRONOVA_ID])
if stove_config := config.get(CONF_STOVE):
register_micronova_writer()
sw = await switch.new_switch(stove_config, mv)
await to_code_micronova_listener(mv, sw, stove_config)
cg.add(sw.set_memory_data_on(stove_config[CONF_MEMORY_DATA_ON]))
@@ -2,33 +2,42 @@
namespace esphome::micronova {
static const char *const TAG = "micronova.switch";
void MicroNovaSwitch::dump_config() {
LOG_SWITCH("", "Micronova switch", this);
this->dump_base_config();
}
void MicroNovaSwitch::write_state(bool state) {
if (state) {
// Only send power-on when current state is Off
if (this->raw_state_ == 0) {
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_);
this->publish_state(true);
if (this->micronova_->queue_write_command(this->memory_location_, this->memory_address_, this->memory_data_on_)) {
this->publish_state(true);
}
} else {
ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", this->raw_state_);
}
} else {
// don't send power-off when status is Off or Final cleaning
if (this->raw_state_ != 0 && this->raw_state_ != 6) {
this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_);
this->publish_state(false);
if (this->micronova_->queue_write_command(this->memory_location_, this->memory_address_,
this->memory_data_off_)) {
this->publish_state(false);
}
} else {
ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", this->raw_state_);
}
}
this->set_needs_update(true);
}
void MicroNovaSwitch::process_value_from_stove(int value_from_stove) {
this->raw_state_ = value_from_stove;
if (value_from_stove == -1) {
ESP_LOGE(TAG, "Error reading stove state");
return;
}
this->raw_state_ = value_from_stove;
// set the stove switch to on for any value but 0
bool state = value_from_stove != 0;
@@ -9,13 +9,7 @@ namespace esphome::micronova {
class MicroNovaSwitch : public switch_::Switch, public MicroNovaListener {
public:
MicroNovaSwitch(MicroNova *m) : MicroNovaListener(m) {}
void dump_config() override {
LOG_SWITCH("", "Micronova switch", this);
this->dump_base_config();
}
void request_value_from_stove() override {
this->micronova_->request_address(this->memory_location_, this->memory_address_, this);
}
void dump_config() override;
void process_value_from_stove(int value_from_stove) override;
void set_memory_data_on(uint8_t f) { this->memory_data_on_ = f; }
@@ -2,6 +2,13 @@
namespace esphome::micronova {
static const char *const TAG = "micronova.text_sensor";
void MicroNovaTextSensor::dump_config() {
LOG_TEXT_SENSOR("", "Micronova text sensor", this);
this->dump_base_config();
}
void MicroNovaTextSensor::process_value_from_stove(int value_from_stove) {
if (value_from_stove == -1) {
this->publish_state("unknown");
@@ -20,13 +20,7 @@ static const char *const STOVE_STATES[11] = {"Off",
class MicroNovaTextSensor : public text_sensor::TextSensor, public MicroNovaListener {
public:
MicroNovaTextSensor(MicroNova *m) : MicroNovaListener(m) {}
void dump_config() override {
LOG_TEXT_SENSOR("", "Micronova text sensor", this);
this->dump_base_config();
}
void request_value_from_stove() override {
this->micronova_->request_address(this->memory_location_, this->memory_address_, this);
}
void dump_config() override;
void process_value_from_stove(int value_from_stove) override;
};
@@ -150,17 +150,16 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) {
this->on_publish_.call((int) event.msg_id);
break;
case MQTT_EVENT_DATA: {
static std::string topic;
if (!event.topic.empty()) {
// When a single message arrives as multiple chunks, the topic will be empty
// on any but the first message, leading to event.topic being an empty string.
// To ensure handlers get the correct topic, cache the last seen topic to
// simulate always receiving the topic from underlying library
topic = event.topic;
this->cached_topic_ = event.topic;
}
ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str());
this->on_message_.call(topic.c_str(), event.data.data(), event.data.size(), event.current_data_offset,
event.total_data_len);
ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", this->cached_topic_.c_str());
this->on_message_.call(this->cached_topic_.c_str(), event.data.data(), event.data.size(),
event.current_data_offset, event.total_data_len);
} break;
case MQTT_EVENT_ERROR:
ESP_LOGE(TAG, "MQTT_EVENT_ERROR");
@@ -265,6 +265,7 @@ class MQTTBackendESP32 final : public MQTTBackend {
CallbackManager<on_unsubscribe_callback_t> on_unsubscribe_;
CallbackManager<on_message_callback_t> on_message_;
CallbackManager<on_publish_user_callback_t> on_publish_;
std::string cached_topic_;
std::queue<Event> mqtt_events_;
#if defined(USE_MQTT_IDF_ENQUEUE)
+3 -2
View File
@@ -124,6 +124,7 @@ void SGP4xComponent::self_test_() {
}
this->self_test_complete_ = true;
this->nox_conditioning_start_ = millis();
ESP_LOGD(TAG, "Self-test complete");
});
}
@@ -161,7 +162,6 @@ void SGP4xComponent::update_gas_indices_() {
void SGP4xComponent::measure_raw_() {
float humidity = NAN;
static uint32_t nox_conditioning_start = millis();
if (!this->self_test_complete_) {
ESP_LOGW(TAG, "Self-test incomplete");
@@ -191,10 +191,11 @@ void SGP4xComponent::measure_raw_() {
response_words = 1;
} else {
// SGP41 sensor must use NOx conditioning command for the first 10 seconds
if (millis() - nox_conditioning_start < 10000) {
if (this->nox_conditioning_start_.has_value() && millis() - *this->nox_conditioning_start_ < 10000) {
command = SGP41_CMD_NOX_CONDITIONING;
response_words = 1;
} else {
this->nox_conditioning_start_.reset();
command = SGP41_CMD_MEASURE_RAW;
response_words = 2;
}
+2 -1
View File
@@ -127,8 +127,9 @@ class SGP4xComponent : public PollingComponent, public sensor::Sensor, public se
uint16_t measure_time_;
uint8_t samples_read_ = 0;
uint8_t samples_to_stabilize_ = static_cast<int8_t>(GasIndexAlgorithm_INITIAL_BLACKOUT) * 2;
bool store_baseline_;
optional<uint32_t> nox_conditioning_start_{};
ESPPreferenceObject pref_;
uint32_t seconds_since_last_store_;
SGP4xBaselines voc_baselines_storage_;
+2
View File
@@ -107,6 +107,8 @@
#define MDNS_SERVICE_COUNT 3
#define USE_MDNS_DYNAMIC_TXT
#define MDNS_DYNAMIC_TXT_COUNT 2
#define MICRONOVA_LISTENER_COUNT 1
#define USE_MICRONOVA_WRITER
#define SERIAL_PROXY_COUNT 2
#define SNTP_SERVER_COUNT 3
#define USE_MEDIA_PLAYER
+75
View File
@@ -296,6 +296,81 @@ template<typename T, size_t N> class StaticVector {
operator std::span<const T>() const { return std::span<const T>(data_.data(), count_); }
};
/// Fixed-size circular buffer with FIFO semantics and iteration support.
///
/// A tiny ring buffer that avoids dynamic allocations from std::deque/std::queue
/// (which can be wasteful on MCUs), while supporting iteration over queued elements.
///
/// Not thread-safe. All access (push/pop/iteration) must occur from a single
/// context, or the caller must provide external synchronization.
template<typename T, size_t N> class StaticRingBuffer {
using index_type = std::conditional_t<(N <= 255), uint8_t, uint16_t>;
public:
class Iterator {
public:
Iterator(StaticRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {}
T &operator*() { return buf_->data_[(buf_->head_ + pos_) % N]; }
Iterator &operator++() {
++pos_;
return *this;
}
bool operator!=(const Iterator &other) const { return pos_ != other.pos_; }
private:
StaticRingBuffer *buf_;
index_type pos_;
};
class ConstIterator {
public:
ConstIterator(const StaticRingBuffer *buf, index_type pos) : buf_(buf), pos_(pos) {}
const T &operator*() const { return buf_->data_[(buf_->head_ + pos_) % N]; }
ConstIterator &operator++() {
++pos_;
return *this;
}
bool operator!=(const ConstIterator &other) const { return pos_ != other.pos_; }
private:
const StaticRingBuffer *buf_;
index_type pos_;
};
bool push(const T &value) {
if (this->count_ >= N) {
return false;
}
this->data_[this->tail_] = value;
this->tail_ = (this->tail_ + 1) % N;
++this->count_;
return true;
}
void pop() {
if (this->count_ > 0) {
this->head_ = (this->head_ + 1) % N;
--this->count_;
}
}
T &front() { return this->data_[this->head_]; }
const T &front() const { return this->data_[this->head_]; }
index_type size() const { return this->count_; }
bool empty() const { return this->count_ == 0; }
Iterator begin() { return Iterator(this, 0); }
Iterator end() { return Iterator(this, this->count_); }
ConstIterator begin() const { return ConstIterator(this, 0); }
ConstIterator end() const { return ConstIterator(this, this->count_); }
protected:
T data_[N];
index_type head_{0};
index_type tail_{0};
index_type count_{0};
};
/// Fixed-capacity vector - allocates once at runtime, never reallocates
/// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append)
/// when size is known at initialization but not at compile time
+4
View File
@@ -79,6 +79,10 @@ def create_test_config(config_name: str, includes: list[str]) -> dict:
"-Og", # optimize for debug
"-DUSE_TIME_TIMEZONE", # enable timezone code paths for testing
"-DESPHOME_DEBUG", # enable debug assertions
# Enable the address and undefined behavior sanitizers
"-fsanitize=address",
"-fsanitize=undefined",
"-fno-omit-frame-pointer",
],
"debug_build_flags": [ # only for debug builds
"-g3", # max debug info