Merge branch 'logger_raii' into integration

This commit is contained in:
J. Nick Koston
2026-01-13 13:40:25 -10:00
28 changed files with 846 additions and 111 deletions

View File

@@ -187,6 +187,7 @@ async def to_code(config):
height,
init_sequence_id,
init_sequence_length,
*model.get_constructor_args(config),
)
# Rotation is handled by setting the transform

View File

@@ -54,20 +54,14 @@ void EPaperBase::setup_pins_() const {
float EPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; }
void EPaperBase::command(uint8_t value) {
this->start_command_();
ESP_LOGV(TAG, "Command: 0x%02X", value);
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(value);
this->end_command_();
}
void EPaperBase::data(uint8_t value) {
this->start_data_();
this->write_byte(value);
this->end_data_();
this->disable();
}
// write a command followed by zero or more bytes of data.
// The command is the first byte, length is the length of data only in the second byte, followed by the data.
// [COMMAND, LENGTH, DATA...]
void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(EPAPER_MAX_CMD_LOG_BYTES)];
@@ -130,14 +124,10 @@ void EPaperBase::wait_for_idle_(bool should_wait) {
void EPaperBase::loop() {
auto now = millis();
if (this->delay_until_ != 0) {
// using modulus arithmetic to handle wrap-around
int diff = now - this->delay_until_;
if (diff < 0) {
return;
}
this->delay_until_ = 0;
}
// using modulus arithmetic to handle wrap-around
int diff = now - this->delay_until_;
if (diff < 0)
return;
if (this->waiting_for_idle_) {
if (this->is_idle_()) {
this->waiting_for_idle_ = false;
@@ -192,7 +182,7 @@ void EPaperBase::process_state_() {
this->set_state_(EPaperState::RESET);
break;
case EPaperState::INITIALISE:
this->initialise_();
this->initialise(this->update_count_ != 0);
this->set_state_(EPaperState::TRANSFER_DATA);
break;
case EPaperState::TRANSFER_DATA:
@@ -230,11 +220,11 @@ void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_());
this->state_ = state;
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
if (delay != 0) {
this->delay_until_ = millis() + delay;
} else {
this->delay_until_ = 0;
}
// allow subclasses to nominate delays
if (delay == 0)
delay = this->next_delay_;
this->next_delay_ = 0;
this->delay_until_ = millis() + delay;
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
TRUEFALSE(this->waiting_for_idle_));
if (state == EPaperState::IDLE) {
@@ -242,22 +232,14 @@ void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
}
}
void EPaperBase::start_command_() {
this->dc_pin_->digital_write(false);
this->enable();
}
void EPaperBase::end_command_() { this->disable(); }
void EPaperBase::start_data_() {
this->dc_pin_->digital_write(true);
this->enable();
}
void EPaperBase::end_data_() { this->disable(); }
void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
void EPaperBase::initialise_() {
void EPaperBase::initialise(bool partial) {
size_t index = 0;
auto *sequence = this->init_sequence_;
@@ -317,9 +299,8 @@ bool EPaperBase::rotate_coordinates_(int &x, int &y) {
void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
if (!rotate_coordinates_(x, y))
return;
const size_t pixel_position = y * this->width_ + x;
const size_t byte_position = pixel_position / 8;
const uint8_t bit_position = pixel_position % 8;
const size_t byte_position = y * this->row_width_ + x / 8;
const uint8_t bit_position = x % 8;
const uint8_t pixel_bit = 0x80 >> bit_position;
const auto original = this->buffer_[byte_position];
if ((color_to_bit(color) == 0)) {

View File

@@ -36,14 +36,16 @@ class EPaperBase : public Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_2MHZ> {
public:
EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY)
EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence = nullptr,
size_t init_sequence_length = 0, DisplayType display_type = DISPLAY_TYPE_BINARY)
: name_(name),
width_(width),
height_(height),
init_sequence_(init_sequence),
init_sequence_length_(init_sequence_length),
display_type_(display_type) {}
display_type_(display_type) {
this->row_width_ = (this->width_ + 7) / 8; // width of a row in bytes
}
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
float get_setup_priority() const override;
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
@@ -54,9 +56,13 @@ class EPaperBase : public Display,
void dump_config() override;
void command(uint8_t value);
void data(uint8_t value);
void cmd_data(uint8_t command, const uint8_t *ptr, size_t length);
// variant with in-place initializer list
void cmd_data(uint8_t command, std::initializer_list<uint8_t> data) {
this->cmd_data(command, data.begin(), data.size());
}
void update() override;
void loop() override;
@@ -109,7 +115,7 @@ class EPaperBase : public Display,
bool is_idle_() const;
void setup_pins_() const;
virtual bool reset();
void initialise_();
virtual void initialise(bool partial);
void wait_for_idle_(bool should_wait);
bool init_buffer_(size_t buffer_length);
bool rotate_coordinates_(int &x, int &y);
@@ -143,14 +149,12 @@ class EPaperBase : public Display,
void set_state_(EPaperState state, uint16_t delay = 0);
void start_command_();
void end_command_();
void start_data_();
void end_data_();
// properties initialised in the constructor
const char *name_;
uint16_t width_;
uint16_t row_width_; // width of a row in bytes
uint16_t height_;
const uint8_t *init_sequence_;
size_t init_sequence_length_;
@@ -163,7 +167,8 @@ class EPaperBase : public Display,
GPIOPin *busy_pin_{};
GPIOPin *reset_pin_{};
bool waiting_for_idle_{};
uint32_t delay_until_{};
uint32_t delay_until_{}; // timestamp until which to delay processing
uint16_t next_delay_{}; // milliseconds to delay before next state
uint8_t transform_{};
uint8_t update_count_{};
// these values represent the bounds of the updated buffer. Note that x_high and y_high

View File

@@ -1,25 +1,24 @@
#include "epaper_spi_ssd1677.h"
#include "epaper_spi_mono.h"
#include <algorithm>
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.ssd1677";
static constexpr const char *const TAG = "epaper_spi.mono";
void EPaperSSD1677::refresh_screen(bool partial) {
void EPaperMono::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh screen");
this->command(0x22);
this->data(partial ? 0xFF : 0xF7);
this->cmd_data(0x22, {partial ? (uint8_t) 0xFF : (uint8_t) 0xF7});
this->command(0x20);
}
void EPaperSSD1677::deep_sleep() {
void EPaperMono::deep_sleep() {
ESP_LOGV(TAG, "Deep sleep");
this->command(0x10);
}
bool EPaperSSD1677::reset() {
bool EPaperMono::reset() {
if (EPaperBase::reset()) {
this->command(0x12);
return true;
@@ -27,29 +26,24 @@ bool EPaperSSD1677::reset() {
return false;
}
bool HOT EPaperSSD1677::transfer_data() {
void EPaperMono::set_window() {
// round x-coordinates to byte boundaries
this->x_low_ &= ~7;
this->x_high_ += 7;
this->x_high_ &= ~7;
this->cmd_data(0x44, {(uint8_t) this->x_low_, (uint8_t) (this->x_low_ / 256), (uint8_t) (this->x_high_ - 1),
(uint8_t) ((this->x_high_ - 1) / 256)});
this->cmd_data(0x4E, {(uint8_t) this->x_low_, (uint8_t) (this->x_low_ / 256)});
this->cmd_data(0x45, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256), (uint8_t) (this->y_high_ - 1),
(uint8_t) ((this->y_high_ - 1) / 256)});
this->cmd_data(0x4F, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256)});
}
bool HOT EPaperMono::transfer_data() {
auto start_time = millis();
if (this->current_data_index_ == 0) {
uint8_t data[4]{};
// round to byte boundaries
this->x_low_ &= ~7;
this->y_low_ &= ~7;
this->x_high_ += 7;
this->x_high_ &= ~7;
this->y_high_ += 7;
this->y_high_ &= ~7;
data[0] = this->x_low_;
data[1] = this->x_low_ / 256;
data[2] = this->x_high_ - 1;
data[3] = (this->x_high_ - 1) / 256;
cmd_data(0x4E, data, 2);
cmd_data(0x44, data, sizeof(data));
data[0] = this->y_low_;
data[1] = this->y_low_ / 256;
data[2] = this->y_high_ - 1;
data[3] = (this->y_high_ - 1) / 256;
cmd_data(0x4F, data, 2);
this->cmd_data(0x45, data, sizeof(data));
this->set_window();
// for monochrome, we still need to clear the red data buffer at least once to prevent it
// causing dirty pixels after partial refresh.
this->command(this->send_red_ ? 0x26 : 0x24);
@@ -58,10 +52,10 @@ bool HOT EPaperSSD1677::transfer_data() {
size_t row_length = (this->x_high_ - this->x_low_) / 8;
FixedVector<uint8_t> bytes_to_send{};
bytes_to_send.init(row_length);
ESP_LOGV(TAG, "Writing bytes at line %zu at %ums", this->current_data_index_, (unsigned) millis());
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
this->start_data_();
while (this->current_data_index_ != this->y_high_) {
size_t data_idx = (this->current_data_index_ * this->width_ + this->x_low_) / 8;
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_ / 8;
for (size_t i = 0; i != row_length; i++) {
bytes_to_send[i] = this->send_red_ ? 0 : this->buffer_[data_idx++];
}
@@ -69,12 +63,12 @@ bool HOT EPaperSSD1677::transfer_data() {
this->write_array(&bytes_to_send.front(), row_length); // NOLINT
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
this->end_data_();
this->disable();
return false;
}
}
this->end_data_();
this->disable();
this->current_data_index_ = 0;
if (this->send_red_) {
this->send_red_ = false;

View File

@@ -3,13 +3,15 @@
#include "epaper_spi.h"
namespace esphome::epaper_spi {
class EPaperSSD1677 : public EPaperBase {
/**
* A class for monochrome epaper displays.
*/
class EPaperMono : public EPaperBase {
public:
EPaperSSD1677(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
EPaperMono(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {
this->buffer_length_ = width * height / 8; // 8 pixels per byte
this->buffer_length_ = (width + 7) / 8 * height; // 8 pixels per byte, rounded up
}
protected:
@@ -18,6 +20,7 @@ class EPaperSSD1677 : public EPaperBase {
void power_off() override{};
void deep_sleep() override;
bool reset() override;
virtual void set_window();
bool transfer_data() override;
bool send_red_{true};
};

View File

@@ -80,20 +80,17 @@ void EPaperSpectraE6::power_on() {
void EPaperSpectraE6::power_off() {
ESP_LOGV(TAG, "Power off");
this->command(0x02);
this->data(0x00);
this->cmd_data(0x02, {0x00});
}
void EPaperSpectraE6::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh");
this->command(0x12);
this->data(0x00);
this->cmd_data(0x12, {0x00});
}
void EPaperSpectraE6::deep_sleep() {
ESP_LOGV(TAG, "Deep sleep");
this->command(0x07);
this->data(0xA5);
this->cmd_data(0x07, {0xA5});
}
void EPaperSpectraE6::fill(Color color) {
@@ -143,7 +140,7 @@ bool HOT EPaperSpectraE6::transfer_data() {
if (buf_idx == sizeof bytes_to_send) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->end_data_();
this->disable();
ESP_LOGV(TAG, "Wrote %d bytes at %ums", buf_idx, (unsigned) millis());
buf_idx = 0;
@@ -157,7 +154,7 @@ bool HOT EPaperSpectraE6::transfer_data() {
if (buf_idx != 0) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->end_data_();
this->disable();
}
this->current_data_index_ = 0;
return true;

View File

@@ -0,0 +1,47 @@
#include "epaper_waveshare.h"
namespace esphome::epaper_spi {
static const char *const TAG = "epaper_spi.waveshare";
void EpaperWaveshare::initialise(bool partial) {
EPaperBase::initialise(partial);
if (partial) {
this->cmd_data(0x32, this->partial_lut_, this->partial_lut_length_);
this->cmd_data(0x3C, {0x80});
this->cmd_data(0x22, {0xC0});
this->command(0x20);
this->next_delay_ = 100;
} else {
this->cmd_data(0x32, this->lut_, this->lut_length_);
this->cmd_data(0x3C, {0x05});
}
this->send_red_ = true;
}
void EpaperWaveshare::set_window() {
this->x_low_ &= ~7;
this->x_high_ += 7;
this->x_high_ &= ~7;
uint16_t x_start = this->x_low_ / 8;
uint16_t x_end = (this->x_high_ - 1) / 8;
this->cmd_data(0x44, {(uint8_t) x_start, (uint8_t) (x_end)});
this->cmd_data(0x4E, {(uint8_t) x_start});
this->cmd_data(0x45, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256), (uint8_t) (this->y_high_ - 1),
(uint8_t) ((this->y_high_ - 1) / 256)});
this->cmd_data(0x4F, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256)});
ESP_LOGV(TAG, "Set window X: %u-%u, Y: %u-%u", this->x_low_, this->x_high_, this->y_low_, this->y_high_);
}
void EpaperWaveshare::refresh_screen(bool partial) {
if (partial) {
this->cmd_data(0x22, {0x0F});
} else {
this->cmd_data(0x22, {0xC7});
}
this->command(0x20);
this->next_delay_ = partial ? 100 : 3000;
}
void EpaperWaveshare::deep_sleep() { this->cmd_data(0x10, {0x01}); }
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,30 @@
#pragma once
#include "epaper_spi.h"
#include "epaper_spi_mono.h"
namespace esphome::epaper_spi {
/**
* An epaper display that needs LUTs to be sent to it.
*/
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,
uint16_t partial_lut_length)
: EPaperMono(name, width, height, init_sequence, init_sequence_length),
lut_(lut),
lut_length_(lut_length),
partial_lut_(partial_lut),
partial_lut_length_(partial_lut_length) {}
protected:
void initialise(bool partial) override;
void set_window() override;
void refresh_screen(bool partial) override;
void deep_sleep() override;
const uint8_t *lut_;
size_t lut_length_;
const uint8_t *partial_lut_;
uint16_t partial_lut_length_;
};
} // namespace esphome::epaper_spi

View File

@@ -32,6 +32,9 @@ class EpaperModel:
return cv.Required(name)
return cv.Optional(name, default=self.get_default(name, fallback))
def get_constructor_args(self, config) -> tuple:
return ()
def get_dimensions(self, config) -> tuple[int, int]:
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is

View File

@@ -4,10 +4,9 @@ from . import EpaperModel
class SSD1677(EpaperModel):
def __init__(self, name, class_name="EPaperSSD1677", **kwargs):
if CONF_DATA_RATE not in kwargs:
kwargs[CONF_DATA_RATE] = "20MHz"
super().__init__(name, class_name, **kwargs)
def __init__(self, name, class_name="EPaperMono", data_rate="20MHz", **defaults):
defaults[CONF_DATA_RATE] = data_rate
super().__init__(name, class_name, **defaults)
# fmt: off
def get_init_sequence(self, config: dict):
@@ -23,11 +22,15 @@ class SSD1677(EpaperModel):
ssd1677 = SSD1677("ssd1677")
ssd1677.extend(
"seeed-ee04-mono-4.26",
wave_4_26 = ssd1677.extend(
"waveshare-4.26in",
width=800,
height=480,
mirror_x=True,
)
wave_4_26.extend(
"seeed-ee04-mono-4.26",
cs_pin=44,
dc_pin=10,
reset_pin=38,

View File

@@ -0,0 +1,88 @@
import esphome.codegen as cg
from esphome.core import ID
from ..display import CONF_INIT_SEQUENCE_ID
from . import EpaperModel
class WaveshareModel(EpaperModel):
def __init__(self, name, lut, lut_partial=None, **defaults):
super().__init__(name, "EpaperWaveshare", **defaults)
self.lut = lut
self.lut_partial = lut_partial
def get_constructor_args(self, config) -> tuple:
lut = (
cg.static_const_array(
ID(config[CONF_INIT_SEQUENCE_ID].id + "_lut", type=cg.uint8), self.lut
),
len(self.lut),
)
if self.lut_partial is None:
lut_partial = cg.nullptr, 0
else:
lut_partial = (
cg.static_const_array(
ID(
config[CONF_INIT_SEQUENCE_ID].id + "_lut_partial", type=cg.uint8
),
self.lut_partial,
),
len(self.lut_partial),
)
return *lut, *lut_partial
# fmt: off
WaveshareModel(
"waveshare-2.13in-v3",
width=122,
height=250,
initsequence=(
(0x01, 0x27, 0x01, 0x00), # driver output control
(0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00),
(0x11, 0x03), # Data entry mode
(0x3F, 0x22), # Undocumented command
(0x2C, 0x36), # write VCOM register
(0x04, 0x41, 0x0C, 0x32), # SRC voltage
(0x03, 0x17), # Gate voltage
(0x21, 0x00, 0x80), # Display update control
(0x18, 0x80), # Select internal temperature sensor
),
lut=(
0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x40, 0x4A, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x80, 0x4A, 0x40, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x4A, 0x80, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF, 0x0, 0x0,
0xF, 0x0, 0x0, 0x2, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
0x0, 0x0, 0x0,
),
lut_partial=(
0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x40, 0x40, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
0x0, 0x0, 0x0,
),
)

View File

@@ -8,6 +8,7 @@
#include <esp_app_desc.h>
#include <esp_hosted.h>
#include <esp_hosted_host_fw_ver.h>
#include <esp_ota_ops.h>
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
#include "esphome/components/json/json_util.h"
@@ -442,6 +443,12 @@ void Esp32HostedUpdate::perform(bool force) {
this->status_clear_error();
this->publish_state();
#ifdef USE_OTA_ROLLBACK
// Mark the host partition as valid before rebooting, in case the safe mode
// timer hasn't expired yet.
esp_ota_mark_app_valid_cancel_rollback();
#endif
// Schedule a restart to ensure everything is in sync
ESP_LOGI(TAG, "Restarting in 1 second");
this->set_timeout(1000, []() { App.safe_reboot(); });

View File

@@ -340,8 +340,8 @@ void I2SAudioSpeaker::speaker_task(void *params) {
const uint32_t read_delay =
(this_speaker->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2;
uint8_t *new_data = transfer_buffer->get_buffer_end(); // track start of any newly copied bytes
size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay));
uint8_t *new_data = transfer_buffer->get_buffer_end() - bytes_read;
if (bytes_read > 0) {
if (this_speaker->q15_volume_factor_ < INT16_MAX) {

View File

@@ -236,6 +236,8 @@ class Logger : public Component {
~RecursionGuard() { flag_ = false; }
RecursionGuard(const RecursionGuard &) = delete;
RecursionGuard &operator=(const RecursionGuard &) = delete;
RecursionGuard(RecursionGuard &&) = delete;
RecursionGuard &operator=(RecursionGuard &&) = delete;
private:
bool &flag_;
@@ -466,6 +468,8 @@ class Logger : public Component {
~NonMainTaskRecursionGuard() { pthread_setspecific(key_, nullptr); }
NonMainTaskRecursionGuard(const NonMainTaskRecursionGuard &) = delete;
NonMainTaskRecursionGuard &operator=(const NonMainTaskRecursionGuard &) = delete;
NonMainTaskRecursionGuard(NonMainTaskRecursionGuard &&) = delete;
NonMainTaskRecursionGuard &operator=(NonMainTaskRecursionGuard &&) = delete;
private:
pthread_key_t key_;

View File

@@ -14,6 +14,13 @@ namespace ota {
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::IDFOTABackend>(); }
OTAResponseTypes IDFOTABackend::begin(size_t image_size) {
#ifdef USE_OTA_ROLLBACK
// If we're starting an OTA, the current boot is good enough - mark it valid
// to prevent rollback and allow the OTA to proceed even if the safe mode
// timer hasn't expired yet.
esp_ota_mark_app_valid_cancel_rollback();
#endif
this->partition_ = esp_ota_get_next_update_partition(nullptr);
if (this->partition_ == nullptr) {
return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION;

View File

@@ -1,7 +1,7 @@
from esphome import automation
from esphome.automation import Condition, maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
from esphome.components import mqtt, web_server, zigbee
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
@@ -74,6 +74,7 @@ validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True)
_SWITCH_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(zigbee.SWITCH_SCHEMA)
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent),
@@ -103,6 +104,7 @@ _SWITCH_SCHEMA = (
_SWITCH_SCHEMA.add_extra(entity_duplicate_validator("switch"))
_SWITCH_SCHEMA.add_extra(zigbee.validate_switch)
def switch_schema(
@@ -165,6 +167,7 @@ async def setup_switch_core_(var, config):
cg.add(var.set_device_class(device_class))
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
await zigbee.setup_switch(var, config)
async def register_switch(var, config):

View File

@@ -23,7 +23,7 @@ from .const_zephyr import (
ZigbeeComponent,
zigbee_ns,
)
from .zigbee_zephyr import zephyr_binary_sensor, zephyr_sensor
from .zigbee_zephyr import zephyr_binary_sensor, zephyr_sensor, zephyr_switch
_LOGGER = logging.getLogger(__name__)
@@ -41,6 +41,7 @@ def zigbee_set_core_data(config: ConfigType) -> ConfigType:
BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_binary_sensor)
SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_sensor)
SWITCH_SCHEMA = cv.Schema({}).extend(zephyr_switch)
CONFIG_SCHEMA = cv.All(
cv.Schema(
@@ -107,6 +108,15 @@ async def setup_sensor(entity: cg.MockObj, config: ConfigType) -> None:
await zephyr_setup_sensor(entity, config)
async def setup_switch(entity: cg.MockObj, config: ConfigType) -> None:
if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL):
return
if CORE.using_zephyr:
from .zigbee_zephyr import zephyr_setup_switch
await zephyr_setup_switch(entity, config)
def consume_endpoint(config: ConfigType) -> ConfigType:
if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL):
return config
@@ -130,6 +140,10 @@ def validate_sensor(config: ConfigType) -> ConfigType:
return consume_endpoint(config)
def validate_switch(config: ConfigType) -> ConfigType:
return consume_endpoint(config)
ZIGBEE_ACTION_SCHEMA = automation.maybe_simple_id(
cv.Schema(
{

View File

@@ -11,6 +11,7 @@ CONF_ON_JOIN = "on_join"
CONF_WIPE_ON_BOOT = "wipe_on_boot"
CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor"
CONF_ZIGBEE_SENSOR = "zigbee_sensor"
CONF_ZIGBEE_SWITCH = "zigbee_switch"
CONF_POWER_SOURCE = "power_source"
POWER_SOURCE = {
"UNKNOWN": "ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN",
@@ -35,3 +36,4 @@ ZB_ZCL_CLUSTER_ID_BASIC = "ZB_ZCL_CLUSTER_ID_BASIC"
ZB_ZCL_CLUSTER_ID_IDENTIFY = "ZB_ZCL_CLUSTER_ID_IDENTIFY"
ZB_ZCL_CLUSTER_ID_BINARY_INPUT = "ZB_ZCL_CLUSTER_ID_BINARY_INPUT"
ZB_ZCL_CLUSTER_ID_ANALOG_INPUT = "ZB_ZCL_CLUSTER_ID_ANALOG_INPUT"
ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT = "ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT"

View File

@@ -0,0 +1,111 @@
#include "zigbee_switch_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_SWITCH)
#include "esphome/core/log.h"
#include <zephyr/settings/settings.h>
extern "C" {
#include <zboss_api.h>
#include <zboss_api_addons.h>
#include <zb_nrf_platform.h>
#include <zigbee/zigbee_app_utils.h>
#include <zb_error_to_string.h>
}
namespace esphome::zigbee {
static const char *const TAG = "zigbee_on_off.switch";
void ZigbeeSwitch::dump_config() {
ESP_LOGCONFIG(TAG,
"Zigbee Switch\n"
" Endpoint: %d, present_value %u",
this->endpoint_, this->cluster_attributes_->present_value);
}
void ZigbeeSwitch::setup() {
this->parent_->add_callback(this->endpoint_, [this](zb_bufid_t bufid) { this->zcl_device_cb_(bufid); });
this->switch_->add_on_state_callback([this](bool state) {
this->cluster_attributes_->present_value = state ? ZB_TRUE : ZB_FALSE;
ESP_LOGD(TAG, "Set attribute endpoint: %d, present_value %d", this->endpoint_,
this->cluster_attributes_->present_value);
ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_CLUSTER_SERVER_ROLE,
ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID, &this->cluster_attributes_->present_value,
ZB_FALSE);
this->parent_->flush();
});
}
void ZigbeeSwitch::zcl_device_cb_(zb_bufid_t bufid) {
zb_zcl_device_callback_param_t *p_device_cb_param = ZB_BUF_GET_PARAM(bufid, zb_zcl_device_callback_param_t);
zb_zcl_device_callback_id_t device_cb_id = p_device_cb_param->device_cb_id;
zb_uint16_t cluster_id = p_device_cb_param->cb_param.set_attr_value_param.cluster_id;
zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id;
p_device_cb_param->status = RET_OK;
switch (device_cb_id) {
/* ZCL set attribute value */
case ZB_ZCL_SET_ATTR_VALUE_CB_ID:
if (cluster_id == ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT) {
uint8_t value = p_device_cb_param->cb_param.set_attr_value_param.values.data8;
ESP_LOGI(TAG, "Binary output attribute setting to %hd", value);
if (attr_id == ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID) {
this->defer([this, value]() {
this->cluster_attributes_->present_value = value ? ZB_TRUE : ZB_FALSE;
this->switch_->publish_state(value);
});
}
} else {
/* other clusters attribute handled here */
ESP_LOGI(TAG, "Unhandled cluster attribute id: %d", cluster_id);
}
break;
default:
p_device_cb_param->status = RET_ERROR;
break;
}
ESP_LOGD(TAG, "%s status: %hd", __func__, p_device_cb_param->status);
}
const zb_uint8_t ZB_ZCL_BINARY_OUTPUT_STATUS_FLAG_MAX_VALUE = 0x0F;
static zb_ret_t check_value_binary_output_server(zb_uint16_t attr_id, zb_uint8_t endpoint,
zb_uint8_t *value) { // NOLINT(readability-non-const-parameter)
zb_ret_t ret = RET_OK;
ZVUNUSED(endpoint);
switch (attr_id) {
case ZB_ZCL_ATTR_BINARY_OUTPUT_OUT_OF_SERVICE_ID:
case ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID:
ret = ZB_ZCL_CHECK_BOOL_VALUE(*value) ? RET_OK : RET_ERROR;
break;
case ZB_ZCL_ATTR_BINARY_OUTPUT_STATUS_FLAG_ID:
if (*value > ZB_ZCL_BINARY_OUTPUT_STATUS_FLAG_MAX_VALUE) {
ret = RET_ERROR;
}
break;
default:
break;
}
return ret;
}
} // namespace esphome::zigbee
void zb_zcl_binary_output_init_server() {
zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_CLUSTER_SERVER_ROLE,
esphome::zigbee::check_value_binary_output_server,
(zb_zcl_cluster_write_attr_hook_t) NULL, (zb_zcl_cluster_handler_t) NULL);
}
void zb_zcl_binary_output_init_client() {
zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_CLUSTER_CLIENT_ROLE,
(zb_zcl_cluster_check_value_t) NULL, (zb_zcl_cluster_write_attr_hook_t) NULL,
(zb_zcl_cluster_handler_t) NULL);
}
#endif

View File

@@ -0,0 +1,83 @@
#pragma once
#include "esphome/components/zigbee/zigbee_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_SWITCH)
#include "esphome/core/component.h"
#include "esphome/components/switch/switch.h"
extern "C" {
#include <zboss_api.h>
#include <zboss_api_addons.h>
}
#define ZB_ZCL_BINARY_OUTPUT_CLUSTER_REVISION_DEFAULT ((zb_uint16_t) 0x0001u)
// NOLINTNEXTLINE(readability-identifier-naming)
enum zb_zcl_binary_output_attr_e {
ZB_ZCL_ATTR_BINARY_OUTPUT_DESCRIPTION_ID = 0x001C,
ZB_ZCL_ATTR_BINARY_OUTPUT_OUT_OF_SERVICE_ID = 0x0051,
ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID = 0x0055,
ZB_ZCL_ATTR_BINARY_OUTPUT_STATUS_FLAG_ID = 0x006F,
};
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_BINARY_OUTPUT_OUT_OF_SERVICE_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_BINARY_OUTPUT_OUT_OF_SERVICE_ID, ZB_ZCL_ATTR_TYPE_BOOL, \
ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \
(void *) (data_ptr) \
}
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID, ZB_ZCL_ATTR_TYPE_BOOL, \
ZB_ZCL_ATTR_ACCESS_READ_WRITE | ZB_ZCL_ATTR_ACCESS_REPORTING, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \
(void *) (data_ptr) \
}
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_BINARY_OUTPUT_STATUS_FLAG_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_BINARY_OUTPUT_STATUS_FLAG_ID, ZB_ZCL_ATTR_TYPE_8BITMAP, \
ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_REPORTING, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \
(void *) (data_ptr) \
}
#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_BINARY_OUTPUT_DESCRIPTION_ID(data_ptr) \
{ \
ZB_ZCL_ATTR_BINARY_OUTPUT_DESCRIPTION_ID, ZB_ZCL_ATTR_TYPE_CHAR_STRING, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \
(ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \
}
#define ESPHOME_ZB_ZCL_DECLARE_BINARY_OUTPUT_ATTRIB_LIST(attr_list, out_of_service, present_value, status_flag, \
description) \
ZB_ZCL_START_DECLARE_ATTRIB_LIST_CLUSTER_REVISION(attr_list, ZB_ZCL_BINARY_OUTPUT) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_OUTPUT_OUT_OF_SERVICE_ID, (out_of_service)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID, (present_value)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_OUTPUT_STATUS_FLAG_ID, (status_flag)) \
ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_BINARY_OUTPUT_DESCRIPTION_ID, (description)) \
ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST
void zb_zcl_binary_output_init_server();
void zb_zcl_binary_output_init_client();
#define ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT_SERVER_ROLE_INIT zb_zcl_binary_output_init_server
#define ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT_CLIENT_ROLE_INIT zb_zcl_binary_output_init_client
namespace esphome::zigbee {
class ZigbeeSwitch : public ZigbeeEntity, public Component {
public:
ZigbeeSwitch(switch_::Switch *s) : switch_(s) {}
void set_cluster_attributes(BinaryAttrs &cluster_attributes) { this->cluster_attributes_ = &cluster_attributes; }
void setup() override;
void dump_config() override;
protected:
void zcl_device_cb_(zb_bufid_t bufid);
BinaryAttrs *cluster_attributes_{nullptr};
switch_::Switch *switch_;
};
} // namespace esphome::zigbee
#endif

View File

@@ -104,9 +104,15 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) {
ESP_LOGI(TAG, "Zcl_device_cb %s id %hd, cluster_id %d, attr_id %d, endpoint: %d", __func__, device_cb_id, cluster_id,
attr_id, endpoint);
/* Set default response value. */
p_device_cb_param->status = RET_OK;
// endpoints are enumerated from 1
if (global_zigbee->callbacks_.size() >= endpoint) {
global_zigbee->callbacks_[endpoint - 1](bufid);
const auto &cb = global_zigbee->callbacks_[endpoint - 1];
if (cb) {
cb(bufid);
}
return;
}
p_device_cb_param->status = RET_ERROR;

View File

@@ -81,7 +81,7 @@ class ZigbeeComponent : public Component {
#ifdef USE_ZIGBEE_WIPE_ON_BOOT
void erase_flash_(int area);
#endif
StaticVector<std::function<void(zb_bufid_t bufid)>, ZIGBEE_ENDPOINTS_COUNT> callbacks_;
std::array<std::function<void(zb_bufid_t bufid)>, ZIGBEE_ENDPOINTS_COUNT> callbacks_{};
CallbackManager<void()> join_cb_;
Trigger<> join_trigger_;
bool need_flush_{false};

View File

@@ -55,6 +55,7 @@ from .const_zephyr import (
CONF_ZIGBEE_BINARY_SENSOR,
CONF_ZIGBEE_ID,
CONF_ZIGBEE_SENSOR,
CONF_ZIGBEE_SWITCH,
KEY_EP_NUMBER,
KEY_ZIGBEE,
POWER_SOURCE,
@@ -62,6 +63,7 @@ from .const_zephyr import (
ZB_ZCL_CLUSTER_ID_ANALOG_INPUT,
ZB_ZCL_CLUSTER_ID_BASIC,
ZB_ZCL_CLUSTER_ID_BINARY_INPUT,
ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT,
ZB_ZCL_CLUSTER_ID_IDENTIFY,
ZB_ZCL_IDENTIFY_ATTRS_T,
AnalogAttrs,
@@ -72,6 +74,7 @@ from .const_zephyr import (
ZigbeeBinarySensor = zigbee_ns.class_("ZigbeeBinarySensor", cg.Component)
ZigbeeSensor = zigbee_ns.class_("ZigbeeSensor", cg.Component)
ZigbeeSwitch = zigbee_ns.class_("ZigbeeSwitch", cg.Component)
# BACnet engineering units mapping (ZCL uses BACnet unit codes)
# See: https://github.com/zigpy/zha/blob/dev/zha/application/platforms/number/bacnet.py
@@ -126,6 +129,15 @@ zephyr_sensor = cv.Schema(
}
)
zephyr_switch = cv.Schema(
{
cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(ZigbeeComponent),
cv.OnlyWith(CONF_ZIGBEE_SWITCH, ["nrf52", "zigbee"]): cv.declare_id(
ZigbeeSwitch
),
}
)
async def zephyr_to_code(config: ConfigType) -> None:
zephyr_add_prj_conf("ZIGBEE", True)
@@ -320,6 +332,10 @@ async def zephyr_setup_sensor(entity: cg.MockObj, config: ConfigType) -> None:
CORE.add_job(_add_sensor, entity, config)
async def zephyr_setup_switch(entity: cg.MockObj, config: ConfigType) -> None:
CORE.add_job(_add_switch, entity, config)
def _slot_index() -> int:
"""Find the next available endpoint slot"""
slot = next(
@@ -332,7 +348,7 @@ def _slot_index() -> int:
return slot
async def _add_zigbee_input(
async def _add_zigbee_ep(
entity: cg.MockObj,
config: ConfigType,
component_key,
@@ -389,7 +405,7 @@ async def _add_zigbee_input(
async def _add_binary_sensor(entity: cg.MockObj, config: ConfigType) -> None:
await _add_zigbee_input(
await _add_zigbee_ep(
entity,
config,
CONF_ZIGBEE_BINARY_SENSOR,
@@ -405,7 +421,7 @@ async def _add_sensor(entity: cg.MockObj, config: ConfigType) -> None:
unit = config.get(CONF_UNIT_OF_MEASUREMENT, "")
bacnet_unit = BACNET_UNITS.get(unit, BACNET_UNIT_NO_UNITS)
await _add_zigbee_input(
await _add_zigbee_ep(
entity,
config,
CONF_ZIGBEE_SENSOR,
@@ -415,3 +431,15 @@ async def _add_sensor(entity: cg.MockObj, config: ConfigType) -> None:
"ZB_HA_CUSTOM_ATTR_DEVICE_ID",
extra_field_values={"engineering_units": bacnet_unit},
)
async def _add_switch(entity: cg.MockObj, config: ConfigType) -> None:
await _add_zigbee_ep(
entity,
config,
CONF_ZIGBEE_SWITCH,
BinaryAttrs,
"ESPHOME_ZB_ZCL_DECLARE_BINARY_OUTPUT_ATTRIB_LIST",
ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT,
"ZB_HA_CUSTOM_ATTR_DEVICE_ID",
)

View File

@@ -1,4 +1,4 @@
# Useful stuff when working in a development environment
clang-format==13.0.1 # also change in .pre-commit-config.yaml and Dockerfile when updating
clang-tidy==18.1.8 # When updating clang-tidy, also update Dockerfile
yamllint==1.37.1 # also change in .pre-commit-config.yaml when updating
yamllint==1.38.0 # also change in .pre-commit-config.yaml when updating

View File

@@ -0,0 +1,291 @@
"""Tests for epaper_spi configuration validation."""
from collections.abc import Callable
from typing import Any
import pytest
from esphome import config_validation as cv
from esphome.components.epaper_spi.display import (
CONFIG_SCHEMA,
FINAL_VALIDATE_SCHEMA,
MODELS,
)
from esphome.components.esp32 import (
KEY_BOARD,
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32S3,
)
from esphome.const import (
CONF_BUSY_PIN,
CONF_CS_PIN,
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_HEIGHT,
CONF_INIT_SEQUENCE,
CONF_RESET_PIN,
CONF_WIDTH,
PlatformFramework,
)
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
def run_schema_validation(
config: ConfigType, with_final_validate: bool = False
) -> None:
"""Run schema validation on a configuration.
Args:
config: The configuration to validate
with_final_validate: If True, also run final validation (requires full config setup)
"""
result = CONFIG_SCHEMA(config)
if with_final_validate:
FINAL_VALIDATE_SCHEMA(result)
return result
@pytest.mark.parametrize(
("config", "error_match"),
[
pytest.param(
"a string",
"expected a dictionary",
id="invalid_string_config",
),
pytest.param(
{"id": "display_id"},
r"required key not provided @ data\['model'\]",
id="missing_model",
),
pytest.param(
{
"id": "display_id",
"model": "ssd1677",
"dimensions": {"width": 200, "height": 200},
},
r"required key not provided @ data\['dc_pin'\]",
id="missing_dc_pin",
),
],
)
def test_basic_configuration_errors(
config: str | ConfigType,
error_match: str,
set_core_config: SetCoreConfigCallable,
) -> None:
"""Test basic configuration validation errors"""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
with pytest.raises(cv.Invalid, match=error_match):
CONFIG_SCHEMA(config)
def test_all_predefined_models(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test all predefined epaper models validate successfully with appropriate defaults."""
# Test all models, providing default values where necessary
for name, model in MODELS.items():
# SEEED models are designed for ESP32-S3 hardware
if name in ("SEEED-EE04-MONO-4.26", "SEEED-RETERMINAL-E1002"):
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={
KEY_BOARD: "esp32-s3-devkitc-1",
KEY_VARIANT: VARIANT_ESP32S3,
},
)
else:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
config = {"model": name}
# Add ID field
config["id"] = "test_display"
# Add required fields that don't have defaults
# Use safe GPIO pins that work on ESP32 (avoiding flash pins 6-11)
if not model.get_default(CONF_DC_PIN):
config[CONF_DC_PIN] = 21
# Add dimensions if not provided by model
if not model.get_default(CONF_WIDTH):
config[CONF_DIMENSIONS] = {CONF_HEIGHT: 240, CONF_WIDTH: 320}
# Add init sequence if model doesn't provide one
if model.initsequence is None:
config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]]
# Add other optional pins that some models might require
if not model.get_default(CONF_BUSY_PIN):
config[CONF_BUSY_PIN] = 22
if not model.get_default(CONF_RESET_PIN):
config[CONF_RESET_PIN] = 23
if not model.get_default(CONF_CS_PIN):
config[CONF_CS_PIN] = 5
run_schema_validation(config)
@pytest.mark.parametrize(
"model_name",
[pytest.param(name, id=name.lower()) for name in sorted(MODELS.keys())],
)
def test_individual_models(
model_name: str,
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test each epaper model individually to ensure it validates correctly."""
# SEEED models are designed for ESP32-S3 hardware
if model_name in ("SEEED-EE04-MONO-4.26", "SEEED-RETERMINAL-E1002"):
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={
KEY_BOARD: "esp32-s3-devkitc-1",
KEY_VARIANT: VARIANT_ESP32S3,
},
)
else:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
model = MODELS[model_name]
config: dict[str, Any] = {"model": model_name, "id": "test_display"}
# Add required fields based on model defaults
# Use safe GPIO pins that work on ESP32
if not model.get_default(CONF_DC_PIN):
config[CONF_DC_PIN] = 21
if not model.get_default(CONF_WIDTH):
config[CONF_DIMENSIONS] = {CONF_HEIGHT: 240, CONF_WIDTH: 320}
if model.initsequence is None:
config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]]
if not model.get_default(CONF_BUSY_PIN):
config[CONF_BUSY_PIN] = 22
if not model.get_default(CONF_RESET_PIN):
config[CONF_RESET_PIN] = 23
if not model.get_default(CONF_CS_PIN):
config[CONF_CS_PIN] = 5
# This should not raise any exceptions
run_schema_validation(config)
def test_model_with_explicit_dimensions(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test model configuration with explicitly provided dimensions."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
run_schema_validation(
{
"id": "test_display",
"model": "ssd1677",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
"dimensions": {
"width": 200,
"height": 200,
},
}
)
def test_model_with_transform(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test model configuration with transform options."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
run_schema_validation(
{
"id": "test_display",
"model": "ssd1677",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
"dimensions": {
"width": 200,
"height": 200,
},
"transform": {
"mirror_x": True,
"mirror_y": False,
},
}
)
def test_model_with_full_update_every(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test model configuration with full_update_every option."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
# Configure SPI component which is required by epaper_spi
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
run_schema_validation(
{
"id": "test_display",
"model": "ssd1677",
"dc_pin": 21,
"busy_pin": 22,
"reset_pin": 23,
"cs_pin": 5,
"dimensions": {
"width": 200,
"height": 200,
},
"full_update_every": 10,
}
)

View File

@@ -8,15 +8,39 @@ display:
dimensions:
width: 800
height: 480
cs_pin: GPIO5
dc_pin: GPIO17
reset_pin: GPIO16
busy_pin: GPIO4
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
rotation: 0
update_interval: 60s
lambda: |-
it.circle(64, 64, 50, Color::BLACK);
- platform: epaper_spi
spi_id: spi_bus
model: waveshare-2.13in-v3
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
model: seeed-reterminal-e1002
- platform: epaper_spi

View File

@@ -11,9 +11,7 @@ binary_sensor:
- platform: template
name: "Garage Door Open 5"
- platform: template
name: "Garage Door Open 6"
- platform: template
name: "Garage Door Open 7"
name: "Garage Door Internal"
internal: True
sensor:
@@ -36,3 +34,8 @@ output:
type: binary
write_action:
- zigbee.factory_reset
switch:
- platform: template
name: "Template Switch"
optimistic: true