Merge branch 'cron-trigger-interval' into integration

This commit is contained in:
J. Nick Koston
2026-04-03 18:22:14 -10:00
65 changed files with 2394 additions and 482 deletions
+5 -4
View File
@@ -235,19 +235,20 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
}
}
// Get PR head to fetch files from the PR branch
const prNumber = context.payload.pull_request.number;
// Get base branch ref to check if deprecation already exists for the component
// This prevents flagging a PR that simply adds deprecation
const baseRef = context.payload.pull_request.base.ref;
// Check each component's __init__.py for DEPRECATED_COMPONENT constant
for (const component of components) {
const initFile = `esphome/components/${component}/__init__.py`;
try {
// Fetch file content from PR head using GitHub API
// Fetch file content from base branch using GitHub API
const { data: fileData } = await github.rest.repos.getContent({
owner,
repo,
path: initFile,
ref: `refs/pull/${prNumber}/head`
ref: baseRef
});
// Decode base64 content
+1 -1
View File
@@ -723,7 +723,7 @@ jobs:
cache-key: ${{ needs.common.outputs.cache-key }}
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
env:
SKIP: pylint,clang-tidy-hash
SKIP: pylint,clang-tidy-hash,ci-custom
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
if: always()
+4
View File
@@ -65,3 +65,7 @@ repos:
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
pass_filenames: false
additional_dependencies: []
- id: ci-custom
name: ci-custom
entry: python3 script/run-in-env.py script/ci-custom.py
language: system
+2
View File
@@ -32,4 +32,6 @@ ICON_CURRENT_DC = "mdi:current-dc"
ICON_SOLAR_PANEL = "mdi:solar-panel"
ICON_SOLAR_POWER = "mdi:solar-power"
KEY_METADATA = "metadata"
UNIT_AMPERE_HOUR = "Ah"
+38
View File
@@ -1,6 +1,9 @@
from dataclasses import dataclass
from esphome import automation, core
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components.const import KEY_METADATA
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
@@ -16,7 +19,9 @@ from esphome.const import (
SCHEDULER_DONT_RUN,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj
DOMAIN = "display"
IS_PLATFORM_COMPONENT = True
display_ns = cg.esphome_ns.namespace("display")
@@ -146,6 +151,39 @@ async def setup_display_core_(var, config):
cg.add(var.show_test_card())
# Storage of display metadata in a central location, accessible via the id
@dataclass(frozen=True)
class DisplayMetaData:
width: int = 0
height: int = 0
has_writer: bool = False
has_hardware_rotation: bool = False
def get_all_display_metadata() -> dict[str, DisplayMetaData]:
"""Get all display metadata."""
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
def get_display_metadata(display_id: str) -> DisplayMetaData | None:
"""Get display metadata by ID for use by other components."""
return get_all_display_metadata().get(display_id, DisplayMetaData())
def add_metadata(
id: str | MockObj,
width: int,
height: int,
has_writer: bool,
has_hardware_rotation: bool = False,
):
get_all_display_metadata()[str(id)] = DisplayMetaData(
width, height, has_writer, has_hardware_rotation
)
async def register_display(var, config):
await cg.register_component(var, config)
await setup_display_core_(var, config)
+1 -1
View File
@@ -704,7 +704,7 @@ class Display : public PollingComponent {
void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
/// Internal method to set the display rotation with.
void set_rotation(DisplayRotation rotation);
virtual void set_rotation(DisplayRotation rotation);
// Internal method to set display auto clearing.
void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
+1 -1
View File
@@ -286,7 +286,7 @@ async def ezo_pmp_change_i2c_address_to_code(config, action_id, template_arg, ar
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.double)
template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.int_)
cg.add(var.set_address(template_))
return var
+13 -1
View File
@@ -5,11 +5,17 @@
#include "esphome/core/helpers.h"
#include "preferences.h"
#include <csignal>
#include <sched.h>
#include <time.h>
#include <cmath>
#include <cstdlib>
namespace {
volatile sig_atomic_t s_signal_received = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void signal_handler(int signal) { s_signal_received = signal; }
} // namespace
namespace esphome {
void HOT yield() { ::sched_yield(); }
@@ -72,11 +78,17 @@ uint32_t arch_get_cpu_freq_hz() { return 1000000000U; }
void setup();
void loop();
int main() {
// Install signal handlers for graceful shutdown (flushes preferences to disk)
std::signal(SIGINT, signal_handler);
std::signal(SIGTERM, signal_handler);
esphome::host::setup_preferences();
setup();
while (true) {
while (s_signal_received == 0) {
loop();
}
esphome::App.run_safe_shutdown_hooks();
return 0;
}
#endif // USE_HOST
+4
View File
@@ -0,0 +1,4 @@
DEPRECATED_COMPONENT = """
The 'ili9xxx' component is deprecated and no new models will be added to it.
New model PRs should target the newer and more performant 'mipi_spi' component.
"""
+5 -2
View File
@@ -210,8 +210,8 @@ def final_validate(config):
):
LOGGER.info("Consider enabling PSRAM if available for the display buffer")
return spi.final_validate_device_schema(
"ili9xxx", require_miso=False, require_mosi=True
spi.final_validate_device_schema("ili9xxx", require_miso=False, require_mosi=True)(
config
)
@@ -219,6 +219,9 @@ FINAL_VALIDATE_SCHEMA = final_validate
async def to_code(config):
LOGGER.warning(
"The 'ili9xxx' component is deprecated, it is recommended to use 'mipi_spi' instead."
)
rhs = MODELS[config[CONF_MODEL]].new()
var = cg.Pvariable(config[CONF_ID], rhs)
+1 -2
View File
@@ -12,7 +12,7 @@ from PIL import Image, UnidentifiedImageError
from esphome import core, external_files
import esphome.codegen as cg
from esphome.components.const import CONF_BYTE_ORDER
from esphome.components.const import CONF_BYTE_ORDER, KEY_METADATA
import esphome.config_validation as cv
from esphome.const import (
CONF_DEFAULTS,
@@ -53,7 +53,6 @@ CONF_CHROMA_KEY = "chroma_key"
CONF_ALPHA_CHANNEL = "alpha_channel"
CONF_INVERT_ALPHA = "invert_alpha"
CONF_IMAGES = "images"
KEY_METADATA = "metadata"
TRANSPARENCY_TYPES = (
CONF_OPAQUE,
+3 -1
View File
@@ -23,6 +23,7 @@ from esphome.core.config import StartupTrigger
from . import defines as df, lv_validation as lvalid
from .defines import (
CONF_EXT_CLICK_AREA,
CONF_SCROLL_DIR,
CONF_SCROLL_SNAP_X,
CONF_SCROLL_SNAP_Y,
@@ -311,6 +312,7 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex
cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant(
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
).one_of,
cv.Optional(CONF_EXT_CLICK_AREA): lvalid.pixels,
cv.Optional(CONF_SCROLL_DIR): df.SCROLL_DIRECTIONS.one_of,
cv.Optional(CONF_SCROLL_SNAP_X): df.SNAP_DIRECTIONS.one_of,
cv.Optional(CONF_SCROLL_SNAP_Y): df.SNAP_DIRECTIONS.one_of,
@@ -318,6 +320,7 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex
)
OBJ_PROPERTIES = {
CONF_EXT_CLICK_AREA,
CONF_SCROLL_SNAP_X,
CONF_SCROLL_SNAP_Y,
CONF_SCROLL_DIR,
@@ -433,7 +436,6 @@ def obj_schema(widget_type: WidgetType):
return (
part_schema(widget_type.parts)
.extend(ALIGN_TO_SCHEMA)
.extend({cv.Optional(df.CONF_EXT_CLICK_AREA): lvalid.pixels})
.extend(automation_schema(widget_type.w_type))
.extend(
{
-3
View File
@@ -15,7 +15,6 @@ from .defines import (
CONF_ALIGN,
CONF_ALIGN_TO,
CONF_ALIGN_TO_LAMBDA_ID,
CONF_EXT_CLICK_AREA,
DIRECTIONS,
LV_EVENT_MAP,
LV_EVENT_TRIGGERS,
@@ -114,8 +113,6 @@ async def generate_align_tos(config: dict):
x = align_to[CONF_X]
y = align_to[CONF_Y]
lv.obj_align_to(w.obj, target, align, x, y)
if ext_click_area := w.config.get(CONF_EXT_CLICK_AREA):
lv.obj_set_ext_click_area(w.obj, ext_click_area)
action_id = config[CONF_ALIGN_TO_LAMBDA_ID]
var = new_Pvariable(action_id, await context.get_lambda())
+47 -23
View File
@@ -128,6 +128,8 @@ MADCTL_MH = 0x04 # Bit 2 LCD refresh right to left
MADCTL_XFLIP = 0x02 # Mirror the display horizontally
MADCTL_YFLIP = 0x01 # Mirror the display vertically
MADCTL_FLIP_FLAG = 0x100 # meta-flag to indicate use of axis flips
# Special constant for delays in command sequences
DELAY_FLAG = 0xFFF # Special flag to indicate a delay
@@ -329,7 +331,13 @@ class DriverChip:
return CONF_SWAP_XY in transforms and CONF_MIRROR_X in transforms
return CONF_SWAP_XY in transforms and CONF_MIRROR_Y in transforms
def get_dimensions(self, config) -> tuple[int, int, int, int]:
def get_dimensions(self, config, swap: bool = True) -> tuple[int, int, int, int]:
"""
Return the dimensions of the current model.
:param config: The current configuration
:param swap: If width/height should be swapped when axes are swapped.
:return:
"""
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
dimensions = config[CONF_DIMENSIONS]
@@ -361,13 +369,12 @@ class DriverChip:
)
offset_height = native_height - height - offset_height
# Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer
if transform.get(CONF_SWAP_XY) is True:
if swap and transform.get(CONF_SWAP_XY) is True:
width, height = height, width
offset_height, offset_width = offset_width, offset_height
return width, height, offset_width, offset_height
def get_transform(self, config) -> dict[str, bool]:
can_transform = self.rotation_as_transform(config)
def get_base_transform(self, config):
transform = config.get(
CONF_TRANSFORM,
{
@@ -376,14 +383,20 @@ class DriverChip:
CONF_SWAP_XY: self.get_default(CONF_SWAP_XY),
},
)
if not isinstance(transform, dict):
# Presumably disabled
return {
CONF_MIRROR_X: False,
CONF_MIRROR_Y: False,
CONF_SWAP_XY: False,
CONF_TRANSFORM: False,
}
if isinstance(transform, dict):
return transform
# Transform is disabled
return {
CONF_MIRROR_X: False,
CONF_MIRROR_Y: False,
CONF_SWAP_XY: False,
CONF_TRANSFORM: False,
}
def get_transform(self, config) -> dict[str, bool]:
transform = self.get_base_transform(config)
can_transform = self.rotation_as_transform(config)
# Can we use the MADCTL register to set the rotation?
if can_transform and CONF_TRANSFORM not in config:
rotation = config[CONF_ROTATION]
@@ -411,11 +424,15 @@ class DriverChip:
return {cv.Required(CONF_SWAP_XY): cv.boolean}
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
def add_madctl(self, sequence: list, config: dict):
# Add the MADCTL command to the sequence based on the configuration.
use_flip = config.get(CONF_USE_AXIS_FLIPS)
madctl = 0
transform = self.get_transform(config)
def get_madctl(self, transform: dict, config: dict) -> int:
"""
Convert a transform to MADCTL bits
:param transform: The transform dict
:param use_flip: Whether to use axis flips
:return: MADCTL value
"""
use_flip = config.get(CONF_USE_AXIS_FLIPS, False)
madctl = MADCTL_FLIP_FLAG if use_flip else 0
if transform[CONF_MIRROR_X]:
madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX
if transform[CONF_MIRROR_Y]:
@@ -424,22 +441,28 @@ class DriverChip:
madctl |= MADCTL_MV
if config[CONF_COLOR_ORDER] == MODE_BGR:
madctl |= MADCTL_BGR
sequence.append((MADCTL, madctl))
return madctl
def add_madctl(self, sequence: list, config: dict):
# Add the MADCTL command to the sequence based on the configuration.
# This takes into account rotation if it can be implemented in the transform
transform = self.get_transform(config)
madctl = self.get_madctl(transform, config)
sequence.append((MADCTL, madctl & 0xFF))
def skip_command(self, command: str):
"""
Allow suppressing a standard command in the init sequence.
"""
return self.get_default(f"no_{command.lower()}", False)
def get_sequence(self, config) -> tuple[tuple[int, ...], int]:
def get_sequence(self, config, add_madctl=True) -> tuple[int, ...]:
"""
Create the init sequence for the display.
Use the default sequence from the model, if any, and append any custom sequence provided in the config.
Append SLPOUT (if not already in the sequence) and DISPON to the end of the sequence
Pixel format, color order, and orientation will be set.
Returns a tuple of the init sequence and the computed MADCTL value.
MADCTL will be set if add_madctl is True
Returns the init sequence
"""
sequence = list(self.initsequence or ())
custom_sequence = config.get(CONF_INIT_SEQUENCE, [])
@@ -457,7 +480,8 @@ class DriverChip:
if self.rotation_as_transform(config):
LOGGER.info("Using hardware transform to implement rotation")
madctl = self.add_madctl(sequence, config)
if add_madctl:
self.add_madctl(sequence, config)
if config[CONF_INVERT_COLORS]:
sequence.append((INVON,))
else:
@@ -471,7 +495,7 @@ class DriverChip:
# Flatten the sequence into a list of bytes, with the length of each command
# or the delay flag inserted where needed
return flatten_sequence(sequence), madctl
return flatten_sequence(sequence)
def requires_buffer(config) -> bool:
+1 -2
View File
@@ -192,10 +192,9 @@ async def to_code(config):
width, height, _offset_width, _offset_height = model.get_dimensions(config)
var = cg.new_Pvariable(config[CONF_ID], width, height, color_depth, pixel_mode)
sequence, madctl = model.get_sequence(config)
sequence = model.get_sequence(config)
cg.add(var.set_model(config[CONF_MODEL]))
cg.add(var.set_init_sequence(sequence))
cg.add(var.set_madctl(madctl))
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH]))
cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH]))
+3 -9
View File
@@ -392,9 +392,6 @@ void MIPI_DSI::dump_config() {
"\n Model: %s"
"\n Width: %u"
"\n Height: %u"
"\n Mirror X: %s"
"\n Mirror Y: %s"
"\n Swap X/Y: %s"
"\n Rotation: %d degrees"
"\n DSI Lanes: %u"
"\n Lane Bit Rate: %.0fMbps"
@@ -406,14 +403,11 @@ void MIPI_DSI::dump_config() {
"\n VSync Front Porch: %u"
"\n Buffer Color Depth: %d bit"
"\n Display Pixel Mode: %d bit"
"\n Color Order: %s"
"\n Invert Colors: %s"
"\n Pixel Clock: %.1fMHz",
this->model_, this->width_, this->height_, YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)),
YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY)), YESNO(this->madctl_ & MADCTL_MV), this->rotation_,
this->lanes_, this->lane_bit_rate_, this->hsync_pulse_width_, this->hsync_back_porch_,
this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, this->vsync_front_porch_,
(3 - this->color_depth_) * 8, this->pixel_mode_, this->madctl_ & MADCTL_BGR ? "BGR" : "RGB",
this->model_, this->width_, this->height_, this->rotation_, this->lanes_, this->lane_bit_rate_,
this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_,
this->vsync_back_porch_, this->vsync_front_porch_, (3 - this->color_depth_) * 8, this->pixel_mode_,
YESNO(this->invert_colors_), this->pclk_frequency_);
LOG_PIN(" Reset Pin ", this->reset_pin_);
}
-2
View File
@@ -60,7 +60,6 @@ class MIPI_DSI : public display::Display {
void set_model(const char *model) { this->model_ = model; }
void set_lane_bit_rate(float lane_bit_rate) { this->lane_bit_rate_ = lane_bit_rate; }
void set_lanes(uint8_t lanes) { this->lanes_ = lanes; }
void set_madctl(uint8_t madctl) { this->madctl_ = madctl; }
void smark_failed(const LogString *message, esp_err_t err);
@@ -86,7 +85,6 @@ class MIPI_DSI : public display::Display {
std::vector<GPIOPin *> enable_pins_{};
size_t width_{};
size_t height_{};
uint8_t madctl_{};
uint16_t hsync_pulse_width_ = 10;
uint16_t hsync_back_porch_ = 10;
uint16_t hsync_front_porch_ = 20;
+1 -2
View File
@@ -265,9 +265,8 @@ async def to_code(config):
if CONF_SPI_ID in config:
await spi.register_spi_device(var, config, write_only=True)
sequence, madctl = model.get_sequence(config)
sequence = model.get_sequence(config)
cg.add(var.set_init_sequence(sequence))
cg.add(var.set_madctl(madctl))
cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]]))
cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS]))
+1 -9
View File
@@ -118,15 +118,7 @@ void MipiRgbSpi::dump_config() {
MipiRgb::dump_config();
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
ESP_LOGCONFIG(TAG,
" SPI Data rate: %uMHz"
"\n Mirror X: %s"
"\n Mirror Y: %s"
"\n Swap X/Y: %s"
"\n Color Order: %s",
(unsigned) (this->data_rate_ / 1000000), YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)),
YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY | MADCTL_ML)), YESNO(this->madctl_ & MADCTL_MV),
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB");
ESP_LOGCONFIG(TAG, " SPI Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000));
}
#endif // USE_SPI
-2
View File
@@ -38,7 +38,6 @@ class MipiRgb : public display::Display {
display::ColorOrder get_color_mode() { return this->color_mode_; }
void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; }
void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; }
void set_madctl(uint8_t madctl) { this->madctl_ = madctl; }
void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; };
void set_de_pin(InternalGPIOPin *de_pin) { this->de_pin_ = de_pin; }
@@ -84,7 +83,6 @@ class MipiRgb : public display::Display {
uint16_t vsync_front_porch_ = 10;
uint32_t pclk_frequency_ = 16 * 1000 * 1000;
bool pclk_inverted_{true};
uint8_t madctl_{};
const char *model_{"Unknown"};
bool invert_colors_{};
display::ColorOrder color_mode_{display::COLOR_ORDER_BGR};
+35 -87
View File
@@ -10,7 +10,7 @@ from esphome.components.const import (
CONF_COLOR_DEPTH,
CONF_DRAW_ROUNDING,
)
from esphome.components.display import CONF_SHOW_TEST_CARD, DISPLAY_ROTATIONS
from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.components.mipi import (
CONF_PIXEL_MODE,
CONF_USE_AXIS_FLIPS,
@@ -47,12 +47,10 @@ from esphome.const import (
CONF_MIRROR_Y,
CONF_MODEL,
CONF_RESET_PIN,
CONF_ROTATION,
CONF_SWAP_XY,
CONF_TRANSFORM,
CONF_WIDTH,
)
from esphome.core import CORE
from esphome.cpp_generator import TemplateArguments
from esphome.final_validate import full_config
@@ -113,22 +111,21 @@ DISPLAY_PIXEL_MODES = {
def denominator(config):
"""
Calculate the best denominator for a buffer size fraction.
The denominator must be a number between 2 and 16 that divides the display height evenly,
The denominator should be a number between 2 and 16 that divides the display height evenly,
and the fraction represented by the denominator must be less than or equal to the given fraction.
:config: The configuration dictionary containing the buffer size fraction and display dimensions
:return: The denominator to use for the buffer size fraction
"""
model = MODELS[config[CONF_MODEL]]
frac = config.get(CONF_BUFFER_SIZE)
if frac is None or frac > 0.75:
_width, height, _offset_width, _offset_height = model.get_dimensions(config)
if frac is None or frac > 0.75 or height < 32:
return 1
height, _width, _offset_width, _offset_height = model.get_dimensions(config)
try:
return next(x for x in range(2, 17) if frac >= 1 / x and height % x == 0)
except StopIteration:
raise cv.Invalid(
f"Buffer size fraction {frac} is not compatible with display height {height}"
) from StopIteration
# No exact divisor, just use the closest.
return next(x for x in range(2, 17) if frac >= 1 / x)
def model_schema(config):
@@ -287,30 +284,19 @@ def _final_validate(config):
config[CONF_SHOW_TEST_CARD] = True
if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config:
if not requires_buffer(config):
return config # No buffer needed, so no need to set a buffer size
# If PSRAM is not enabled, choose a small buffer size by default
if not requires_buffer(config):
# not our problem.
return config
return config # No buffer needed, so no need to set a buffer size
color_depth = get_color_depth(config)
frac = denominator(config)
height, width, _offset_width, _offset_height = model.get_dimensions(config)
width, height, _offset_width, _offset_height = model.get_dimensions(config)
buffer_size = color_depth // 8 * width * height // frac
# Target a buffer size of 20kB
fraction = 20000.0 / buffer_size
try:
config[CONF_BUFFER_SIZE] = 1.0 / next(
x for x in range(2, 17) if fraction >= 1 / x and height % x == 0
)
except StopIteration:
# Either the screen is too big, or the height is not divisible by any of the fractions, so use 1.0
# PSRAM will be needed.
if CORE.is_esp32:
raise cv.Invalid(
"PSRAM is required for this display"
) from StopIteration
# Target a buffer size of 20kB, except for large displays, which shouldn't end up here
fraction = min(20000.0, buffer_size // 16) / buffer_size
config[CONF_BUFFER_SIZE] = 1.0 / next(
x for x in range(2, 17) if fraction >= 1 / x
)
return config
@@ -318,39 +304,6 @@ def _final_validate(config):
FINAL_VALIDATE_SCHEMA = _final_validate
def get_transform(config):
"""
Get the transformation configuration for the display.
:param config:
:return:
"""
model = MODELS[config[CONF_MODEL]]
can_transform = model.rotation_as_transform(config)
transform = config.get(
CONF_TRANSFORM,
{
CONF_MIRROR_X: model.get_default(CONF_MIRROR_X, False),
CONF_MIRROR_Y: model.get_default(CONF_MIRROR_Y, False),
CONF_SWAP_XY: model.get_default(CONF_SWAP_XY, False),
},
)
# Can we use the MADCTL register to set the rotation?
if can_transform and CONF_TRANSFORM not in config:
rotation = config[CONF_ROTATION]
if rotation == 180:
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
elif rotation == 90:
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
else:
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
transform[CONF_TRANSFORM] = True
return transform
def get_instance(config):
"""
Get the type of MipiSpi instance to create based on the configuration,
@@ -359,7 +312,16 @@ def get_instance(config):
:return: type, template arguments
"""
model = MODELS[config[CONF_MODEL]]
width, height, offset_width, offset_height = model.get_dimensions(config)
has_hardware_transform = config.get(
CONF_TRANSFORM
) != CONF_DISABLED and model.transforms == {
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_SWAP_XY,
}
width, height, offset_width, offset_height = model.get_dimensions(
config, not has_hardware_transform
)
color_depth = int(config[CONF_COLOR_DEPTH].removesuffix("bit"))
bufferpixels = COLOR_DEPTHS[color_depth]
@@ -373,57 +335,43 @@ def get_instance(config):
bus_type = BusTypes[bus_type]
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
frac = denominator(config)
rotation = (
0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0)
)
madctl = model.get_madctl(model.get_base_transform(config), config)
has_writer = requires_buffer(config)
templateargs = [
buffer_type,
bufferpixels,
config[CONF_BYTE_ORDER] == "big_endian",
display_pixel_mode,
bus_type,
width,
height,
offset_width,
offset_height,
madctl,
has_hardware_transform,
]
display.add_metadata(
config[CONF_ID], width, height, has_writer, has_hardware_transform
)
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
if requires_buffer(config):
templateargs.extend(
[
width,
height,
offset_width,
offset_height,
DISPLAY_ROTATIONS[rotation],
frac,
config[CONF_DRAW_ROUNDING],
]
)
return MipiSpiBuffer, templateargs
# Swap height and width if the display is rotated 90 or 270 degrees in software
if rotation in (90, 270):
width, height = height, width
offset_width, offset_height = offset_height, offset_width
templateargs.extend(
[
width,
height,
offset_width,
offset_height,
]
)
return MipiSpi, templateargs
async def to_code(config):
model = MODELS[config[CONF_MODEL]]
var_id = config[CONF_ID]
init_sequence = model.get_sequence(config, False)
var_id.type, templateargs = get_instance(config)
var = cg.new_Pvariable(var_id, TemplateArguments(*templateargs))
init_sequence, _madctl = model.get_sequence(config)
cg.add(var.set_init_sequence(init_sequence))
if model.rotation_as_transform(config):
if CONF_TRANSFORM in config:
LOGGER.warning("Use of 'transform' with 'rotation' is not recommended")
else:
config[CONF_ROTATION] = 0
cg.add(var.set_model(config[CONF_MODEL]))
if enable_pin := config.get(CONF_ENABLE_PIN):
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
+6 -4
View File
@@ -5,7 +5,8 @@ namespace esphome::mipi_spi {
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width) {
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width,
bool has_hardware_rotation) {
ESP_LOGCONFIG(TAG,
"MIPI_SPI Display\n"
" Model: %s\n"
@@ -14,6 +15,7 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
" Swap X/Y: %s\n"
" Mirror X: %s\n"
" Mirror Y: %s\n"
" Hardware rotation: %s\n"
" Invert colors: %s\n"
" Color order: %s\n"
" Display pixels: %d bits\n"
@@ -22,9 +24,9 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
" SPI Data rate: %uMHz\n"
" SPI Bus width: %d",
model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)),
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(invert_colors), (madctl & MADCTL_BGR) ? "BGR" : "RGB",
display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast<unsigned>(data_rate / 1000000),
bus_width);
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(has_hardware_rotation), YESNO(invert_colors),
(madctl & MADCTL_BGR) ? "BGR" : "RGB", display_bits, is_big_endian ? "Big" : "Little", spi_mode,
static_cast<unsigned>(data_rate / 1000000), bus_width);
LOG_PIN(" CS Pin: ", cs);
LOG_PIN(" Reset Pin: ", reset);
LOG_PIN(" DC Pin: ", dc);
+138 -74
View File
@@ -34,13 +34,14 @@ static constexpr uint8_t SWIRE1 = 0x5A;
static constexpr uint8_t SWIRE2 = 0x5B;
static constexpr uint8_t PAGESEL = 0xFE;
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
static constexpr uint8_t MADCTL_RGB = 0x00; // Bit 3 Red-Green-Blue pixel order
static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
static constexpr uint16_t MADCTL_FLIP_FLAG = 0x100; // controller uses axis flip bits
static constexpr uint8_t DELAY_FLAG = 0xFF;
// store a 16 bit value in a buffer, big endian.
@@ -66,7 +67,8 @@ enum BusType {
// Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width);
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width,
bool has_hardware_rotation);
/**
* Base class for MIPI SPI displays.
@@ -83,7 +85,7 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
* buffer
*/
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT>
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL, bool HAS_HARDWARE_ROTATION>
class MipiSpi : public display::Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_1MHZ> {
@@ -103,10 +105,39 @@ class MipiSpi : public display::Display,
this->brightness_ = brightness;
this->reset_params_();
}
void set_rotation(display::DisplayRotation rotation) override {
this->rotation_ = rotation;
if constexpr (HAS_HARDWARE_ROTATION) {
this->reset_params_();
}
}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
int get_width_internal() override { return WIDTH; }
int get_height_internal() override { return HEIGHT; }
int get_width() override {
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
return HEIGHT;
return WIDTH;
}
int get_height() override {
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
return WIDTH;
return HEIGHT;
}
// If hardware rotation is in use, the actual display width/height changes with rotation
int get_width_internal() override {
if constexpr (HAS_HARDWARE_ROTATION)
return get_width();
return WIDTH;
}
int get_height_internal() override {
if constexpr (HAS_HARDWARE_ROTATION)
return get_height();
return HEIGHT;
}
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
// reset the display, and write the init sequence
@@ -166,9 +197,6 @@ class MipiSpi : public display::Display,
case INVERT_ON:
this->invert_colors_ = true;
break;
case MADCTL_CMD:
this->madctl_ = arg_byte;
break;
case BRIGHTNESS:
this->brightness_ = arg_byte;
break;
@@ -177,13 +205,13 @@ class MipiSpi : public display::Display,
break;
}
const auto *ptr = vec.data() + index;
esph_log_d(TAG, "Command %02X, length %d, byte %02X", cmd, num_args, arg_byte);
this->write_command_(cmd, ptr, num_args);
index += num_args;
if (cmd == SLEEP_OUT)
delay(10);
}
}
this->reset_params_();
// init sequence no longer needed
this->init_sequence_.clear();
}
@@ -206,9 +234,10 @@ class MipiSpi : public display::Display,
}
void dump_config() override {
internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_,
DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_,
this->mode_, this->data_rate_, BUS_TYPE);
internal_dump_config(this->model_, this->get_width(), this->get_height(), OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL,
this->invert_colors_, DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_,
this->reset_pin_, this->dc_pin_, this->mode_, this->data_rate_, BUS_TYPE,
HAS_HARDWARE_ROTATION);
}
protected:
@@ -219,10 +248,13 @@ class MipiSpi : public display::Display,
// Writes a command to the display, with the given bytes.
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)];
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
#endif
// Don't spam the log after setup
if (this->init_sequence_.empty()) {
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
} else {
esph_log_d(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
}
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
this->enable();
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
@@ -271,16 +303,60 @@ class MipiSpi : public display::Display,
this->write_command_(this->invert_colors_ ? INVERT_ON : INVERT_OFF);
if (this->brightness_.has_value())
this->write_command_(BRIGHTNESS, this->brightness_.value());
// calculate new madctl value from base value adjusted for rotation
uint8_t madctl = MADCTL; // lower 8 bits only
constexpr bool use_flips = (MADCTL & MADCTL_FLIP_FLAG) != 0;
constexpr uint8_t x_mask = use_flips ? MADCTL_XFLIP : MADCTL_MX;
constexpr uint8_t y_mask = use_flips ? MADCTL_YFLIP : MADCTL_MY;
if constexpr (HAS_HARDWARE_ROTATION) {
switch (this->rotation_) {
default:
break;
case display::DISPLAY_ROTATION_90_DEGREES:
madctl ^= x_mask; // flip X axis
madctl ^= MADCTL_MV; // swap X and Y axes
break;
case display::DISPLAY_ROTATION_180_DEGREES:
madctl ^= x_mask; // flip X axis
madctl ^= y_mask; // flip Y axis
break;
case display::DISPLAY_ROTATION_270_DEGREES:
madctl ^= y_mask; // flip Y axis
madctl ^= MADCTL_MV; // swap X and Y axes
break;
}
}
esph_log_d(TAG, "Setting MADCTL for rotation %d, value %X", this->rotation_, madctl);
this->write_command_(MADCTL_CMD, madctl);
}
uint16_t get_offset_width_() {
if constexpr (HAS_HARDWARE_ROTATION) {
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
return OFFSET_HEIGHT;
}
return OFFSET_WIDTH;
}
uint16_t get_offset_height_() {
if constexpr (HAS_HARDWARE_ROTATION) {
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
return OFFSET_WIDTH;
}
return OFFSET_HEIGHT;
}
// set the address window for the next data write
void set_addr_window_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
esph_log_v(TAG, "Set addr %d/%d, %d/%d", x1, y1, x2, y2);
uint8_t buf[4];
x1 += OFFSET_WIDTH;
x2 += OFFSET_WIDTH;
y1 += OFFSET_HEIGHT;
y2 += OFFSET_HEIGHT;
x1 += get_offset_width_();
x2 += get_offset_width_();
y1 += get_offset_height_();
y2 += get_offset_height_();
put16_be(buf, y1);
put16_be(buf + 2, y2);
this->write_command_(RASET, buf, sizeof buf);
@@ -408,7 +484,6 @@ class MipiSpi : public display::Display,
optional<uint8_t> brightness_{};
const char *model_{"Unknown"};
std::vector<uint8_t> init_sequence_{};
uint8_t madctl_{};
};
/**
@@ -427,22 +502,21 @@ class MipiSpi : public display::Display,
* @tparam ROUNDING The alignment requirement for drawing operations (e.g. 2 means that x coordinates must be even)
*/
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION,
int FRACTION, unsigned ROUNDING>
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, uint16_t MADCTL,
bool HAS_HARDWARE_ROTATION, int FRACTION, unsigned ROUNDING>
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
OFFSET_WIDTH, OFFSET_HEIGHT> {
OFFSET_WIDTH, OFFSET_HEIGHT, MADCTL, HAS_HARDWARE_ROTATION> {
public:
// these values define the buffer size needed to write in accordance with the chip pixel alignment
// requirements. If the required rounding does not divide the width and height, we round up to the next multiple and
// ignore the extra columns and rows when drawing, but use them to write to the display.
static constexpr unsigned BUFFER_WIDTH = (WIDTH + ROUNDING - 1) / ROUNDING * ROUNDING;
static constexpr unsigned BUFFER_HEIGHT = (HEIGHT + ROUNDING - 1) / ROUNDING * ROUNDING;
static constexpr size_t round_buffer(size_t size) { return (size + ROUNDING - 1) / ROUNDING * ROUNDING; }
MipiSpiBuffer() { this->rotation_ = ROTATION; }
MipiSpiBuffer() = default;
void dump_config() override {
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
OFFSET_HEIGHT>::dump_config();
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
MADCTL, HAS_HARDWARE_ROTATION>::dump_config();
esph_log_config(TAG,
" Rotation: %d°\n"
" Buffer pixels: %d bits\n"
@@ -450,14 +524,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
" Buffer bytes: %zu\n"
" Draw rounding: %u",
this->rotation_, BUFFERPIXEL * 8, FRACTION,
sizeof(BUFFERTYPE) * BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION, ROUNDING);
sizeof(BUFFERTYPE) * round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION, ROUNDING);
}
void setup() override {
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
OFFSET_HEIGHT>::setup();
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT,
MADCTL, HAS_HARDWARE_ROTATION>::setup();
RAMAllocator<BUFFERTYPE> allocator{};
this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION);
this->buffer_ = allocator.allocate(round_buffer(WIDTH) * round_buffer(HEIGHT) / FRACTION);
if (this->buffer_ == nullptr) {
this->mark_failed(LOG_STR("Buffer allocation failed"));
}
@@ -472,11 +546,13 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
}
// for updates with a small buffer, we repeatedly call the writer_ function, clipping the height to a fraction of
// the display height,
for (this->start_line_ = 0; this->start_line_ < HEIGHT; this->start_line_ += HEIGHT / FRACTION) {
for (this->start_line_ = 0; this->start_line_ < this->get_height_internal();
this->start_line_ += this->get_height_internal() / FRACTION) {
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
auto lap = millis();
#endif
this->end_line_ = this->start_line_ + HEIGHT / FRACTION;
this->end_line_ =
clamp_at_most(this->start_line_ + this->get_height_internal() / FRACTION, this->get_height_internal());
if (this->auto_clear_enabled_) {
this->clear();
}
@@ -503,10 +579,10 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
this->y_low_ - this->start_line_, BUFFER_WIDTH - w);
this->y_low_ - this->start_line_, round_buffer(this->get_width_internal()) - w);
// invalidate watermarks
this->x_low_ = WIDTH;
this->y_low_ = HEIGHT;
this->x_low_ = this->get_width_internal();
this->y_low_ = this->get_height_internal();
this->x_high_ = 0;
this->y_high_ = 0;
#if ESPHOME_LOG_LEVEL == ESPHOME_LOG_LEVEL_VERBOSE
@@ -523,10 +599,23 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
void draw_pixel_at(int x, int y, Color color) override {
if (!this->get_clipping().inside(x, y))
return;
rotate_coordinates(x, y);
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
if constexpr (not HAS_HARDWARE_ROTATION) {
if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) {
x = WIDTH - x - 1;
y = HEIGHT - y - 1;
} else if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES) {
auto tmp = x;
x = WIDTH - y - 1;
y = tmp;
} else if (this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES) {
auto tmp = y;
y = HEIGHT - x - 1;
x = tmp;
}
}
if (x < 0 || x >= this->get_width_internal() || y < this->start_line_ || y >= this->end_line_)
return;
this->buffer_[(y - this->start_line_) * BUFFER_WIDTH + x] = convert_color(color);
this->buffer_[(y - this->start_line_) * round_buffer(this->get_width_internal()) + x] = convert_color(color);
if (x < this->x_low_) {
this->x_low_ = x;
}
@@ -551,39 +640,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
this->x_low_ = 0;
this->y_low_ = this->start_line_;
this->x_high_ = WIDTH - 1;
this->x_high_ = this->get_width_internal() - 1;
this->y_high_ = this->end_line_ - 1;
std::fill_n(this->buffer_, HEIGHT * BUFFER_WIDTH / FRACTION, convert_color(color));
}
int get_width() override {
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
return HEIGHT;
return WIDTH;
}
int get_height() override {
if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES || ROTATION == display::DISPLAY_ROTATION_270_DEGREES)
return WIDTH;
return HEIGHT;
std::fill_n(this->buffer_, (this->end_line_ - this->start_line_) * round_buffer(this->get_width_internal()),
convert_color(color));
}
protected:
// Rotate the coordinates to match the display orientation.
static void rotate_coordinates(int &x, int &y) {
if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) {
x = WIDTH - x - 1;
y = HEIGHT - y - 1;
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_90_DEGREES) {
auto tmp = x;
x = WIDTH - y - 1;
y = tmp;
} else if constexpr (ROTATION == display::DISPLAY_ROTATION_270_DEGREES) {
auto tmp = y;
y = HEIGHT - x - 1;
x = tmp;
}
}
// Convert a color to the buffer pixel format.
static BUFFERTYPE convert_color(const Color &color) {
@@ -376,7 +376,7 @@ size_t ModbusController::create_register_ranges_() {
while (ix != this->sensorset_.end()) {
SensorItem *curr = *ix;
ESP_LOGV(TAG, "Register: 0x%X %d %d %d offset=%u skip=%u addr=%p", curr->start_address, curr->register_count,
ESP_LOGV(TAG, "Register: 0x%X %d %d %zu offset=%u skip=%u addr=%p", curr->start_address, curr->register_count,
curr->offset, curr->get_register_size(), curr->offset, curr->skip_updates, curr);
if (r.register_count == 0) {
@@ -484,18 +484,18 @@ void ModbusController::dump_config() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGCONFIG(TAG, "sensormap");
for (auto &it : this->sensorset_) {
ESP_LOGCONFIG(TAG, " Sensor type=%zu start=0x%X offset=0x%X count=%d size=%d",
ESP_LOGCONFIG(TAG, " Sensor type=%u start=0x%X offset=0x%X count=%d size=%zu",
static_cast<uint8_t>(it->register_type), it->start_address, it->offset, it->register_count,
it->get_register_size());
}
ESP_LOGCONFIG(TAG, "ranges");
for (auto &it : this->register_ranges_) {
ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
it.start_address, it.register_count, it.skip_updates);
}
ESP_LOGCONFIG(TAG, "server registers");
for (auto &r : this->server_registers_) {
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%zu register_count=%u", r->address,
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address,
static_cast<uint8_t>(r->value_type), r->register_count);
}
#endif
@@ -524,7 +524,7 @@ void ModbusController::on_write_register_response(ModbusRegisterType register_ty
void ModbusController::dump_sensors_() {
ESP_LOGV(TAG, "sensors");
for (auto &it : this->sensorset_) {
ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%d offset=%d", it->start_address, it->register_count,
ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%zu offset=%d", it->start_address, it->register_count,
it->get_register_size(), it->offset);
}
}
+5
View File
@@ -1,3 +1,8 @@
import esphome.codegen as cg
st7735_ns = cg.esphome_ns.namespace("st7735")
DEPRECATED_COMPONENT = """
The 'st7735' component is deprecated and no new models will be added to it.
New model PRs should target the newer and more performant 'mipi_spi' component.
"""
+6
View File
@@ -1,3 +1,5 @@
import logging
from esphome import pins
import esphome.codegen as cg
from esphome.components import display, spi
@@ -15,6 +17,7 @@ from esphome.const import (
from . import st7735_ns
CODEOWNERS = ["@SenexCrenshaw"]
LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["spi"]
@@ -87,6 +90,9 @@ async def setup_st7735(var, config):
async def to_code(config):
LOGGER.warning(
"The 'st7735' component is deprecated, it is recommended to use 'mipi_spi' instead."
)
var = cg.new_Pvariable(
config[CONF_ID],
config[CONF_MODEL],
+6 -1
View File
@@ -20,7 +20,12 @@ bool CronTrigger::matches(const ESPTime &time) {
return time.is_valid() && this->seconds_[time.second] && this->minutes_[time.minute] && this->hours_[time.hour] &&
this->days_of_month_[time.day_of_month] && this->months_[time.month] && this->days_of_week_[time.day_of_week];
}
void CronTrigger::loop() {
void CronTrigger::setup() {
// Cron resolution is 1 second — check once per second instead of every loop iteration
this->set_interval(1000, [this]() { this->check_time_(); });
}
void CronTrigger::check_time_() {
ESPTime time = this->rtc_->now();
if (!time.is_valid())
return;
+2 -1
View File
@@ -26,10 +26,11 @@ class CronTrigger : public Trigger<>, public Component {
void add_day_of_week(uint8_t day_of_week);
void add_days_of_week(const std::vector<uint8_t> &days_of_week);
bool matches(const ESPTime &time);
void loop() override;
void setup() override;
float get_setup_priority() const override;
protected:
void check_time_();
std::bitset<61> seconds_;
std::bitset<60> minutes_;
std::bitset<24> hours_;
@@ -70,7 +70,7 @@ void UptimeTextSensor::update() {
if (show_seconds)
append_unit(buf, sizeof(buf), pos, this->separator_, seconds, this->seconds_text_);
this->publish_state(buf);
this->publish_state(buf, pos);
}
float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
+15 -2
View File
@@ -286,10 +286,11 @@ void DeferredUpdateEventSource::try_send_nodefer(const char *message, const char
this->send(message, event, id, reconnect);
}
void DeferredUpdateEventSourceList::loop() {
bool DeferredUpdateEventSourceList::loop() {
for (DeferredUpdateEventSource *dues : *this) {
dues->loop();
}
return !this->empty();
}
void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const char *event_type,
@@ -318,6 +319,7 @@ void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServer
es->onDisconnect([this, es](AsyncEventSourceClient *client) { this->on_client_disconnect_(es); });
es->handleRequest(request);
ws->enable_loop_soon_any_context();
}
void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource *source) {
@@ -413,13 +415,24 @@ void WebServer::setup() {
// doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly
// getting a lot of events
this->set_interval(10000, [this]() {
if (this->events_.empty())
return;
char buf[32];
auto uptime = static_cast<uint32_t>(millis_64() / 1000);
buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
this->events_.try_send_nodefer(buf, "ping", millis(), 30000);
});
}
void WebServer::loop() { this->events_.loop(); }
void WebServer::loop() {
// No SSE clients connected; stop looping until a new client connects via
// enable_loop_soon_any_context(). This is safe because:
// - set_interval/set_timeout/defer run via the Scheduler, independent of loop()
// - deferrable_send_state early-outs when no clients are connected
// - try_send_nodefer (log, ping) iterates sessions which are empty
// - REST API handlers use defer() which runs via the Scheduler
if (!this->events_.loop())
this->disable_loop();
}
#ifdef USE_LOGGER
void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
+2 -1
View File
@@ -169,7 +169,8 @@ class DeferredUpdateEventSourceList final : public std::list<DeferredUpdateEvent
void on_client_disconnect_(DeferredUpdateEventSource *source);
public:
void loop();
/// Returns true if there are event sources remaining (including pending cleanup).
bool loop();
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
@@ -484,9 +484,12 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
this->on_connect_(rsp);
}
this->sessions_.push_back(rsp);
// Wake up WebServer::loop() to drain deferred event queues for this client.
// Safe from httpd task context via the pending_enable_loop_ flag.
this->web_server_->enable_loop_soon_any_context();
}
void AsyncEventSource::loop() {
bool AsyncEventSource::loop() {
// Clean up dead sessions safely
// This follows the ESP-IDF pattern where free_ctx marks resources as dead
// and the main loop handles the actual cleanup to avoid race conditions
@@ -504,6 +507,7 @@ void AsyncEventSource::loop() {
++i;
}
}
return !this->sessions_.empty();
}
void AsyncEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
@@ -340,7 +340,8 @@ class AsyncEventSource : public AsyncWebHandler {
void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
void loop();
/// Returns true if there are sessions remaining (including pending cleanup).
bool loop();
bool empty() { return this->count() == 0; }
size_t count() const { return this->sessions_.size(); }
+2 -2
View File
@@ -10,9 +10,9 @@ tzdata>=2021.1 # from time
pyserial==3.5
platformio==6.1.19
esptool==5.2.0
click==8.3.1
click==8.3.2
esphome-dashboard==20260210.0
aioesphomeapi==44.8.1
aioesphomeapi==44.9.0
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.19.1 # dashboard_import
+2 -1
View File
@@ -44,7 +44,8 @@ def find_and_activate_virtualenv():
def run_command():
# Execute the remaining arguments in the new environment
if len(sys.argv) > 1:
subprocess.run(sys.argv[1:], check=False, close_fds=False)
result = subprocess.run(sys.argv[1:], check=False, close_fds=False)
sys.exit(result.returncode)
else:
print(
"No command provided to run in the virtual environment.",
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,55 @@
#include <benchmark/benchmark.h>
#include "esphome/components/button/button.h"
namespace esphome::button::benchmarks {
static constexpr int kInnerIterations = 2000;
// Minimal Button for benchmarking — press_action() is a no-op.
class BenchButton : public Button {
public:
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
protected:
void press_action() override {}
};
// --- Button::press() ---
// Measures: ESP_LOGD + press_action() + callback dispatch.
static void ButtonPress(benchmark::State &state) {
BenchButton button;
button.configure("test_button");
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
button.press();
}
benchmark::DoNotOptimize(&button);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ButtonPress);
// --- Button::press() with callback ---
// Measures callback dispatch overhead.
static void ButtonPress_WithCallback(benchmark::State &state) {
BenchButton button;
button.configure("test_button");
uint64_t callback_count = 0;
button.add_on_press_callback([&callback_count]() { callback_count++; });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
button.press();
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(ButtonPress_WithCallback);
} // namespace esphome::button::benchmarks
@@ -0,0 +1 @@
button:
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,121 @@
#include <benchmark/benchmark.h>
#include "esphome/components/number/number.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
// Minimal Number for benchmarking — control() publishes the value back.
class BenchNumber : public number::Number {
public:
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
protected:
void control(float value) override { this->publish_state(value); }
};
// Helper to create a typical number entity for benchmarks.
static void setup_number(BenchNumber &number) {
number.configure("test_number");
number.traits.set_min_value(0.0f);
number.traits.set_max_value(100.0f);
number.traits.set_step(1.0f);
number.traits.set_mode(number::NUMBER_MODE_SLIDER);
}
// --- Number::publish_state() ---
// Measures the publish path: set_has_state, store value, callback dispatch.
static void NumberPublish_State(benchmark::State &state) {
BenchNumber number;
setup_number(number);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
number.publish_state(static_cast<float>(i % 100));
}
benchmark::DoNotOptimize(number.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(NumberPublish_State);
// --- Number::publish_state() with callback ---
// Measures callback dispatch overhead.
static void NumberPublish_WithCallback(benchmark::State &state) {
BenchNumber number;
setup_number(number);
uint64_t callback_count = 0;
number.add_on_state_callback([&callback_count](float) { callback_count++; });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
number.publish_state(static_cast<float>(i % 100));
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(NumberPublish_WithCallback);
// --- NumberCall::perform() set value ---
// The most common number call — setting an absolute value.
// Exercises: validation against min/max, control() dispatch.
static void NumberCall_SetValue(benchmark::State &state) {
BenchNumber number;
setup_number(number);
number.publish_state(50.0f);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
float val = static_cast<float>(i % 100);
number.make_call().set_value(val).perform();
}
benchmark::DoNotOptimize(number.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(NumberCall_SetValue);
// --- NumberCall::perform() increment ---
// Exercises: state read, step arithmetic, max clamping.
static void NumberCall_Increment(benchmark::State &state) {
BenchNumber number;
setup_number(number);
number.publish_state(0.0f);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
number.make_call().number_increment(true).perform();
}
benchmark::DoNotOptimize(number.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(NumberCall_Increment);
// --- NumberCall::perform() decrement ---
// Exercises: state read, step arithmetic, min clamping.
static void NumberCall_Decrement(benchmark::State &state) {
BenchNumber number;
setup_number(number);
number.publish_state(100.0f);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
number.make_call().number_decrement(true).perform();
}
benchmark::DoNotOptimize(number.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(NumberCall_Decrement);
} // namespace esphome::benchmarks
@@ -0,0 +1 @@
number:
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,157 @@
#include <benchmark/benchmark.h>
#include "esphome/components/select/select.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
// Minimal Select for benchmarking — control() publishes directly by index.
class BenchSelect : public select::Select {
public:
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
protected:
void control(size_t index) override { this->publish_state(index); }
};
// Helper to create a select with the given options.
static void setup_select(BenchSelect &select, const char *name, std::initializer_list<const char *> options) {
select.configure(name);
select.traits.set_options(options);
select.publish_state(size_t(0));
}
// --- Select::publish_state(size_t) ---
// The fast path: publish by index, no string lookup.
static void SelectPublish_ByIndex(benchmark::State &state) {
BenchSelect select;
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.publish_state(static_cast<size_t>(i % 4));
}
benchmark::DoNotOptimize(select.active_index());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectPublish_ByIndex);
// --- Select::publish_state(const char *) ---
// The string path: requires index_of() lookup via strncmp.
static void SelectPublish_ByString(benchmark::State &state) {
BenchSelect select;
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
const char *options[] = {"off", "still", "move", "still+move"};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.publish_state(options[i % 4]);
}
benchmark::DoNotOptimize(select.active_index());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectPublish_ByString);
// --- Select::publish_state() with callback ---
// Measures callback dispatch overhead on the index path.
static void SelectPublish_WithCallback(benchmark::State &state) {
BenchSelect select;
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
uint64_t callback_count = 0;
select.add_on_state_callback([&callback_count](size_t) { callback_count++; });
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.publish_state(static_cast<size_t>(i % 4));
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectPublish_WithCallback);
// --- SelectCall::perform() set by index ---
// The fast call path — no string matching needed.
static void SelectCall_SetByIndex(benchmark::State &state) {
BenchSelect select;
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.make_call().set_index(i % 4).perform();
}
benchmark::DoNotOptimize(select.active_index());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectCall_SetByIndex);
// --- SelectCall::perform() set by option string ---
// Exercises the string lookup path through index_of().
static void SelectCall_SetByOption(benchmark::State &state) {
BenchSelect select;
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
const char *options[] = {"off", "still", "move", "still+move"};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.make_call().set_option(options[i % 4]).perform();
}
benchmark::DoNotOptimize(select.active_index());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectCall_SetByOption);
// --- SelectCall::perform() next with cycling ---
// Exercises the navigation path through active_index_.
static void SelectCall_NextCycle(benchmark::State &state) {
BenchSelect select;
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.make_call().select_next(true).perform();
}
benchmark::DoNotOptimize(select.active_index());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectCall_NextCycle);
// --- SelectCall with 10 options (string lookup) ---
// Worst-case string matching with more options.
static void SelectCall_SetByOption_10Options(benchmark::State &state) {
BenchSelect select;
setup_select(
select, "test_select",
{"off", "still", "move", "still+move", "custom1", "custom2", "custom3", "custom4", "custom5", "custom6"});
// Pick options spread across the list to exercise different search depths
const char *picks[] = {"off", "custom3", "custom6", "move"};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
select.make_call().set_option(picks[i % 4]).perform();
}
benchmark::DoNotOptimize(select.active_index());
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SelectCall_SetByOption_10Options);
} // namespace esphome::benchmarks
@@ -0,0 +1 @@
select:
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,137 @@
#include <benchmark/benchmark.h>
#include "esphome/components/switch/switch.h"
namespace esphome::benchmarks {
// Inner iteration count to amortize CodSpeed instrumentation overhead.
static constexpr int kInnerIterations = 2000;
// Minimal Switch for benchmarking — write_state() publishes directly.
class BenchSwitch : public switch_::Switch {
public:
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
protected:
void write_state(bool state) override { this->publish_state(state); }
};
// --- Switch::publish_state() alternating ---
// Forces state change every call, exercising the full publish path.
static void SwitchPublish_Alternating(benchmark::State &state) {
BenchSwitch sw;
sw.configure("test_switch");
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
sw.publish_state(false);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sw.publish_state(i % 2 == 0);
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SwitchPublish_Alternating);
// --- Switch::publish_state() no change ---
// Tests the deduplication fast path in publish_dedup_.
static void SwitchPublish_NoChange(benchmark::State &state) {
BenchSwitch sw;
sw.configure("test_switch");
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
sw.publish_state(true);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sw.publish_state(true);
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SwitchPublish_NoChange);
// --- Switch::publish_state() with callback ---
// Measures callback dispatch overhead on state changes.
static void SwitchPublish_WithCallback(benchmark::State &state) {
BenchSwitch sw;
sw.configure("test_switch");
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
uint64_t callback_count = 0;
sw.add_on_state_callback([&callback_count](bool) { callback_count++; });
sw.publish_state(false);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sw.publish_state(i % 2 == 0);
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SwitchPublish_WithCallback);
// --- Switch::turn_on() / turn_off() ---
// The front-end call path: turn_on → write_state → publish_state.
static void SwitchTurnOn(benchmark::State &state) {
BenchSwitch sw;
sw.configure("test_switch");
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
sw.publish_state(false);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sw.turn_on();
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SwitchTurnOn);
// --- Switch::toggle() alternating ---
// Exercises the toggle path which reads current state to determine target.
static void SwitchToggle(benchmark::State &state) {
BenchSwitch sw;
sw.configure("test_switch");
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
sw.publish_state(false);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sw.toggle();
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SwitchToggle);
// --- Switch::publish_state() inverted ---
// Verifies the inversion path doesn't add significant overhead.
static void SwitchPublish_Inverted(benchmark::State &state) {
BenchSwitch sw;
sw.configure("test_switch");
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
sw.set_inverted(true);
sw.publish_state(false);
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sw.publish_state(i % 2 == 0);
}
benchmark::DoNotOptimize(sw.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(SwitchPublish_Inverted);
} // namespace esphome::benchmarks
@@ -0,0 +1 @@
switch:
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.enable_codegen()
@@ -0,0 +1,108 @@
#include <benchmark/benchmark.h>
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome::text_sensor::benchmarks {
static constexpr int kInnerIterations = 2000;
// --- publish_state(const char *) with short string, value changes each time ---
// Exercises: memcmp check (mismatch), string assign, callback dispatch.
static void TextSensorPublish_Short_Changing(benchmark::State &state) {
TextSensor sensor;
// Pre-populate with different short strings
const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(values[i % 4]);
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(TextSensorPublish_Short_Changing);
// --- publish_state(const char *) with short string, same value (dedup path) ---
// Exercises: memcmp check (match), skips string assign.
static void TextSensorPublish_Short_NoChange(benchmark::State &state) {
TextSensor sensor;
sensor.publish_state("192.168.1.100");
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state("192.168.1.100");
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(TextSensorPublish_Short_NoChange);
// --- publish_state with longer string (firmware version, MAC address) ---
// Exercises: memcmp on longer strings, string assign with potential realloc.
static void TextSensorPublish_Long_Changing(benchmark::State &state) {
TextSensor sensor;
const char *values[] = {
"2025.12.0-dev (Jan 15 2025, 10:30:00)",
"2025.12.1-dev (Feb 20 2025, 14:45:00)",
"2025.12.2-dev (Mar 10 2025, 08:15:00)",
"2025.12.3-dev (Apr 5 2025, 16:00:00)",
};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(values[i % 4]);
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(TextSensorPublish_Long_Changing);
// --- publish_state with callback ---
// Measures callback dispatch overhead for text sensors.
static void TextSensorPublish_WithCallback(benchmark::State &state) {
TextSensor sensor;
uint64_t callback_count = 0;
sensor.add_on_state_callback([&callback_count](const std::string &) { callback_count++; });
const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(values[i % 4]);
}
benchmark::DoNotOptimize(callback_count);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(TextSensorPublish_WithCallback);
// --- publish_state(const char *, size_t) direct ---
// The lowest-level overload, avoids strlen.
static void TextSensorPublish_WithLen(benchmark::State &state) {
TextSensor sensor;
static constexpr const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"};
static constexpr size_t lens[] = {11, 11, 11, 11};
for (auto _ : state) {
for (int i = 0; i < kInnerIterations; i++) {
sensor.publish_state(values[i % 4], lens[i % 4]);
}
benchmark::DoNotOptimize(sensor.state);
}
state.SetItemsProcessed(state.iterations() * kInnerIterations);
}
BENCHMARK(TextSensorPublish_WithLen);
} // namespace esphome::text_sensor::benchmarks
@@ -0,0 +1 @@
text_sensor:

Some files were not shown because too many files have changed in this diff Show More