1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-09 01:01:56 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
J. Nick Koston
39013388dd pipsolar: batch UART reads to reduce per-loop overhead 2026-02-07 00:26:33 +01:00
26 changed files with 66 additions and 684 deletions

View File

@@ -87,7 +87,6 @@ from esphome.cpp_types import ( # noqa: F401
size_t,
std_ns,
std_shared_ptr,
std_span,
std_string,
std_string_ref,
std_vector,

View File

@@ -1,67 +0,0 @@
#pragma once
#include <cstdint>
#include <algorithm>
#include "esphome/core/color.h"
/* Utility for converting internal \a Color RGB representation to supported IC hardware color keys
*
* Focus in driver layer is on efficiency.
* For optimum output quality on RGB inputs consider offline color keying/dithering.
* Also see e.g. Image component.
*/
namespace esphome::epaper_spi {
/** Delta for when to regard as gray */
static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50;
/** Map RGB color to discrete BWYR hex 4 color key
*
* @tparam NATIVE_COLOR Type of native hardware color values
* @param color RGB color to convert from
* @param hw_black Native value for black
* @param hw_white Native value for white
* @param hw_yellow Native value for yellow
* @param hw_red Native value for red
* @return Converted native hardware color value
* @internal Constexpr. Does not depend on side effects ("pure").
*/
template<typename NATIVE_COLOR>
constexpr NATIVE_COLOR color_to_bwyr(Color color, NATIVE_COLOR hw_black, NATIVE_COLOR hw_white, NATIVE_COLOR hw_yellow,
NATIVE_COLOR hw_red) {
// --- Step 1: Check for Grayscale (Black or White) ---
// We define "grayscale" as a color where the min and max components
// are close to each other.
const auto [min_rgb, max_rgb] = std::minmax({color.r, color.g, color.b});
if ((max_rgb - min_rgb) < COLORCONV_GRAY_THRESHOLD) {
// It's a shade of gray. Map to BLACK or WHITE.
// We split the luminance at the halfway point (382 = (255*3)/2)
if ((static_cast<int>(color.r) + color.g + color.b) > 382) {
return hw_white;
}
return hw_black;
}
// --- Step 2: Check for Primary/Secondary Colors ---
// If it's not gray, it's a color. We check which components are
// "on" (over 128) vs "off". This divides the RGB cube into 8 corners.
const bool r_on = (color.r > 128);
const bool g_on = (color.g > 128);
const bool b_on = (color.b > 128);
if (r_on) {
if (!b_on) {
return g_on ? hw_yellow : hw_red;
}
// At least red+blue high (but not gray) -> White
return hw_white;
} else {
return (b_on && g_on) ? hw_white : hw_black;
}
}
} // namespace esphome::epaper_spi

View File

@@ -1,227 +0,0 @@
#include "epaper_spi_jd79660.h"
#include "colorconv.h"
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.jd79660";
/** Pixel color as 2bpp. Must match IC LUT values. */
enum JD79660Color : uint8_t {
BLACK = 0b00,
WHITE = 0b01,
YELLOW = 0b10,
RED = 0b11,
};
/** Map RGB color to JD79660 BWYR hex color keys */
static JD79660Color HOT color_to_hex(Color color) {
return color_to_bwyr(color, JD79660Color::BLACK, JD79660Color::WHITE, JD79660Color::YELLOW, JD79660Color::RED);
}
void EPaperJD79660::fill(Color color) {
// If clipping is active, fall back to base implementation
if (this->get_clipping().is_set()) {
EPaperBase::fill(color);
return;
}
const auto pixel_color = color_to_hex(color);
// We store 4 pixels per byte
this->buffer_.fill(pixel_color | (pixel_color << 2) | (pixel_color << 4) | (pixel_color << 6));
}
void HOT EPaperJD79660::draw_pixel_at(int x, int y, Color color) {
if (!this->rotate_coordinates_(x, y))
return;
const auto pixel_bits = color_to_hex(color);
const uint32_t pixel_position = x + y * this->get_width_internal();
// We store 4 pixels per byte at LSB offsets 6, 4, 2, 0
const uint32_t byte_position = pixel_position / 4;
const uint32_t bit_offset = 6 - ((pixel_position % 4) * 2);
const auto original = this->buffer_[byte_position];
this->buffer_[byte_position] = (original & (~(0b11 << bit_offset))) | // mask old 2bpp
(pixel_bits << bit_offset); // add new 2bpp
}
bool EPaperJD79660::reset() {
// On entry state RESET set step, next state will be RESET_END
if (this->state_ == EPaperState::RESET) {
this->step_ = FSMState::RESET_STEP0_H;
}
switch (this->step_) {
case FSMState::RESET_STEP0_H:
// Step #0: Reset H for some settle time.
ESP_LOGVV(TAG, "reset #0");
this->reset_pin_->digital_write(true);
this->reset_duration_ = SLEEP_MS_RESET0;
this->step_ = FSMState::RESET_STEP1_L;
return false; // another loop: step #1 below
case FSMState::RESET_STEP1_L:
// Step #1: Reset L pulse for slightly >1.5ms.
// This is actual reset trigger.
ESP_LOGVV(TAG, "reset #1");
// As commented on SLEEP_MS_RESET1: Reset pulse must happen within time window.
// So do not use FSM loop, and avoid other calls/logs during pulse below.
this->reset_pin_->digital_write(false);
delay(SLEEP_MS_RESET1);
this->reset_pin_->digital_write(true);
this->reset_duration_ = SLEEP_MS_RESET2;
this->step_ = FSMState::RESET_STEP2_IDLECHECK;
return false; // another loop: step #2 below
case FSMState::RESET_STEP2_IDLECHECK:
// Step #2: Basically finished. Check sanity, and move FSM to INITIALISE state
ESP_LOGVV(TAG, "reset #2");
if (!this->is_idle_()) {
// Expectation: Idle after reset + settle time.
// Improperly connected/unexpected hardware?
// Error path reproducable e.g. with disconnected VDD/... pins
// (optimally while busy_pin configured with local pulldown).
// -> Mark failed to avoid followup problems.
this->mark_failed(LOG_STR("Busy after reset"));
}
break; // End state loop below
default:
// Unexpected step = bug?
this->mark_failed();
}
this->step_ = FSMState::INIT_STEP0_REGULARINIT; // reset for initialize state
return true;
}
bool EPaperJD79660::initialise(bool partial) {
switch (this->step_) {
case FSMState::INIT_STEP0_REGULARINIT:
// Step #0: Regular init sequence
ESP_LOGVV(TAG, "init #0");
if (!EPaperBase::initialise(partial)) { // Call parent impl
return false; // If parent should request another loop, do so
}
// Fast init requested + supported?
if (partial && (this->fast_update_length_ > 0)) {
this->step_ = FSMState::INIT_STEP1_FASTINIT;
this->wait_for_idle_(true); // Must wait for idle before fastinit sequence in next loop
return false; // another loop: step #1 below
}
break; // End state loop below
case FSMState::INIT_STEP1_FASTINIT:
// Step #1: Fast init sequence
ESP_LOGVV(TAG, "init #1");
this->write_fastinit_();
break; // End state loop below
default:
// Unexpected step = bug?
this->mark_failed();
}
this->step_ = FSMState::NONE;
return true; // Finished: State transition waits for idle
}
bool EPaperJD79660::transfer_buffer_chunks_() {
size_t buf_idx = 0;
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
const uint32_t start_time = App.get_loop_component_start_time();
const auto buffer_length = this->buffer_length_;
while (this->current_data_index_ != buffer_length) {
bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++];
if (buf_idx == sizeof bytes_to_send) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->disable();
ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis());
buf_idx = 0;
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
return false;
}
}
}
// Finished the entire dataset
if (buf_idx != 0) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->disable();
ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis());
}
// Cleanup for next transfer
this->current_data_index_ = 0;
// Finished with all buffer chunks
return true;
}
void EPaperJD79660::write_fastinit_() {
// Undocumented register sequence in vendor register range.
// Related to Fast Init/Update.
// Should likely happen after regular init seq and power on, but before refresh.
// Might only work for some models with certain factory MTP.
// Please do not change without knowledge to avoid breakage.
this->send_init_sequence_(this->fast_update_, this->fast_update_length_);
}
bool EPaperJD79660::transfer_data() {
// For now always send full frame buffer in chunks.
// JD79660 might support partial window transfers. But sample code missing.
// And likely minimal impact, solely on SPI transfer time into RAM.
if (this->current_data_index_ == 0) {
this->command(CMD_TRANSFER);
}
return this->transfer_buffer_chunks_();
}
void EPaperJD79660::refresh_screen([[maybe_unused]] bool partial) {
ESP_LOGV(TAG, "Refresh");
this->cmd_data(CMD_REFRESH, {(uint8_t) 0x00});
}
void EPaperJD79660::power_off() {
ESP_LOGV(TAG, "Power off");
this->cmd_data(CMD_POWEROFF, {(uint8_t) 0x00});
}
void EPaperJD79660::deep_sleep() {
ESP_LOGV(TAG, "Deep sleep");
// "Deepsleep between update": Ensure EPD sleep to avoid early hardware wearout!
this->cmd_data(CMD_DEEPSLEEP, {(uint8_t) 0xA5});
// Notes:
// - VDD: Some boards (Waveshare) with "clever reset logic" would allow switching off
// EPD VDD by pulling reset pin low for longer time.
// However, a) not all boards have this, b) reliable sequence timing is difficult,
// c) saving is not worth it after deepsleep command above.
// If needed: Better option is to drive VDD via MOSFET with separate enable pin.
//
// - Possible safe shutdown:
// EPaperBase::on_safe_shutdown() may also trigger deep_sleep() again.
// Regularly, in IDLE state, this does not make sense for this "deepsleep between update" model,
// but SPI sequence should simply be ignored by sleeping receiver.
// But if triggering during lengthy update, this quick SPI sleep sequence may have benefit.
// Optimally, EPDs should even be set all white for longer storage.
// But full sequence (>15s) not possible w/o app logic.
}
} // namespace esphome::epaper_spi

View File

@@ -1,145 +0,0 @@
#pragma once
#include "epaper_spi.h"
namespace esphome::epaper_spi {
/**
* JD7966x IC driver implementation
*
* Currently tested with:
* - JD79660 (max res: 200x200)
*
* May also work for other JD7966x chipset family members with minimal adaptations.
*
* Capabilities:
* - HW frame buffer layout:
* 4 colors (gray0..3, commonly BWYR). Bytes consist of 4px/2bpp.
* Width must be rounded to multiple of 4.
* - Fast init/update (shorter wave forms): Yes. Controlled by CONF_FULL_UPDATE_EVERY.
* Needs undocumented fastinit sequence, based on likely vendor specific MTP content.
* - Partial transfer (transfer only changed window): No. Maybe possible by HW.
* - Partial refresh (refresh only changed window): No. Likely HW limit.
*
* @internal \c final saves few bytes by devirtualization. Remove \c final when subclassing.
*/
class EPaperJD79660 final : public EPaperBase {
public:
EPaperJD79660(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length, const uint8_t *fast_update, uint16_t fast_update_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR),
fast_update_(fast_update),
fast_update_length_(fast_update_length) {
this->row_width_ = (width + 3) / 4; // Fix base class calc (2bpp instead of 1bpp)
this->buffer_length_ = this->row_width_ * height;
}
void fill(Color color) override;
protected:
/** Draw colored pixel into frame buffer */
void draw_pixel_at(int x, int y, Color color) override;
/** Reset (multistep sequence)
* @pre this->reset_pin_ != nullptr // cv.Required check
* @post Should be idle on successful reset. Can mark failures.
*/
bool reset() override;
/** Initialise (multistep sequence) */
bool initialise(bool partial) override;
/** Buffer transfer */
bool transfer_data() override;
/** Power on: Already part of init sequence (likely needed there before transferring buffers).
* So nothing to do in FSM state.
*/
void power_on() override {}
/** Refresh screen
* @param partial Ignored: Needed earlier in \a ::initialize
* @pre Must be idle.
* @post Should return to idle later after processing.
*/
void refresh_screen([[maybe_unused]] bool partial) override;
/** Power off
* @pre Must be idle.
* @post Should return to idle later after processing.
* (latter will take long period like ~15-20s on actual refresh!)
*/
void power_off() override;
/** Deepsleep: Must be used to avoid hardware wearout!
* @pre Must be idle.
* @post Will go busy, and not return idle till ::reset!
*/
void deep_sleep() override;
/** Internal: Send fast init sequence via undocumented vendor registers
* @pre Must be directly after regular ::initialise sequence, before ::transfer_data
* @pre Must be idle.
* @post Should return to idle later after processing.
*/
void write_fastinit_();
/** Internal: Send raw buffer in chunks
* \retval true Finished
* \retval false Loop time elapsed. Need to call again next loop.
*/
bool transfer_buffer_chunks_();
/** @name IC commands @{ */
static constexpr uint8_t CMD_POWEROFF = 0x02;
static constexpr uint8_t CMD_DEEPSLEEP = 0x07;
static constexpr uint8_t CMD_TRANSFER = 0x10;
static constexpr uint8_t CMD_REFRESH = 0x12;
/** @} */
/** State machine constants for \a step_ */
enum class FSMState : uint8_t {
NONE = 0, //!< Initial/default value: Unused
/* Reset state steps */
RESET_STEP0_H,
RESET_STEP1_L,
RESET_STEP2_IDLECHECK,
/* Init state steps */
INIT_STEP0_REGULARINIT,
INIT_STEP1_FASTINIT,
};
/** Wait time (millisec) for first reset phase: High
*
* Wait via FSM loop.
*/
static constexpr uint16_t SLEEP_MS_RESET0 = 200;
/** Wait time (millisec) for second reset phase: Low
*
* Holding Reset Low too long may trigger "clever reset" logic
* of e.g. Waveshare Rev2 boards: VDD is shut down via MOSFET, and IC
* will not report idle anymore!
* FSM loop may spuriously increase delay, e.g. >16ms.
* Therefore, sync wait below, as allowed (code rule "delays > 10ms not permitted"),
* yet only slightly exceeding known IC min req of >1.5ms.
*/
static constexpr uint16_t SLEEP_MS_RESET1 = 2;
/** Wait time (millisec) for third reset phase: High
*
* Wait via FSM loop.
*/
static constexpr uint16_t SLEEP_MS_RESET2 = 200;
// properties initialised in the constructor
const uint8_t *const fast_update_{};
const uint16_t fast_update_length_{};
/** Counter for tracking substeps within FSM state */
FSMState step_{FSMState::NONE};
};
} // namespace esphome::epaper_spi

View File

@@ -4,7 +4,7 @@
namespace esphome::epaper_spi {
class EPaperSpectraE6 final : public EPaperBase {
class EPaperSpectraE6 : public EPaperBase {
public:
EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)

View File

@@ -6,7 +6,7 @@ namespace esphome::epaper_spi {
/**
* An epaper display that needs LUTs to be sent to it.
*/
class EpaperWaveshare final : public EPaperMono {
class EpaperWaveshare : public EPaperMono {
public:
EpaperWaveshare(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length, const uint8_t *lut, size_t lut_length, const uint8_t *partial_lut,

View File

@@ -1,86 +0,0 @@
import esphome.codegen as cg
from esphome.components.mipi import flatten_sequence
import esphome.config_validation as cv
from esphome.const import CONF_BUSY_PIN, CONF_RESET_PIN
from esphome.core import ID
from ..display import CONF_INIT_SEQUENCE_ID
from . import EpaperModel
class JD79660(EpaperModel):
def __init__(self, name, class_name="EPaperJD79660", fast_update=None, **kwargs):
super().__init__(name, class_name, **kwargs)
self.fast_update = fast_update
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
# Validate required pins, as C++ code will assume existence
if name in (CONF_RESET_PIN, CONF_BUSY_PIN):
return cv.Required(name)
# Delegate to parent
return super().option(name, fallback)
def get_constructor_args(self, config) -> tuple:
# Resembles init_sequence handling for fast_update config
if self.fast_update is None:
fast_update = cg.nullptr, 0
else:
flat_fast_update = flatten_sequence(self.fast_update)
fast_update = (
cg.static_const_array(
ID(
config[CONF_INIT_SEQUENCE_ID].id + "_fast_update", type=cg.uint8
),
flat_fast_update,
),
len(flat_fast_update),
)
return (*fast_update,)
jd79660 = JD79660(
"jd79660",
# Specified refresh times are ~20s (full) or ~15s (fast) due to BWRY.
# So disallow low update intervals (with safety margin), to avoid e.g. FSM update loops.
# Even less frequent intervals (min/h) highly recommended to optimize lifetime!
minimum_update_interval="30s",
# SPI rate: From spec comparisons, IC should allow SCL write cycles up to 10MHz rate.
# Existing code samples also prefer 10MHz. So justifies as default.
# Decrease value further in user config if needed (e.g. poor cabling).
data_rate="10MHz",
# No need to set optional reset_duration:
# Code requires multistep reset sequence with precise timings
# according to data sheet or samples.
)
# Waveshare 1.54-G
#
# Device may have specific factory provisioned MTP content to facilitate vendor register features like fast init.
# Vendor specific init derived from vendor sample code
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/1in54_e-Paper_G/ESP32/EPD_1in54g.cpp>
# Compatible MIT license, see esphome/LICENSE file.
#
# fmt: off
jd79660.extend(
"Waveshare-1.54in-G",
width=200,
height=200,
initsequence=(
(0x4D, 0x78,),
(0x00, 0x0F, 0x29,),
(0x06, 0x0d, 0x12, 0x30, 0x20, 0x19, 0x2a, 0x22,),
(0x50, 0x37,),
(0x61, 200 // 256, 200 % 256, 200 // 256, 200 % 256,), # RES: 200x200 fixed
(0xE9, 0x01,),
(0x30, 0x08,),
# Power On (0x04): Must be early part of init seq = Disabled later!
(0x04,),
),
fast_update=(
(0xE0, 0x02,),
(0xE6, 0x5D,),
(0xA5, 0x00,),
),
)

View File

@@ -68,7 +68,7 @@ void HOT Logger::write_msg_(const char *msg, uint16_t len) {
#ifdef CONFIG_PRINTK
// Requires the debug component and an active SWD connection.
// It is used for pyocd rtt -t nrf52840
printk("%.*s", static_cast<int>(len), msg);
k_str_out(const_cast<char *>(msg), len);
#endif
if (this->uart_dev_ == nullptr) {
return;

View File

@@ -396,9 +396,9 @@ static bool process_rolling_code(Provider &provider, PacketDecoder &decoder) {
/**
* Process a received packet
*/
void PacketTransport::process_(std::span<const uint8_t> data) {
void PacketTransport::process_(const std::vector<uint8_t> &data) {
auto ping_key_seen = !this->ping_pong_enable_;
PacketDecoder decoder(data.data(), data.size());
PacketDecoder decoder((data.data()), data.size());
char namebuf[256]{};
uint8_t byte;
FuData rdata{};

View File

@@ -9,9 +9,8 @@
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#include <map>
#include <span>
#include <vector>
#include <map>
/**
* Providing packet encoding functions for exchanging data with a remote host.
@@ -114,7 +113,7 @@ class PacketTransport : public PollingComponent {
virtual bool should_send() { return true; }
// to be called by child classes when a data packet is received.
void process_(std::span<const uint8_t> data);
void process_(const std::vector<uint8_t> &data);
void send_data_(bool all);
void flush_();
void add_data_(uint8_t key, const char *id, float data);

View File

@@ -13,9 +13,12 @@ void Pipsolar::setup() {
}
void Pipsolar::empty_uart_buffer_() {
uint8_t byte;
while (this->available()) {
this->read_byte(&byte);
uint8_t buf[64];
int avail;
while ((avail = this->available()) > 0) {
if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) {
break;
}
}
}
@@ -94,32 +97,47 @@ void Pipsolar::loop() {
}
if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) {
while (this->available()) {
uint8_t byte;
this->read_byte(&byte);
// make sure data and null terminator fit in buffer
if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) {
this->read_pos_ = 0;
this->empty_uart_buffer_();
ESP_LOGW(TAG, "response data too long, discarding.");
int avail = this->available();
while (avail > 0) {
uint8_t buf[64];
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
avail -= to_read;
bool done = false;
for (size_t i = 0; i < to_read; i++) {
uint8_t byte = buf[i];
// end of answer
if (byte == 0x0D) {
this->read_buffer_[this->read_pos_] = 0;
this->empty_uart_buffer_();
if (this->state_ == STATE_POLL) {
this->state_ = STATE_POLL_COMPLETE;
// make sure data and null terminator fit in buffer
if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) {
this->read_pos_ = 0;
this->empty_uart_buffer_();
ESP_LOGW(TAG, "response data too long, discarding.");
done = true;
break;
}
if (this->state_ == STATE_COMMAND) {
this->state_ = STATE_COMMAND_COMPLETE;
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
// end of answer
if (byte == 0x0D) {
this->read_buffer_[this->read_pos_] = 0;
this->empty_uart_buffer_();
if (this->state_ == STATE_POLL) {
this->state_ = STATE_POLL_COMPLETE;
}
if (this->state_ == STATE_COMMAND) {
this->state_ = STATE_COMMAND_COMPLETE;
}
done = true;
break;
}
}
} // available
if (done) {
break;
}
}
}
if (this->state_ == STATE_COMMAND) {
if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) {

View File

@@ -46,7 +46,6 @@ CONFIG_SCHEMA = (
RESTORE_MODES, upper=True
),
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda,
cv.Optional(CONF_TARGET_TEMPERATURE): cv.returning_lambda,
cv.Optional(CONF_MODE): cv.returning_lambda,
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
water_heater.validate_water_heater_mode
@@ -79,14 +78,6 @@ async def to_code(config: ConfigType) -> None:
)
cg.add(var.set_current_temperature_lambda(template_))
if CONF_TARGET_TEMPERATURE in config:
template_ = await cg.process_lambda(
config[CONF_TARGET_TEMPERATURE],
[],
return_type=cg.optional.template(cg.float_),
)
cg.add(var.set_target_temperature_lambda(template_))
if CONF_MODE in config:
template_ = await cg.process_lambda(
config[CONF_MODE],

View File

@@ -16,8 +16,7 @@ void TemplateWaterHeater::setup() {
restore->perform();
}
}
if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() &&
!this->mode_f_.has_value())
if (!this->current_temperature_f_.has_value() && !this->mode_f_.has_value())
this->disable_loop();
}
@@ -29,9 +28,6 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
}
traits.set_supports_current_temperature(true);
if (this->target_temperature_f_.has_value()) {
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE);
}
return traits;
}
@@ -46,14 +42,6 @@ void TemplateWaterHeater::loop() {
}
}
auto target_temp = this->target_temperature_f_.call();
if (target_temp.has_value()) {
if (*target_temp != this->target_temperature_) {
this->target_temperature_ = *target_temp;
changed = true;
}
}
auto new_mode = this->mode_f_.call();
if (new_mode.has_value()) {
if (*new_mode != this->mode_) {

View File

@@ -20,9 +20,6 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
template<typename F> void set_current_temperature_lambda(F &&f) {
this->current_temperature_f_.set(std::forward<F>(f));
}
template<typename F> void set_target_temperature_lambda(F &&f) {
this->target_temperature_f_.set(std::forward<F>(f));
}
template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
@@ -47,7 +44,6 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
// Ordered to minimize padding on 32-bit: 4-byte members first, then smaller
Trigger<> set_trigger_;
TemplateLambda<float> current_temperature_f_;
TemplateLambda<float> target_temperature_f_;
TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
water_heater::WaterHeaterModeMask supported_modes_;

View File

@@ -13,7 +13,7 @@ from esphome.components.packet_transport import (
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
from esphome.core import ID
from esphome.cpp_generator import MockObj
from esphome.cpp_generator import literal
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["network"]
@@ -23,12 +23,8 @@ MULTI_CONF = True
udp_ns = cg.esphome_ns.namespace("udp")
UDPComponent = udp_ns.class_("UDPComponent", cg.Component)
UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action)
trigger_argname = "data"
# Listener callback type (non-owning span from UDP component)
listener_args = cg.std_span.template(cg.uint8.operator("const"))
listener_argtype = [(listener_args, trigger_argname)]
# Automation/trigger type (owned vector, safe for deferred actions like delay)
trigger_args = cg.std_vector.template(cg.uint8)
trigger_argname = "data"
trigger_argtype = [(trigger_args, trigger_argname)]
CONF_ADDRESSES = "addresses"
@@ -122,13 +118,7 @@ async def to_code(config):
trigger_id, trigger_argtype, on_receive
)
trigger_lambda = await cg.process_lambda(
trigger.trigger(
cg.std_vector.template(cg.uint8)(
MockObj(trigger_argname).begin(),
MockObj(trigger_argname).end(),
)
),
listener_argtype,
trigger.trigger(literal(trigger_argname)), trigger_argtype
)
cg.add(var.add_listener(trigger_lambda))
cg.add(var.set_should_listen())

View File

@@ -12,7 +12,7 @@ bool UDPTransport::should_send() { return network::is_connected(); }
void UDPTransport::setup() {
PacketTransport::setup();
if (!this->providers_.empty() || this->is_encrypted_()) {
this->parent_->add_listener([this](std::span<const uint8_t> data) { this->process_(data); });
this->parent_->add_listener([this](std::vector<uint8_t> &buf) { this->process_(buf); });
}
}

View File

@@ -103,8 +103,8 @@ void UDPComponent::setup() {
}
void UDPComponent::loop() {
auto buf = std::vector<uint8_t>(MAX_PACKET_SIZE);
if (this->should_listen_) {
std::array<uint8_t, MAX_PACKET_SIZE> buf;
for (;;) {
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
auto len = this->listen_socket_->read(buf.data(), buf.size());
@@ -116,9 +116,9 @@ void UDPComponent::loop() {
#endif
if (len <= 0)
break;
size_t packet_len = static_cast<size_t>(len);
ESP_LOGV(TAG, "Received packet of length %zu", packet_len);
this->packet_listeners_.call(std::span<const uint8_t>(buf.data(), packet_len));
buf.resize(len);
ESP_LOGV(TAG, "Received packet of length %zu", len);
this->packet_listeners_.call(buf);
}
}
}

View File

@@ -10,9 +10,7 @@
#ifdef USE_SOCKET_IMPL_LWIP_TCP
#include <WiFiUdp.h>
#endif
#include <array>
#include <initializer_list>
#include <span>
#include <vector>
namespace esphome::udp {
@@ -28,7 +26,7 @@ class UDPComponent : public Component {
void set_broadcast_port(uint16_t port) { this->broadcast_port_ = port; }
void set_should_broadcast() { this->should_broadcast_ = true; }
void set_should_listen() { this->should_listen_ = true; }
void add_listener(std::function<void(std::span<const uint8_t>)> &&listener) {
void add_listener(std::function<void(std::vector<uint8_t> &)> &&listener) {
this->packet_listeners_.add(std::move(listener));
}
void setup() override;
@@ -43,7 +41,7 @@ class UDPComponent : public Component {
uint16_t broadcast_port_{};
bool should_broadcast_{};
bool should_listen_{};
CallbackManager<void(std::span<const uint8_t>)> packet_listeners_{};
CallbackManager<void(std::vector<uint8_t> &)> packet_listeners_{};
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
std::unique_ptr<socket::Socket> broadcast_socket_ = nullptr;

View File

@@ -12,7 +12,6 @@ std_shared_ptr = std_ns.class_("shared_ptr")
std_string = std_ns.class_("string")
std_string_ref = std_ns.namespace("string &")
std_vector = std_ns.class_("vector")
std_span = std_ns.class_("span")
uint8 = global_ns.namespace("uint8_t")
uint16 = global_ns.namespace("uint16_t")
uint32 = global_ns.namespace("uint32_t")

View File

@@ -317,7 +317,6 @@ class EsphomeCommandWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandl
# Check if the proc was not forcibly closed
_LOGGER.info("Process exited with return code %s", returncode)
self.write_message({"event": "exit", "code": returncode})
self.close()
def on_close(self) -> None:
# Check if proc exists (if 'start' has been run)

View File

@@ -25,22 +25,6 @@ display:
lambda: |-
it.circle(64, 64, 50, Color::BLACK);
- platform: epaper_spi
spi_id: spi_bus
model: waveshare-1.54in-G
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO17
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
- platform: epaper_spi
spi_id: spi_bus
model: waveshare-2.13in-v3

View File

@@ -412,7 +412,6 @@ water_heater:
name: "Template Water Heater"
optimistic: true
current_temperature: !lambda "return 42.0f;"
target_temperature: !lambda "return 60.0f;"
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
supported_modes:
- "OFF"

View File

@@ -29,7 +29,7 @@ from esphome.dashboard.entries import (
bool_to_entry_state,
)
from esphome.dashboard.models import build_importable_device_dict
from esphome.dashboard.web_server import DashboardSubscriber, EsphomeCommandWebSocket
from esphome.dashboard.web_server import DashboardSubscriber
from esphome.zeroconf import DiscoveredImport
from .common import get_fixture_path
@@ -1654,25 +1654,3 @@ async def test_websocket_check_origin_multiple_trusted_domains(
assert data["event"] == "initial_state"
finally:
ws.close()
def test_proc_on_exit_calls_close() -> None:
"""Test _proc_on_exit sends exit event and closes the WebSocket."""
handler = Mock(spec=EsphomeCommandWebSocket)
handler._is_closed = False
EsphomeCommandWebSocket._proc_on_exit(handler, 0)
handler.write_message.assert_called_once_with({"event": "exit", "code": 0})
handler.close.assert_called_once()
def test_proc_on_exit_skips_when_already_closed() -> None:
"""Test _proc_on_exit does nothing when WebSocket is already closed."""
handler = Mock(spec=EsphomeCommandWebSocket)
handler._is_closed = True
EsphomeCommandWebSocket._proc_on_exit(handler, 0)
handler.write_message.assert_not_called()
handler.close.assert_not_called()

View File

@@ -10,7 +10,6 @@ water_heater:
name: Test Boiler
optimistic: true
current_temperature: !lambda "return 45.0f;"
target_temperature: !lambda "return 60.0f;"
# Note: No mode lambda - we want optimistic mode changes to stick
# A mode lambda would override mode changes in loop()
supported_modes:

View File

@@ -93,34 +93,23 @@ async def udp_listener(port: int = 0) -> AsyncGenerator[tuple[int, UDPReceiver]]
sock.close()
def _get_free_udp_port() -> int:
"""Get a free UDP port by binding to port 0 and releasing."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()
return port
@pytest.mark.asyncio
async def test_udp_send_receive(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test UDP component can send and receive messages."""
"""Test UDP component can send messages with multiple addresses configured."""
# Track log lines to verify dump_config output
log_lines: list[str] = []
receive_event = asyncio.Event()
def on_log_line(line: str) -> None:
log_lines.append(line)
if "Received UDP:" in line:
receive_event.set()
async with udp_listener() as (broadcast_port, receiver):
listen_port = _get_free_udp_port()
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(listen_port))
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(broadcast_port))
async with udp_listener() as (udp_port, receiver):
# Replace placeholders in the config
config = yaml_config.replace("UDP_LISTEN_PORT_PLACEHOLDER", str(udp_port + 1))
config = config.replace("UDP_BROADCAST_PORT_PLACEHOLDER", str(udp_port))
async with (
run_compiled(config, line_callback=on_log_line),
@@ -180,19 +169,3 @@ async def test_udp_send_receive(
assert "Address: 127.0.0.2" in log_text, (
f"Address 127.0.0.2 not found in dump_config. Log: {log_text[-2000:]}"
)
# Test receiving a UDP packet (exercises on_receive with std::span)
test_payload = b"TEST_RECEIVE_UDP"
send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
send_sock.sendto(test_payload, ("127.0.0.1", listen_port))
finally:
send_sock.close()
try:
await asyncio.wait_for(receive_event.wait(), timeout=5.0)
except TimeoutError:
pytest.fail(
f"on_receive did not fire. Expected 'Received UDP:' in logs. "
f"Last log lines: {log_lines[-20:]}"
)

View File

@@ -85,9 +85,6 @@ async def test_water_heater_template(
assert initial_state.current_temperature == 45.0, (
f"Expected current temp 45.0, got {initial_state.current_temperature}"
)
assert initial_state.target_temperature == 60.0, (
f"Expected target temp 60.0, got {initial_state.target_temperature}"
)
# Test changing to GAS mode
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS)