mirror of
https://github.com/esphome/esphome.git
synced 2026-09-19 02:58:38 +00:00
[micronova] Add command queue (#12268)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: edenhaus <edenhaus@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick+github@koston.org>
This commit is contained in:
co-authored by
pre-commit-ci-lite[bot]
Copilot
Jonathan Swoboda
edenhaus
J. Nick Koston
J. Nick Koston
parent
e8e700a683
commit
236f6b1935
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -293,6 +293,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
|
||||
|
||||
Reference in New Issue
Block a user