Merge branch 'dev' into api/peel-first-write-iteration

This commit is contained in:
J. Nick Koston
2026-04-03 22:07:42 -10:00
committed by GitHub
146 changed files with 3539 additions and 881 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 -4
View File
@@ -102,12 +102,12 @@ jobs:
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to docker hub
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -182,13 +182,13 @@ jobs:
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+5 -1
View File
@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.8
rev: v0.15.9
hooks:
# Run the linter.
- id: ruff
@@ -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
+1 -1
View File
@@ -142,7 +142,7 @@ esphome/components/dlms_meter/* @SimonFischer04
esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds2484/* @mrk-its
esphome/components/dsmr/* @glmnet @PolarGoose @zuidwijk
esphome/components/dsmr/* @glmnet @PolarGoose
esphome/components/duty_time/* @dudanov
esphome/components/ee895/* @Stock-M
esphome/components/ektf2232/touchscreen/* @jesserockz
+2 -2
View File
@@ -177,7 +177,7 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
template_ = int(template_ / 1000000)
cg.add(var.set_frequency(template_))
if sens_dist := config.get(CONF_SENSING_DISTANCE):
if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None:
template_ = await cg.templatable(sens_dist, args, int)
cg.add(var.set_sensing_distance(template_))
@@ -209,7 +209,7 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
template_ = int(template_)
cg.add(var.set_trigger_keep(template_))
if stage_gain := config.get(CONF_STAGE_GAIN):
if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None:
template_ = await cg.templatable(stage_gain, args, int)
cg.add(var.set_stage_gain(template_))
+1
View File
@@ -210,6 +210,7 @@ async def to_code(config):
data = _get_data()
if data.flac_support:
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
add_idf_component(name="esphome/micro-flac", ref="0.1.1")
if data.mp3_support:
cg.add_define("USE_AUDIO_MP3_SUPPORT")
if data.opus_support:
+33 -50
View File
@@ -84,13 +84,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) {
switch (this->audio_file_type_) {
#ifdef USE_AUDIO_FLAC_SUPPORT
case AudioFileType::FLAC:
this->flac_decoder_ = make_unique<esp_audio_libs::flac::FLACDecoder>();
// CRC check slows down decoding by 15-20% on an ESP32-S3. FLAC sources in ESPHome are either from an http source
// or built into the firmware, so the data integrity is already verified by the time it gets to the decoder,
// making the CRC check unnecessary.
this->flac_decoder_->set_crc_check_enabled(false);
this->flac_decoder_ = make_unique<micro_flac::FLACDecoder>();
this->free_buffer_required_ =
this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header
this->decoder_buffers_internally_ = true;
break;
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
@@ -268,59 +265,45 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
#ifdef USE_AUDIO_FLAC_SUPPORT
FileDecoderState AudioDecoder::decode_flac_() {
if (!this->audio_stream_info_.has_value()) {
// Header hasn't been read
auto result = this->flac_decoder_->read_header(this->input_buffer_->data(), this->input_buffer_->available());
size_t bytes_consumed, samples_decoded;
if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
// Serrious error reading FLAC header, there is no recovery
return FileDecoderState::FAILED;
micro_flac::FLACDecoderResult result = this->flac_decoder_->decode(
this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(),
this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded);
if (result == micro_flac::FLAC_DECODER_SUCCESS) {
if (samples_decoded > 0 && this->audio_stream_info_.has_value()) {
this->output_transfer_buffer_->increase_buffer_length(
this->audio_stream_info_.value().samples_to_bytes(samples_decoded));
}
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_buffer_->consume(bytes_consumed);
} else if (result == micro_flac::FLAC_DECODER_HEADER_READY) {
// Header just parsed, stream info now available
const auto &info = this->flac_decoder_->get_stream_info();
this->audio_stream_info_ = audio::AudioStreamInfo(info.bits_per_sample(), info.num_channels(), info.sample_rate());
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
return FileDecoderState::MORE_TO_PROCESS;
}
// Reallocate the output transfer buffer to the smallest necessary size
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
// Reallocate the output transfer buffer to the required size
this->free_buffer_required_ = this->flac_decoder_->get_output_buffer_size_samples() * info.bytes_per_sample();
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
// Couldn't reallocate output buffer
return FileDecoderState::FAILED;
}
this->audio_stream_info_ =
audio::AudioStreamInfo(this->flac_decoder_->get_sample_depth(), this->flac_decoder_->get_num_channels(),
this->flac_decoder_->get_sample_rate());
return FileDecoderState::MORE_TO_PROCESS;
}
uint32_t output_samples = 0;
auto result = this->flac_decoder_->decode_frame(this->input_buffer_->data(), this->input_buffer_->available(),
this->output_transfer_buffer_->get_buffer_end(), &output_samples);
if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
// Not an issue, just needs more data that we'll get next time.
return FileDecoderState::POTENTIALLY_FAILED;
}
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_buffer_->consume(bytes_consumed);
if (result > esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
// Corrupted frame, don't retry with current buffer content, wait for new sync
return FileDecoderState::POTENTIALLY_FAILED;
}
// We have successfully decoded some input data and have new output data
this->output_transfer_buffer_->increase_buffer_length(
this->audio_stream_info_.value().samples_to_bytes(output_samples));
if (result == esp_audio_libs::flac::FLAC_DECODER_NO_MORE_FRAMES) {
this->input_buffer_->consume(bytes_consumed);
} else if (result == micro_flac::FLAC_DECODER_END_OF_STREAM) {
this->input_buffer_->consume(bytes_consumed);
return FileDecoderState::END_OF_FILE;
} else if (result == micro_flac::FLAC_DECODER_NEED_MORE_DATA) {
this->input_buffer_->consume(bytes_consumed);
return FileDecoderState::MORE_TO_PROCESS;
} else if (result == micro_flac::FLAC_DECODER_ERROR_OUTPUT_TOO_SMALL) {
// Reallocate to decode the frame on the next call
const auto &info = this->flac_decoder_->get_stream_info();
this->free_buffer_required_ = this->flac_decoder_->get_output_buffer_size_samples() * info.bytes_per_sample();
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
return FileDecoderState::FAILED;
}
} else {
ESP_LOGE(TAG, "FLAC decoder failed: %d", static_cast<int>(result));
return FileDecoderState::POTENTIALLY_FAILED;
}
return FileDecoderState::MORE_TO_PROCESS;
+6 -4
View File
@@ -16,14 +16,16 @@
#include "esp_err.h"
// esp-audio-libs
#ifdef USE_AUDIO_FLAC_SUPPORT
#include <flac_decoder.h>
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
#include <mp3_decoder.h>
#endif
#include <wav_decoder.h>
// micro-flac
#ifdef USE_AUDIO_FLAC_SUPPORT
#include <micro_flac/flac_decoder.h>
#endif
// micro-opus
#ifdef USE_AUDIO_OPUS_SUPPORT
#include <micro_opus/ogg_opus_decoder.h>
@@ -119,7 +121,7 @@ class AudioDecoder {
std::unique_ptr<esp_audio_libs::wav_decoder::WAVDecoder> wav_decoder_;
#ifdef USE_AUDIO_FLAC_SUPPORT
FileDecoderState decode_flac_();
std::unique_ptr<esp_audio_libs::flac::FLACDecoder> flac_decoder_;
std::unique_ptr<micro_flac::FLACDecoder> flac_decoder_;
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
FileDecoderState decode_mp3_();
+1 -1
View File
@@ -126,7 +126,7 @@ def set_reference_values(config):
config.setdefault(CONF_POWER_REFERENCE, DEFAULT_BL0940_LEGACY_PREF)
config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_EREF)
else:
vref = config.get(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_VREF)
vref = config.get(CONF_REFERENCE_VOLTAGE, DEFAULT_BL0940_VREF)
r_one = config.get(CONF_RESISTOR_ONE, DEFAULT_BL0940_R1)
r_two = config.get(CONF_RESISTOR_TWO, DEFAULT_BL0940_R2)
r_shunt = config.get(CONF_RESISTOR_SHUNT, DEFAULT_BL0940_RL)
@@ -88,7 +88,7 @@ async def to_code(config):
)
cg.add(var.set_char_uuid128(uuid128))
if descriptor_uuid := config:
if descriptor_uuid := config.get(CONF_DESCRIPTOR_UUID):
if len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid16_format):
cg.add(var.set_descr_uuid16(esp32_ble_tracker.as_hex(descriptor_uuid)))
elif len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid32_format):
+1 -1
View File
@@ -161,7 +161,7 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_CANBUS_ID])
if can_id := config.get(CONF_CAN_ID):
if (can_id := config.get(CONF_CAN_ID)) is not None:
can_id = await cg.templatable(can_id, args, cg.uint32)
cg.add(var.set_can_id(can_id))
cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID]))
+1 -1
View File
@@ -400,7 +400,7 @@ async def setup_climate_core_(var, config):
)
) is not None:
cg.add(
mqtt_.set_custom_target_temperature_state_topic(
mqtt_.set_custom_target_temperature_low_state_topic(
target_temperature_low_state_topic
)
)
+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"
+1 -2
View File
@@ -266,8 +266,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_WAKEUP_PIN): validate_wakeup_pin,
cv.Optional(CONF_WAKEUP_PIN_MODE): cv.All(
cv.only_on([PLATFORM_ESP32, PLATFORM_BK72XX]),
cv.enum(WAKEUP_PIN_MODES),
upper=True,
cv.enum(WAKEUP_PIN_MODES, upper=True),
),
cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
cv.only_on_esp32,
+41 -2
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")
@@ -112,8 +117,9 @@ FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
async def setup_display_core_(var, config):
if CONF_ROTATION in config:
cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
if rotation := config.get(CONF_ROTATION, 0):
# Default initialised value for rotation is 0
cg.add(var.set_rotation(DISPLAY_ROTATIONS[rotation]))
if (auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED)) is not None:
# Default to true if pages or lambda is specified. Ideally this would be done during validation, but
@@ -146,6 +152,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; }
+4 -1
View File
@@ -4,7 +4,7 @@ from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_UART_ID
CODEOWNERS = ["@glmnet", "@zuidwijk", "@PolarGoose"]
CODEOWNERS = ["@glmnet", "@PolarGoose"]
MULTI_CONF = True
@@ -16,6 +16,7 @@ CONF_DECRYPTION_KEY = "decryption_key"
CONF_DSMR_ID = "dsmr_id"
CONF_GAS_MBUS_ID = "gas_mbus_id"
CONF_WATER_MBUS_ID = "water_mbus_id"
CONF_THERMAL_MBUS_ID = "thermal_mbus_id"
CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length"
CONF_REQUEST_INTERVAL = "request_interval"
CONF_REQUEST_PIN = "request_pin"
@@ -35,6 +36,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean,
cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_,
cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_,
cv.Optional(CONF_THERMAL_MBUS_ID, default=3): cv.int_,
cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_,
cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema,
cv.Optional(
@@ -64,6 +66,7 @@ async def to_code(config):
cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID]))
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
cg.add_build_flag("-DDSMR_THERMAL_MBUS_ID=" + str(config[CONF_THERMAL_MBUS_ID]))
# DSMR Parser
cg.add_library("esphome/dsmr_parser", "1.1.0")
+1 -13
View File
@@ -175,9 +175,7 @@ async def to_code(config):
*model.get_constructor_args(config),
)
# Rotation is handled by setting the transform
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
await display.register_display(var, display_config)
await display.register_display(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
@@ -201,16 +199,6 @@ async def to_code(config):
transform[CONF_SWAP_XY] = False
else:
transform = {x: model.get_default(x, False) for x in TRANSFORM_OPTIONS}
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]
elif rotation == 270:
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
transform_str = "|".join(
{
str(getattr(Transform, x.upper()))
+20 -3
View File
@@ -97,6 +97,23 @@ bool EPaperBase::reset() {
return true;
}
void EPaperBase::update_effective_transform_() {
switch (this->rotation_) {
case DISPLAY_ROTATION_90_DEGREES:
this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_X);
break;
case DISPLAY_ROTATION_180_DEGREES:
this->effective_transform_ = this->transform_ ^ (MIRROR_Y | MIRROR_X);
break;
case DISPLAY_ROTATION_270_DEGREES:
this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_Y);
break;
default:
this->effective_transform_ = this->transform_;
break;
}
}
void EPaperBase::update() {
if (this->state_ != EPaperState::IDLE) {
ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_());
@@ -280,11 +297,11 @@ bool EPaperBase::initialise(bool partial) {
bool EPaperBase::rotate_coordinates_(int &x, int &y) {
if (!this->get_clipping().inside(x, y))
return false;
if (this->transform_ & SWAP_XY)
if (this->effective_transform_ & SWAP_XY)
std::swap(x, y);
if (this->transform_ & MIRROR_X)
if (this->effective_transform_ & MIRROR_X)
x = this->width_ - x - 1;
if (this->transform_ & MIRROR_Y)
if (this->effective_transform_ & MIRROR_Y)
y = this->height_ - y - 1;
if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
return false;
+13 -4
View File
@@ -1,6 +1,6 @@
#pragma once
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/display.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/split_buffer/split_buffer.h"
#include "esphome/core/component.h"
@@ -51,7 +51,14 @@ class EPaperBase : public Display,
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
void set_transform(uint8_t transform) { this->transform_ = transform; }
void set_transform(uint8_t transform) {
this->transform_ = transform;
this->update_effective_transform_();
}
void set_rotation(DisplayRotation rotation) override {
Display::set_rotation(rotation);
this->update_effective_transform_();
}
void set_full_update_every(uint8_t full_update_every) { this->full_update_every_ = full_update_every; }
void dump_config() override;
@@ -106,8 +113,8 @@ class EPaperBase : public Display,
protected:
int get_height_internal() override { return this->height_; };
int get_width_internal() override { return this->width_; };
int get_width() override { return this->transform_ & SWAP_XY ? this->height_ : this->width_; }
int get_height() override { return this->transform_ & SWAP_XY ? this->width_ : this->height_; }
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
void draw_pixel_at(int x, int y, Color color) override;
void process_state_();
@@ -119,6 +126,7 @@ class EPaperBase : public Display,
void send_init_sequence_(const uint8_t *sequence, size_t length);
void wait_for_idle_(bool should_wait);
bool init_buffer_(size_t buffer_length);
void update_effective_transform_();
bool rotate_coordinates_(int &x, int &y);
/**
@@ -171,6 +179,7 @@ class EPaperBase : public Display,
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 effective_transform_{};
uint8_t update_count_{};
// these values represent the bounds of the updated buffer. Note that x_high and y_high
// point to the pixel past the last one updated, i.e. may range up to width/height.
+1
View File
@@ -132,6 +132,7 @@ async def to_code(config):
if wifi_channel := config.get(CONF_CHANNEL):
cg.add(var.set_wifi_channel(wifi_channel))
cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
cg.add(var.set_auto_add_peer(config[CONF_AUTO_ADD_PEER]))
for peer in config.get(CONF_PEERS, []):
+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
+1 -1
View File
@@ -118,6 +118,6 @@ async def set_heater_level_to_code(config, action_id, template_arg, args):
async def set_heater_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
status_ = await cg.templatable(config[CONF_LEVEL], args, bool)
status_ = await cg.templatable(config[CONF_STATUS], args, bool)
cg.add(var.set_status(status_))
return var
+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,
+5 -7
View File
@@ -198,13 +198,13 @@ LightColorValues LightCall::validate_() {
// Ensure there is always a color mode set
if (!this->has_color_mode()) {
this->color_mode_ = this->compute_color_mode_();
this->color_mode_ = this->compute_color_mode_(traits);
this->set_flag_(FLAG_HAS_COLOR_MODE);
}
auto color_mode = this->color_mode_;
// Transform calls that use non-native parameters for the current mode.
this->transform_parameters_();
this->transform_parameters_(traits);
// Business logic adjustments before validation
// Flag whether an explicit turn off was requested, in which case we'll also stop the effect.
@@ -366,9 +366,7 @@ LightColorValues LightCall::validate_() {
return v;
}
void LightCall::transform_parameters_() {
auto traits = this->parent_->get_traits();
void LightCall::transform_parameters_(const LightTraits &traits) {
// Allow CWWW modes to be set with a white value and/or color temperature.
// This is used in three cases in HA:
// - CW/WW lights, which set the "brightness" and "color_temperature"
@@ -407,8 +405,8 @@ void LightCall::transform_parameters_() {
}
}
}
ColorMode LightCall::compute_color_mode_() {
auto supported_modes = this->parent_->get_traits().get_supported_color_modes();
ColorMode LightCall::compute_color_mode_(const LightTraits &traits) {
auto supported_modes = traits.get_supported_color_modes();
int supported_count = supported_modes.size();
// Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown.
+3 -2
View File
@@ -10,6 +10,7 @@ struct LogString;
namespace light {
class LightState;
class LightTraits;
/** This class represents a requested change in a light state.
*
@@ -188,11 +189,11 @@ class LightCall {
LightColorValues validate_();
//// Compute the color mode that should be used for this call.
ColorMode compute_color_mode_();
ColorMode compute_color_mode_(const LightTraits &traits);
/// Get potential color modes bitmask for this light call.
color_mode_bitmask_t get_suitable_color_modes_mask_();
/// Some color modes also can be set using non-native parameters, transform those calls.
void transform_parameters_();
void transform_parameters_(const LightTraits &traits);
// Bitfield flags - each flag indicates whether a corresponding value has been set.
enum FieldFlags : uint16_t {
@@ -104,9 +104,10 @@ class LightColorValues {
this->green_ = 1.0f;
this->blue_ = 1.0f;
} else {
this->red_ /= max_value;
this->green_ /= max_value;
this->blue_ /= max_value;
float inv = 1.0f / max_value;
this->red_ *= inv;
this->green_ *= inv;
this->blue_ *= inv;
}
}
}
+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())
+28 -13
View File
@@ -94,6 +94,9 @@ _STATE_CONDITIONS = [
PlayMediaAction = media_player_ns.class_(
"PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer)
)
EnqueueMediaAction = media_player_ns.class_(
"EnqueueMediaAction", automation.Action, cg.Parented.template(MediaPlayer)
)
VolumeSetAction = media_player_ns.class_(
"VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer)
)
@@ -168,20 +171,17 @@ MEDIA_PLAYER_CONDITION_SCHEMA = automation.maybe_simple_id(
)
@automation.register_action(
"media_player.play_media",
PlayMediaAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(MediaPlayer),
cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url),
cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean),
},
key=CONF_MEDIA_URL,
),
synchronous=True,
_MEDIA_URL_ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(MediaPlayer),
cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url),
cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean),
},
key=CONF_MEDIA_URL,
)
async def media_player_play_media_action(config, action_id, template_arg, args):
async def _media_action_handler(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
media_url = await cg.templatable(config[CONF_MEDIA_URL], args, cg.std_string)
@@ -191,6 +191,21 @@ async def media_player_play_media_action(config, action_id, template_arg, args):
return var
automation.register_action(
"media_player.play_media",
PlayMediaAction,
_MEDIA_URL_ACTION_SCHEMA,
synchronous=True,
)(_media_action_handler)
automation.register_action(
"media_player.enqueue",
EnqueueMediaAction,
_MEDIA_URL_ACTION_SCHEMA,
synchronous=True,
)(_media_action_handler)
def _snake_to_camel(name):
return "".join(word.capitalize() for word in name.split("_"))
+11 -5
View File
@@ -55,17 +55,23 @@ using GroupJoinAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYE
template<typename... Ts>
using ClearPlaylistAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST, Ts...>;
template<typename... Ts> class PlayMediaAction : public Action<Ts...>, public Parented<MediaPlayer> {
template<MediaPlayerCommand Command, typename... Ts>
class MediaPlayerMediaAction : public Action<Ts...>, public Parented<MediaPlayer> {
TEMPLATABLE_VALUE(std::string, media_url)
TEMPLATABLE_VALUE(bool, announcement)
void play(const Ts &...x) override {
this->parent_->make_call()
.set_media_url(this->media_url_.value(x...))
.set_announcement(this->announcement_.value(x...))
.perform();
auto call = this->parent_->make_call();
if constexpr (Command != MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PLAY)
call.set_command(Command);
call.set_media_url(this->media_url_.value(x...)).set_announcement(this->announcement_.value(x...)).perform();
}
};
template<typename... Ts>
using PlayMediaAction = MediaPlayerMediaAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PLAY, Ts...>;
template<typename... Ts>
using EnqueueMediaAction = MediaPlayerMediaAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_ENQUEUE, Ts...>;
template<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Parented<MediaPlayer> {
TEMPLATABLE_VALUE(float, volume)
void play(const Ts &...x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); }
+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) {
@@ -1,7 +1,195 @@
#include <array>
#include <numeric>
#include "mitsubishi_cn105.h"
namespace esphome::mitsubishi_cn105 {
static const char *const TAG = "mitsubishi_cn105.driver";
static constexpr uint32_t WRITE_TIMEOUT_MS = 2000;
static constexpr size_t HEADER_LEN = 5;
static constexpr uint8_t PREAMBLE = 0xFC;
static constexpr uint8_t HEADER_BYTE_1 = 0x01;
static constexpr uint8_t HEADER_BYTE_2 = 0x30;
static constexpr uint8_t PACKET_TYPE_CONNECT_REQUEST = 0x5A;
static constexpr uint8_t PACKET_TYPE_CONNECT_RESPONSE = 0x7A;
static constexpr std::array<uint8_t, 2> CONNECT_REQUEST_PAYLOAD = {{0xCA, 0x01}};
static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) {
return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0}));
}
template<std::size_t PayloadSize>
static constexpr auto make_packet(uint8_t type, const std::array<uint8_t, PayloadSize> &payload) {
const size_t full_len = PayloadSize + HEADER_LEN + 1;
std::array<uint8_t, full_len> packet{PREAMBLE, type, HEADER_BYTE_1, HEADER_BYTE_2, static_cast<uint8_t>(PayloadSize)};
std::copy_n(payload.begin(), PayloadSize, packet.begin() + HEADER_LEN);
packet.back() = checksum(packet.data(), packet.size() - 1);
return packet;
}
static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, CONNECT_REQUEST_PAYLOAD);
void MitsubishiCN105::initialize() { this->set_state_(State::CONNECTING); }
void MitsubishiCN105::update() {
if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) {
this->write_timeout_start_ms_.reset();
this->read_pos_ = 0;
this->set_state_(State::READ_TIMEOUT);
return;
}
this->read_incoming_bytes_();
}
void MitsubishiCN105::set_state_(State new_state) {
if (should_transition(this->state_, new_state)) {
ESP_LOGV(TAG, "Did transition: %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
LOG_STR_ARG(state_to_string(new_state)));
this->state_ = new_state;
this->did_transition_(new_state);
} else {
ESP_LOGV(TAG, "Ignoring unexpected transition %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
LOG_STR_ARG(state_to_string(new_state)));
}
}
bool MitsubishiCN105::should_transition(State from, State to) {
switch (to) {
case State::CONNECTING:
return from == State::NOT_CONNECTED || from == State::READ_TIMEOUT;
case State::CONNECTED:
case State::READ_TIMEOUT:
return from == State::CONNECTING;
default:
return false;
}
}
void MitsubishiCN105::did_transition_(State to) {
switch (to) {
case State::CONNECTING:
this->send_packet_(CONNECT_PACKET);
break;
case State::CONNECTED:
this->write_timeout_start_ms_.reset();
// TODO: read AC status after connected, next PR
break;
case State::READ_TIMEOUT:
this->set_state_(State::CONNECTING);
break;
default:
break;
}
}
void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) {
dump_buffer_vv("TX", packet, len);
this->device_.write_array(packet, len);
this->write_timeout_start_ms_ = get_loop_time_ms();
}
void MitsubishiCN105::read_incoming_bytes_() {
uint8_t watchdog = 64;
while (this->device_.available() > 0 && watchdog-- > 0) {
uint8_t &value = this->read_buffer_[this->read_pos_];
if (!this->device_.read_byte(&value)) {
ESP_LOGW(TAG, "UART read failed while data available");
return;
}
switch (++this->read_pos_) {
case 1:
if (value != PREAMBLE) {
this->reset_read_position_and_dump_buffer_("RX ignoring preamble");
}
continue;
case 2:
continue;
case 3:
if (value != HEADER_BYTE_1) {
this->reset_read_position_and_dump_buffer_("RX invalid: header 1 mismatch");
}
continue;
case 4:
if (value != HEADER_BYTE_2) {
this->reset_read_position_and_dump_buffer_("RX invalid: header 2 mismatch");
}
continue;
case HEADER_LEN:
static_assert(READ_BUFFER_SIZE > HEADER_LEN);
if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) {
this->reset_read_position_and_dump_buffer_("RX invalid: payload too large");
}
continue;
default:
break;
}
const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]);
if (this->read_pos_ <= len_without_checksum) {
continue;
}
if (checksum(this->read_buffer_, len_without_checksum) != value) {
this->reset_read_position_and_dump_buffer_("RX invalid: checksum mismatch");
continue;
}
this->process_rx_packet_(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN);
this->reset_read_position_and_dump_buffer_("RX");
}
}
void MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) {
switch (type) {
case PACKET_TYPE_CONNECT_RESPONSE:
this->set_state_(State::CONNECTED);
break;
default:
ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type);
break;
}
}
void MitsubishiCN105::reset_read_position_and_dump_buffer_(const char *prefix) {
dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_);
this->read_pos_ = 0;
}
void MitsubishiCN105::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char buf[format_hex_pretty_size(READ_BUFFER_SIZE)];
ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len));
#endif
}
const LogString *MitsubishiCN105::state_to_string(State state) {
switch (state) {
case State::NOT_CONNECTED:
return LOG_STR("Not connected");
case State::CONNECTING:
return LOG_STR("Connecting");
case State::CONNECTED:
return LOG_STR("Connected");
case State::READ_TIMEOUT:
return LOG_STR("ReadTimeout");
}
return LOG_STR("Unknown");
}
} // namespace esphome::mitsubishi_cn105
@@ -1,19 +1,45 @@
#pragma once
#include <optional>
#include "esphome/components/uart/uart.h"
namespace esphome::mitsubishi_cn105 {
uint32_t get_loop_time_ms();
class MitsubishiCN105 {
public:
explicit MitsubishiCN105(uart::UARTDevice &device) : device_(device) {}
void initialize();
void update();
uint32_t get_update_interval() const { return this->update_interval_ms_; }
void set_update_interval(uint32_t interval_ms) { this->update_interval_ms_ = interval_ms; }
protected:
enum class State : uint8_t { NOT_CONNECTED, CONNECTING, CONNECTED, READ_TIMEOUT };
void set_state_(State new_state);
void did_transition_(State to);
void read_incoming_bytes_();
void process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len);
void reset_read_position_and_dump_buffer_(const char *prefix);
void send_packet_(const uint8_t *packet, size_t len);
template<typename T> void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); }
static bool should_transition(State from, State to);
static const LogString *state_to_string(State state);
static void dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len);
uart::UARTDevice &device_;
uint32_t update_interval_ms_{1000};
std::optional<uint32_t> write_timeout_start_ms_;
State state_{State::NOT_CONNECTED};
private:
static constexpr size_t READ_BUFFER_SIZE = 32;
uint8_t read_buffer_[READ_BUFFER_SIZE];
uint8_t read_pos_{0};
};
} // namespace esphome::mitsubishi_cn105
@@ -1,3 +1,4 @@
#include <cinttypes>
#include "mitsubishi_cn105_climate.h"
#include "esphome/core/log.h"
@@ -14,9 +15,9 @@ void MitsubishiCN105Climate::dump_config() {
LOG_STR_ARG(parity_to_str(this->parent_->get_parity())), this->parent_->get_stop_bits());
}
void MitsubishiCN105Climate::setup() {}
void MitsubishiCN105Climate::setup() { this->hp_.initialize(); }
void MitsubishiCN105Climate::loop() {}
void MitsubishiCN105Climate::loop() { this->hp_.update(); }
climate::ClimateTraits MitsubishiCN105Climate::traits() {
climate::ClimateTraits traits;
@@ -0,0 +1,7 @@
#include "esphome/core/application.h"
namespace esphome::mitsubishi_cn105 {
uint32_t __attribute__((weak)) get_loop_time_ms() { return App.get_loop_component_start_time(); };
} // namespace esphome::mitsubishi_cn105
-3
View File
@@ -130,9 +130,6 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_DRDY_PIN in config:
pin = await cg.gpio_pin_expression(config[CONF_DRDY_PIN])
cg.add(var.set_drdy_pin(pin))
cg.add(var.set_gain(GAIN[config[CONF_GAIN]]))
cg.add(var.set_oversampling(config[CONF_OVERSAMPLING]))
cg.add(var.set_filter(config[CONF_FILTER]))
@@ -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);
}
}
+2 -4
View File
@@ -2,8 +2,7 @@
#include "esphome/core/automation.h"
#include "nextion.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
template<typename... Ts> class NextionSetBrightnessAction : public Action<Ts...> {
public:
@@ -91,5 +90,4 @@ template<typename... Ts> class NextionPublishBoolAction : public Action<Ts...> {
NextionComponent *component_;
};
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
@@ -2,8 +2,7 @@
#include "esphome/core/util.h"
#include "esphome/core/log.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
static const char *const TAG = "nextion_binarysensor";
@@ -64,5 +63,4 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti
ESP_LOGN(TAG, "Write: %s=%s", this->variable_name_.c_str(), ONOFF(this->state));
}
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
@@ -4,8 +4,8 @@
#include "../nextion_component.h"
#include "../nextion_base.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
class NextionBinarySensor;
class NextionBinarySensor : public NextionComponent,
@@ -38,5 +38,4 @@ class NextionBinarySensor : public NextionComponent,
protected:
uint8_t page_id_;
};
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
+34 -25
View File
@@ -5,8 +5,7 @@
#include "esphome/core/log.h"
#include "esphome/core/util.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
static const char *const TAG = "nextion";
@@ -151,10 +150,12 @@ void Nextion::reset_(bool reset_nextion) {
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
}
this->nextion_queue_.clear();
#ifdef USE_NEXTION_WAVEFORM
for (auto *entry : this->waveform_queue_) {
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
}
this->waveform_queue_.clear();
#endif // USE_NEXTION_WAVEFORM
}
void Nextion::dump_config() {
@@ -497,20 +498,21 @@ void Nextion::process_nextion_commands_() {
ESP_LOGW(TAG, "Invalid baud rate");
break;
case 0x12: // invalid Waveform ID or Channel # was used
#ifdef USE_NEXTION_WAVEFORM
if (this->waveform_queue_.empty()) {
ESP_LOGW(TAG, "Waveform ID/ch used but no sensor queued");
} else {
auto &nb = this->waveform_queue_.front();
NextionComponentBase *component = nb->component;
ESP_LOGW(TAG, "Invalid waveform ID %d/ch %d", component->get_component_id(),
component->get_wave_channel_id());
ESP_LOGN(TAG, "Remove waveform ID %d/ch %d", component->get_component_id(), component->get_wave_channel_id());
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->waveform_queue_.pop_front();
this->waveform_queue_.pop();
}
#else // USE_NEXTION_WAVEFORM
ESP_LOGW(TAG, "Waveform ID/ch error but waveform not enabled");
#endif // USE_NEXTION_WAVEFORM
break;
case 0x1A: // variable name invalid
ESP_LOGW(TAG, "Invalid variable name");
@@ -813,29 +815,30 @@ void Nextion::process_nextion_commands_() {
}
case 0xFD: { // data transparent transmit finished
ESP_LOGVV(TAG, "Data transmit done");
#ifdef USE_NEXTION_WAVEFORM
this->check_pending_waveform_();
#endif // USE_NEXTION_WAVEFORM
break;
}
case 0xFE: { // data transparent transmit ready
ESP_LOGVV(TAG, "Ready for transmit");
#ifdef USE_NEXTION_WAVEFORM
if (this->waveform_queue_.empty()) {
ESP_LOGE(TAG, "No waveforms queued");
break;
}
auto &nb = this->waveform_queue_.front();
auto *component = nb->component;
size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size()
: 255; // ADDT command can only send 255
size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() : 255;
this->write_array(component->get_wave_buffer().data(), static_cast<int>(buffer_to_send));
ESP_LOGN(TAG, "Send waveform: component id %d, waveform id %d, size %zu", component->get_component_id(),
component->get_wave_channel_id(), buffer_to_send);
component->clear_wave_buffer(buffer_to_send);
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->waveform_queue_.pop_front();
this->waveform_queue_.pop();
#else // USE_NEXTION_WAVEFORM
ESP_LOGW(TAG, "Waveform transmit ready but waveform not enabled");
#endif // USE_NEXTION_WAVEFORM
break;
}
default:
@@ -935,8 +938,13 @@ void Nextion::all_components_send_state_(bool force_update) {
binarysensortype->send_state_to_nextion();
}
for (auto *sensortype : this->sensortype_) {
if ((force_update || sensortype->get_needs_to_send_update()) && sensortype->get_wave_channel_id() == 0)
#ifdef USE_NEXTION_WAVEFORM
if ((force_update || sensortype->get_needs_to_send_update()) && sensortype->get_wave_channel_id() == UINT8_MAX) {
#else // USE_NEXTION_WAVEFORM
if (force_update || sensortype->get_needs_to_send_update()) {
#endif // USE_NEXTION_WAVEFORM
sensortype->send_state_to_nextion();
}
}
for (auto *switchtype : this->switchtype_) {
if (force_update || switchtype->get_needs_to_send_update())
@@ -1240,13 +1248,11 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
}
}
#ifdef USE_NEXTION_WAVEFORM
/**
* @brief Add addt command to the queue
* @brief Add addt command to the waveform queue.
*
* @param component_id The waveform component id
* @param wave_chan_id The waveform channel to send it to
* @param buffer_to_send The buffer size
* @param buffer_size The buffer data
* @param component Pointer to the Nextion component with waveform data to send.
*/
void Nextion::add_addt_command_to_queue(NextionComponentBase *component) {
if ((!this->is_setup() && !this->connection_state_.ignore_is_setup_) || this->is_sleeping())
@@ -1263,7 +1269,11 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) {
nextion_queue->component = component;
nextion_queue->queue_time = App.get_loop_component_start_time();
this->waveform_queue_.push_back(nextion_queue);
if (!this->waveform_queue_.push(nextion_queue)) {
ESP_LOGW(TAG, "Waveform queue full, drop");
delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory)
return;
}
if (this->waveform_queue_.size() == 1)
this->check_pending_waveform_();
}
@@ -1274,21 +1284,20 @@ void Nextion::check_pending_waveform_() {
auto *nb = this->waveform_queue_.front();
auto *component = nb->component;
size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size()
: 255; // ADDT command can only send 255
size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() : 255;
char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars
buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(),
component->get_wave_channel_id(), buffer_to_send);
if (!this->send_command_(command)) {
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->waveform_queue_.pop_front();
this->waveform_queue_.pop();
}
}
#endif // USE_NEXTION_WAVEFORM
void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; }
bool Nextion::is_updating() { return this->connection_state_.is_updating_; }
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
+17 -5
View File
@@ -9,6 +9,10 @@
#include "esphome/core/defines.h"
#include "esphome/core/time.h"
#ifdef USE_NEXTION_WAVEFORM
#include "esphome/core/helpers.h"
#endif // USE_NEXTION_WAVEFORM
#include "nextion_base.h"
#include "nextion_component.h"
@@ -21,8 +25,7 @@
#endif // USE_ESP32 vs USE_ESP8266
#endif // USE_NEXTION_TFT_UPLOAD
namespace esphome {
namespace nextion {
namespace esphome::nextion {
class Nextion;
class NextionComponentBase;
@@ -603,6 +606,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/
void disable_component_touch(const char *component);
#ifdef USE_NEXTION_WAVEFORM
/**
* Add waveform data to a waveform component
* @param component_id The integer component id.
@@ -612,6 +616,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
void add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value);
void open_waveform_channel(uint8_t component_id, uint8_t channel_number, uint8_t value);
#endif // USE_NEXTION_WAVEFORM
/**
* Display a picture at coordinates.
@@ -1206,7 +1211,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
void add_to_get_queue(NextionComponentBase *component) override;
#ifdef USE_NEXTION_WAVEFORM
void add_addt_command_to_queue(NextionComponentBase *component) override;
#endif // USE_NEXTION_WAVEFORM
void update_components_by_prefix(const std::string &prefix);
@@ -1392,7 +1399,11 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
#endif // USE_NEXTION_COMMAND_SPACING
std::list<NextionQueue *> nextion_queue_;
std::list<NextionQueue *> waveform_queue_;
#ifdef USE_NEXTION_WAVEFORM
/// Fixed-size ring buffer for waveform queue. Nextion supports at most 4 waveform
/// channels (IDs 0-3), so 4 entries is both the correct maximum and a safe default.
StaticRingBuffer<NextionQueue *, 4> waveform_queue_;
#endif // USE_NEXTION_WAVEFORM
uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag);
void all_components_send_state_(bool force_update = false);
uint32_t comok_sent_ = 0;
@@ -1461,7 +1472,9 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
const std::string &variable_name_to_send,
const std::string &state_value, bool is_sleep_safe = false);
#ifdef USE_NEXTION_WAVEFORM
void check_pending_waveform_();
#endif // USE_NEXTION_WAVEFORM
#ifdef USE_NEXTION_TFT_UPLOAD
#ifdef USE_ESP8266
@@ -1547,5 +1560,4 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
uint16_t max_q_age_ms_ = 8000; ///< Maximum age for queue items in ms
};
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
+5 -4
View File
@@ -2,8 +2,8 @@
#include "esphome/core/defines.h"
#include "esphome/core/color.h"
#include "nextion_component_base.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
#define NEXTION_PROTOCOL_LOG
@@ -33,7 +33,9 @@ class NextionBase {
const std::string &variable_name_to_send,
const std::string &state_value) = 0;
#ifdef USE_NEXTION_WAVEFORM
virtual void add_addt_command_to_queue(NextionComponentBase *component) = 0;
#endif // USE_NEXTION_WAVEFORM
virtual void add_to_get_queue(NextionComponentBase *component) = 0;
@@ -61,5 +63,4 @@ class NextionBase {
bool is_detected_ = false;
};
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
@@ -3,8 +3,8 @@
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace nextion {
namespace esphome::nextion {
static const char *const TAG = "nextion";
// Sleep safe commands
@@ -217,6 +217,7 @@ void Nextion::set_component_value(const char *component, int32_t value) {
this->add_no_result_to_queue_with_printf_(".val", "%s.val=%" PRId32, component, value);
}
#ifdef USE_NEXTION_WAVEFORM
void Nextion::add_waveform_data(uint8_t component_id, uint8_t channel_number, uint8_t value) {
this->add_no_result_to_queue_with_printf_("add", "add %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number,
value);
@@ -226,6 +227,7 @@ void Nextion::open_waveform_channel(uint8_t component_id, uint8_t channel_number
this->add_no_result_to_queue_with_printf_("addt", "addt %" PRIu8 ",%" PRIu8 ",%" PRIu8, component_id, channel_number,
value);
}
#endif // USE_NEXTION_WAVEFORM
void Nextion::set_component_coordinates(const char *component, uint16_t x, uint16_t y) {
this->add_no_result_to_queue_with_printf_(".xcen", "%s.xcen=%" PRIu16, component, x);
@@ -340,5 +342,4 @@ void Nextion::set_nextion_rtc_time(ESPTime time) {
this->add_no_result_to_queue_with_printf_("rtc5", "rtc5=%u", time.second);
}
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
@@ -1,7 +1,6 @@
#include "nextion_component.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
void NextionComponent::set_background_color(Color bco) {
if (this->variable_name_ == this->variable_name_to_send_) {
@@ -110,5 +109,4 @@ void NextionComponent::update_component_settings(bool force_update) {
this->component_flags_.font_id_needs_update = false;
}
}
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
@@ -3,8 +3,8 @@
#include "esphome/core/color.h"
#include "nextion_base.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
class NextionComponent;
class NextionComponent : public NextionComponentBase {
@@ -80,5 +80,4 @@ class NextionComponent : public NextionComponentBase {
uint16_t reserved : 3;
} component_flags_;
};
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
@@ -5,8 +5,7 @@
#include <vector>
#include "esphome/core/defines.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
enum NextionQueueType {
NO_RESULT = 0,
@@ -65,6 +64,7 @@ class NextionComponentBase {
uint8_t get_component_id() const { return this->component_id_; }
void set_component_id(uint8_t component_id) { this->component_id_ = component_id; }
#ifdef USE_NEXTION_WAVEFORM
uint8_t get_wave_channel_id() const { return this->wave_chan_id_; }
void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; }
@@ -77,6 +77,7 @@ class NextionComponentBase {
this->wave_buffer_.erase(this->wave_buffer_.begin(), this->wave_buffer_.begin() + buffer_sent);
}
}
#endif // USE_NEXTION_WAVEFORM
const std::string &get_variable_name() const { return this->variable_name_; }
const std::string &get_variable_name_to_send() const { return this->variable_name_to_send_; }
@@ -86,21 +87,24 @@ class NextionComponentBase {
virtual void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion){};
virtual void send_state_to_nextion(){};
bool get_needs_to_send_update() const { return this->needs_to_send_update_; }
#ifdef USE_NEXTION_WAVEFORM
// Remove before 2026.10.0
ESPDEPRECATED("Use get_wave_channel_id() instead. Will be removed in 2026.10.0", "2026.4.0")
uint8_t get_wave_chan_id() const { return this->get_wave_channel_id(); }
void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; }
#endif // USE_NEXTION_WAVEFORM
protected:
std::string variable_name_;
std::string variable_name_to_send_;
uint8_t component_id_ = 0;
#ifdef USE_NEXTION_WAVEFORM
uint8_t wave_chan_id_ = UINT8_MAX;
std::vector<uint8_t> wave_buffer_;
int wave_max_length_ = 255;
#endif // USE_NEXTION_WAVEFORM
bool needs_to_send_update_;
};
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
@@ -4,8 +4,8 @@
#include "esphome/core/application.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
static const char *const TAG = "nextion.upload";
bool Nextion::upload_end_(bool successful) {
@@ -33,7 +33,6 @@ bool Nextion::upload_end_(bool successful) {
return successful;
}
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
#endif // USE_NEXTION_TFT_UPLOAD
@@ -11,8 +11,8 @@
#include "esphome/core/log.h"
#include "esphome/core/util.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
static const char *const TAG = "nextion.upload.arduino";
static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
@@ -342,8 +342,7 @@ WiFiClient *Nextion::get_wifi_client_() {
}
#endif // USE_ESP8266
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
#endif // NOT USE_ESP32
#endif // USE_NEXTION_TFT_UPLOAD
@@ -14,8 +14,8 @@
#include "esphome/core/log.h"
#include "esphome/core/util.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
static const char *const TAG = "nextion.upload.esp32";
static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
@@ -344,8 +344,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
return this->upload_end_(true);
}
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
#endif // USE_ESP32
#endif // USE_NEXTION_TFT_UPLOAD
@@ -85,16 +85,16 @@ async def to_code(config):
cg.add(var.set_component_id(config[CONF_COMPONENT_ID]))
if CONF_WAVE_CHANNEL_ID in config:
cg.add_define("USE_NEXTION_WAVEFORM")
cg.add(var.set_wave_channel_id(config[CONF_WAVE_CHANNEL_ID]))
if CONF_WAVEFORM_SEND_LAST_VALUE in config:
cg.add(var.set_waveform_send_last_value(config[CONF_WAVEFORM_SEND_LAST_VALUE]))
if CONF_WAVE_MAX_VALUE in config:
cg.add(var.set_wave_max_value(config[CONF_WAVE_MAX_VALUE]))
if CONF_WAVE_MAX_LENGTH in config:
cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH]))
if CONF_WAVEFORM_SEND_LAST_VALUE in config:
cg.add(
var.set_waveform_send_last_value(config[CONF_WAVEFORM_SEND_LAST_VALUE])
)
if CONF_WAVE_MAX_VALUE in config:
cg.add(var.set_wave_max_value(config[CONF_WAVE_MAX_VALUE]))
if CONF_WAVE_MAX_LENGTH in config:
cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH]))
@automation.register_action(
@@ -2,8 +2,7 @@
#include "esphome/core/util.h"
#include "esphome/core/log.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
static const char *const TAG = "nextion_sensor";
@@ -11,37 +10,44 @@ void NextionSensor::process_sensor(const std::string &variable_name, int state)
if (!this->nextion_->is_setup())
return;
if (this->wave_chan_id_ == UINT8_MAX && this->variable_name_ == variable_name) {
#ifdef USE_NEXTION_WAVEFORM
if (this->wave_chan_id_ == UINT8_MAX && this->variable_name_ == variable_name)
#else // USE_NEXTION_WAVEFORM
if (this->variable_name_ == variable_name)
#endif // USE_NEXTION_WAVEFORM
{
this->publish_state(state);
ESP_LOGD(TAG, "Sensor: %s=%d", variable_name.c_str(), state);
}
}
#ifdef USE_NEXTION_WAVEFORM
void NextionSensor::add_to_wave_buffer(float state) {
this->needs_to_send_update_ = true;
int wave_state = (int) ((state / (float) this->wave_maxvalue_) * 100);
wave_buffer_.push_back(wave_state);
this->wave_buffer_.push_back(wave_state);
if (this->wave_buffer_.size() > (size_t) this->wave_max_length_) {
this->wave_buffer_.erase(this->wave_buffer_.begin());
}
}
#endif // USE_NEXTION_WAVEFORM
void NextionSensor::update() {
if (!this->nextion_->is_setup() || this->nextion_->is_updating())
return;
#ifdef USE_NEXTION_WAVEFORM
if (this->wave_chan_id_ == UINT8_MAX) {
this->nextion_->add_to_get_queue(this);
} else {
if (this->send_last_value_) {
this->add_to_wave_buffer(this->last_value_);
}
this->wave_update_();
}
#else // USE_NEXTION_WAVEFORM
this->nextion_->add_to_get_queue(this);
#endif // USE_NEXTION_WAVEFORM
}
void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) {
@@ -51,62 +57,60 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) {
if (std::isnan(state))
return;
if (this->wave_chan_id_ == UINT8_MAX) {
if (send_to_nextion) {
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
this->needs_to_send_update_ = true;
} else {
this->needs_to_send_update_ = false;
if (this->precision_ > 0) {
double to_multiply = pow(10, this->precision_);
int state_value = (int) (state * to_multiply);
this->nextion_->add_no_result_to_queue_with_set(this, (int) state_value);
} else {
this->nextion_->add_no_result_to_queue_with_set(this, (int) state);
}
}
}
} else {
#ifdef USE_NEXTION_WAVEFORM
if (this->wave_chan_id_ != UINT8_MAX) {
// Waveform sensor — buffer the value, don't send directly.
if (this->send_last_value_) {
this->last_value_ = state; // Update will handle setting the buffer
} else {
this->add_to_wave_buffer(state);
}
this->update_component_settings();
return;
}
#endif // USE_NEXTION_WAVEFORM
if (send_to_nextion) {
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
this->needs_to_send_update_ = true;
} else {
this->needs_to_send_update_ = false;
if (this->precision_ > 0) {
double to_multiply = pow(10, this->precision_);
int state_value = (int) (state * to_multiply);
this->nextion_->add_no_result_to_queue_with_set(this, (int) state_value);
} else {
this->nextion_->add_no_result_to_queue_with_set(this, (int) state);
}
}
}
float published_state = state;
if (this->wave_chan_id_ == UINT8_MAX) {
if (publish) {
if (this->precision_ > 0) {
double to_multiply = pow(10, -this->precision_);
published_state = (float) (state * to_multiply);
}
this->publish_state(published_state);
if (publish) {
if (this->precision_ > 0) {
double to_multiply = pow(10, -this->precision_);
published_state = (float) (state * to_multiply);
}
this->publish_state(published_state);
}
this->update_component_settings();
ESP_LOGN(TAG, "Write: %s=%lf", this->variable_name_.c_str(), published_state);
}
#ifdef USE_NEXTION_WAVEFORM
void NextionSensor::wave_update_() {
if (this->nextion_->is_sleeping() || this->wave_buffer_.empty()) {
return;
}
#ifdef NEXTION_PROTOCOL_LOG
size_t buffer_to_send =
this->wave_buffer_.size() < 255 ? this->wave_buffer_.size() : 255; // ADDT command can only send 255
ESP_LOGN(TAG, "Wave update: %zu/%zu vals to comp %d ch %d", buffer_to_send, this->wave_buffer_.size(),
this->component_id_, this->wave_chan_id_);
#endif
#endif // NEXTION_PROTOCOL_LOG
this->nextion_->add_addt_command_to_queue(this);
}
#endif // USE_NEXTION_WAVEFORM
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
@@ -4,8 +4,8 @@
#include "../nextion_component.h"
#include "../nextion_base.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
class NextionSensor;
class NextionSensor : public NextionComponent, public sensor::Sensor, public PollingComponent {
@@ -15,22 +15,30 @@ class NextionSensor : public NextionComponent, public sensor::Sensor, public Pol
void update_component() override { this->update(); }
void update() override;
void add_to_wave_buffer(float state);
void set_precision(uint8_t precision) { this->precision_ = precision; }
void set_component_id(uint8_t component_id) { this->component_id_ = component_id; }
void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; }
void set_wave_max_value(uint32_t wave_maxvalue) { this->wave_maxvalue_ = wave_maxvalue; }
void process_sensor(const std::string &variable_name, int state) override;
void set_state(float state) override { this->set_state(state, true, true); }
void set_state(float state, bool publish) override { this->set_state(state, publish, true); }
void set_state(float state, bool publish, bool send_to_nextion) override;
NextionQueueType get_queue_type() const override {
#ifdef USE_NEXTION_WAVEFORM
return this->wave_chan_id_ == UINT8_MAX ? NextionQueueType::SENSOR : NextionQueueType::WAVEFORM_SENSOR;
#else // USE_NEXTION_WAVEFORM
return NextionQueueType::SENSOR;
#endif // USE_NEXTION_WAVEFORM
}
#ifdef USE_NEXTION_WAVEFORM
void add_to_wave_buffer(float state);
void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; }
void set_wave_max_value(uint32_t wave_maxvalue) { this->wave_maxvalue_ = wave_maxvalue; }
void set_waveform_send_last_value(bool send_last_value) { this->send_last_value_ = send_last_value; }
void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; }
NextionQueueType get_queue_type() const override {
return this->wave_chan_id_ == UINT8_MAX ? NextionQueueType::SENSOR : NextionQueueType::WAVEFORM_SENSOR;
}
#endif // USE_NEXTION_WAVEFORM
void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {}
void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override {
this->set_state(state_value, publish, send_to_nextion);
@@ -38,11 +46,11 @@ class NextionSensor : public NextionComponent, public sensor::Sensor, public Pol
protected:
uint8_t precision_ = 0;
#ifdef USE_NEXTION_WAVEFORM
uint32_t wave_maxvalue_ = 255;
float last_value_ = 0;
bool send_last_value_ = true;
void wave_update_();
#endif // USE_NEXTION_WAVEFORM
};
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
@@ -2,8 +2,7 @@
#include "esphome/core/util.h"
#include "esphome/core/log.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
static const char *const TAG = "nextion_switch";
@@ -48,5 +47,4 @@ void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) {
void NextionSwitch::write_state(bool state) { this->set_state(state); }
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
@@ -4,8 +4,8 @@
#include "../nextion_component.h"
#include "../nextion_base.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
class NextionSwitch;
class NextionSwitch : public NextionComponent, public switch_::Switch, public PollingComponent {
@@ -30,5 +30,4 @@ class NextionSwitch : public NextionComponent, public switch_::Switch, public Po
protected:
void write_state(bool state) override;
};
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
@@ -2,8 +2,8 @@
#include "esphome/core/util.h"
#include "esphome/core/log.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
static const char *const TAG = "nextion_textsensor";
void NextionTextSensor::process_text(const std::string &variable_name, const std::string &text_value) {
@@ -45,5 +45,4 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s
ESP_LOGN(TAG, "Write: %s='%s'", this->variable_name_.c_str(), state.c_str());
}
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
@@ -4,8 +4,8 @@
#include "../nextion_component.h"
#include "../nextion_base.h"
namespace esphome {
namespace nextion {
namespace esphome::nextion {
class NextionTextSensor;
class NextionTextSensor : public NextionComponent, public text_sensor::TextSensor, public PollingComponent {
@@ -28,5 +28,4 @@ class NextionTextSensor : public NextionComponent, public text_sensor::TextSenso
this->set_state(state_value, publish, send_to_nextion);
}
};
} // namespace nextion
} // namespace esphome
} // namespace esphome::nextion
+2 -4
View File
@@ -4,8 +4,7 @@
#include "esphome/core/automation.h"
namespace esphome {
namespace ota {
namespace esphome::ota {
class OTAStateChangeTrigger final : public Trigger<OTAState>, public OTAStateListener {
public:
@@ -67,6 +66,5 @@ class OTAErrorTrigger final : public Trigger<uint8_t>, public OTAStateListener {
OTAComponent *parent_;
};
} // namespace ota
} // namespace esphome
} // namespace esphome::ota
#endif
+2 -4
View File
@@ -1,7 +1,6 @@
#include "ota_backend.h"
namespace esphome {
namespace ota {
namespace esphome::ota {
#ifdef USE_OTA_STATE_LISTENER
OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -34,5 +33,4 @@ void OTAComponent::notify_state_(OTAState state, float progress, uint8_t error)
}
#endif
} // namespace ota
} // namespace esphome
} // namespace esphome::ota
+2 -4
View File
@@ -8,8 +8,7 @@
#include <vector>
#endif
namespace esphome {
namespace ota {
namespace esphome::ota {
enum OTAResponseTypes {
OTA_RESPONSE_OK = 0x00,
@@ -117,5 +116,4 @@ OTAGlobalCallback *get_global_ota_callback();
// - notify_state_deferred_() when in separate task (e.g., web_server OTA)
// This ensures proper listener execution in all contexts.
#endif
} // namespace ota
} // namespace esphome
} // namespace esphome::ota
@@ -7,8 +7,7 @@
#include <Update.h>
namespace esphome {
namespace ota {
namespace esphome::ota {
static const char *const TAG = "ota.arduino_libretiny";
@@ -66,7 +65,5 @@ OTAResponseTypes ArduinoLibreTinyOTABackend::end() {
void ArduinoLibreTinyOTABackend::abort() { Update.abort(); }
} // namespace ota
} // namespace esphome
} // namespace esphome::ota
#endif // USE_LIBRETINY
@@ -4,8 +4,7 @@
#include "esphome/core/defines.h"
namespace esphome {
namespace ota {
namespace esphome::ota {
class ArduinoLibreTinyOTABackend final {
public:
@@ -22,7 +21,5 @@ class ArduinoLibreTinyOTABackend final {
std::unique_ptr<ArduinoLibreTinyOTABackend> make_ota_backend();
} // namespace ota
} // namespace esphome
} // namespace esphome::ota
#endif // USE_LIBRETINY
@@ -9,8 +9,7 @@
#include <Updater.h>
namespace esphome {
namespace ota {
namespace esphome::ota {
static const char *const TAG = "ota.arduino_rp2040";
@@ -75,8 +74,6 @@ void ArduinoRP2040OTABackend::abort() {
rp2040::preferences_prevent_write(false);
}
} // namespace ota
} // namespace esphome
} // namespace esphome::ota
#endif // USE_RP2040
#endif // USE_ARDUINO
@@ -6,8 +6,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/macros.h"
namespace esphome {
namespace ota {
namespace esphome::ota {
class ArduinoRP2040OTABackend final {
public:
@@ -24,8 +23,6 @@ class ArduinoRP2040OTABackend final {
std::unique_ptr<ArduinoRP2040OTABackend> make_ota_backend();
} // namespace ota
} // namespace esphome
} // namespace esphome::ota
#endif // USE_RP2040
#endif // USE_ARDUINO
@@ -8,8 +8,7 @@
#include <esp_task_wdt.h>
#include <spi_flash_mmap.h>
namespace esphome {
namespace ota {
namespace esphome::ota {
std::unique_ptr<IDFOTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); }
@@ -112,6 +111,5 @@ void IDFOTABackend::abort() {
this->update_handle_ = 0;
}
} // namespace ota
} // namespace esphome
} // namespace esphome::ota
#endif // USE_ESP32
+2 -4
View File
@@ -7,8 +7,7 @@
#include <esp_ota_ops.h>
namespace esphome {
namespace ota {
namespace esphome::ota {
class IDFOTABackend final {
public:
@@ -29,6 +28,5 @@ class IDFOTABackend final {
std::unique_ptr<IDFOTABackend> make_ota_backend();
} // namespace ota
} // namespace esphome
} // namespace esphome::ota
#endif // USE_ESP32
@@ -94,7 +94,7 @@ async def to_code(config):
SetOutputAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(CONF_ID),
cv.Required(CONF_ID): cv.use_id(PipsolarOutput),
cv.Required(CONF_VALUE): cv.templatable(cv.positive_float),
}
),
@@ -248,6 +248,9 @@ void RuntimeImage::release_buffer_() {
this->height_ = 0;
this->buffer_width_ = 0;
this->buffer_height_ = 0;
#ifdef USE_LVGL
memset(&this->dsc_, 0, sizeof(this->dsc_));
#endif
}
}
-2
View File
@@ -25,7 +25,6 @@ from esphome.const import (
CONF_TEMPERATURE_COMPENSATION,
CONF_TIME_CONSTANT,
CONF_VOC,
CONF_VOC_BASELINE,
DEVICE_CLASS_AQI,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM1,
@@ -165,7 +164,6 @@ CONFIG_SCHEMA = (
gain_factor=230,
),
cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean,
cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t,
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
+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],
@@ -108,7 +108,6 @@ async def to_code(config):
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE]))
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
cg.add(var.set_has_position(config[CONF_HAS_POSITION]))
@automation.register_action(
+1 -1
View File
@@ -503,7 +503,7 @@ def validate_thermostat(config):
# If restoring default preset on boot is true then ensure we have a default preset
if (
CONF_ON_BOOT_RESTORE_FROM in config
and config[CONF_ON_BOOT_RESTORE_FROM] is OnBootRestoreFrom.DEFAULT_PRESET
and config[CONF_ON_BOOT_RESTORE_FROM] == "DEFAULT_PRESET"
and CONF_DEFAULT_PRESET not in config
):
raise cv.Invalid(
+35 -28
View File
@@ -34,25 +34,43 @@ bool is_leap_year(int year) { return (year % 4 == 0 && year % 100 != 0) || (year
// Get days in year (avoids duplicate is_leap_year calls)
static inline int days_in_year(int year) { return is_leap_year(year) ? 366 : 365; }
// Convert days since epoch to year, updating days to remainder
static int __attribute__((noinline)) days_to_year(int64_t &days) {
int year = 1970;
int diy;
while (days >= (diy = days_in_year(year)) && year < 2200) {
days -= diy;
// Count leap years in [1, year] (i.e. up to and including year)
static constexpr int count_leap_years_up_to(int year) { return year / 4 - year / 100 + year / 400; }
constexpr int EPOCH_YEAR = 1970;
constexpr int LEAP_YEARS_BEFORE_EPOCH = count_leap_years_up_to(EPOCH_YEAR - 1);
constexpr int DAYS_PER_YEAR = 365;
constexpr int SECONDS_PER_DAY = 86400;
// Days from epoch (Jan 1 1970) to Jan 1 of given year — O(1)
static inline int64_t days_to_year_start(int year) {
return static_cast<int64_t>(DAYS_PER_YEAR) * (year - EPOCH_YEAR) +
(count_leap_years_up_to(year - 1) - LEAP_YEARS_BEFORE_EPOCH);
}
// Convert days since epoch to year, updating days to day-of-year remainder.
// The initial estimate from days/365 can overshoot by multiple years for
// far-future dates (e.g., year 5000+) due to accumulated leap days,
// so we use loops rather than single-step correction.
static int days_to_year(int64_t &days) {
int year = static_cast<int>(EPOCH_YEAR + days / DAYS_PER_YEAR);
int64_t year_start = days_to_year_start(year);
while (days < year_start) {
year--;
year_start = days_to_year_start(year);
}
while (days >= year_start + days_in_year(year)) {
year_start += days_in_year(year);
year++;
}
while (days < 0 && year > 1900) {
year--;
days += days_in_year(year);
}
days -= year_start;
return year;
}
// Extract just the year from a UTC epoch
// Extract just the year from a UTC epoch — O(1)
static int epoch_to_year(time_t epoch) {
int64_t days = epoch / 86400;
if (epoch < 0 && epoch % 86400 != 0)
int64_t days = epoch / SECONDS_PER_DAY;
if (epoch < 0 && epoch % SECONDS_PER_DAY != 0)
days--;
return days_to_year(days);
}
@@ -87,11 +105,11 @@ int __attribute__((noinline)) day_of_week(int year, int month, int day) {
void __attribute__((noinline)) epoch_to_tm_utc(time_t epoch, struct tm *out_tm) {
// Days since epoch
int64_t days = epoch / 86400;
int32_t remaining_secs = epoch % 86400;
int64_t days = epoch / SECONDS_PER_DAY;
int32_t remaining_secs = epoch % SECONDS_PER_DAY;
if (remaining_secs < 0) {
days--;
remaining_secs += 86400;
remaining_secs += SECONDS_PER_DAY;
}
out_tm->tm_sec = remaining_secs % 60;
@@ -280,17 +298,6 @@ static int __attribute__((noinline)) days_from_year_start(int year, int month, i
return days;
}
// Calculate days from epoch to Jan 1 of given year (for DST transition calculations)
// Only supports years >= 1970. Timezone is either compiled in from YAML or set by
// Home Assistant, so pre-1970 dates are not a concern.
static int64_t __attribute__((noinline)) days_to_year_start(int year) {
int64_t days = 0;
for (int y = 1970; y < year; y++) {
days += days_in_year(y);
}
return days;
}
time_t __attribute__((noinline)) calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds) {
int month, day;
@@ -339,7 +346,7 @@ time_t __attribute__((noinline)) calculate_dst_transition(int year, const DSTRul
int64_t days = days_to_year_start(year) + days_from_year_start(year, month, day);
// Convert to epoch and add transition time and base offset
return days * 86400 + rule.time_seconds + base_offset_seconds;
return days * SECONDS_PER_DAY + rule.time_seconds + base_offset_seconds;
}
} // namespace internal
@@ -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(); }
+44 -16
View File
@@ -255,26 +255,25 @@ void ZigbeeComponent::factory_reset() {
ZB_SCHEDULE_APP_CALLBACK(zb_bdb_reset_via_local_action, 0);
}
static void log_reporting_info(zb_zcl_reporting_info_t *rep_info) {
auto now = millis();
ESP_LOGD(TAG, "Reporting: endpoint %d, cluster_id 0x%04X, attr_id 0x%04X, flags 0x%02X, report in %ums", rep_info->ep,
rep_info->cluster_id, rep_info->attr_id, rep_info->flags,
ZB_ZCL_GET_REPORTING_FLAG(rep_info, ZB_ZCL_REPORT_TIMER_STARTED)
? ZB_TIME_BEACON_INTERVAL_TO_MSEC(rep_info->run_time) - now
: 0);
ESP_LOGD(TAG, " min_interval %ds, max_interval %ds, def_min_interval %ds, def_max_interval %ds",
rep_info->u.send_info.min_interval, rep_info->u.send_info.max_interval,
rep_info->u.send_info.def_min_interval, rep_info->u.send_info.def_max_interval);
}
void ZigbeeComponent::dump_reporting_() {
#ifdef ESPHOME_LOG_HAS_VERBOSE
auto now = millis();
bool first = true;
for (zb_uint8_t j = 0; j < ZCL_CTX().device_ctx->ep_count; j++) {
if (ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info) {
zb_zcl_reporting_info_t *rep_info = ZCL_CTX().device_ctx->ep_desc_list[j]->reporting_info;
for (zb_uint8_t i = 0; i < ZCL_CTX().device_ctx->ep_desc_list[j]->rep_info_count; i++) {
if (!first) {
ESP_LOGV(TAG, "");
}
first = false;
ESP_LOGV(TAG, "Endpoint: %d, cluster_id %d, attr_id %d, flags %d, report in %ums", rep_info->ep,
rep_info->cluster_id, rep_info->attr_id, rep_info->flags,
ZB_ZCL_GET_REPORTING_FLAG(rep_info, ZB_ZCL_REPORT_TIMER_STARTED)
? ZB_TIME_BEACON_INTERVAL_TO_MSEC(rep_info->run_time) - now
: 0);
ESP_LOGV(TAG, "Min_interval %ds, max_interval %ds, def_min_interval %ds, def_max_interval %ds",
rep_info->u.send_info.min_interval, rep_info->u.send_info.max_interval,
rep_info->u.send_info.def_min_interval, rep_info->u.send_info.def_max_interval);
log_reporting_info(rep_info);
rep_info++;
}
}
@@ -282,9 +281,38 @@ void ZigbeeComponent::dump_reporting_() {
#endif
}
void ZigbeeComponent::after_reporting_info(zb_zcl_configure_reporting_req_t *config_rep_req,
zb_zcl_attr_addr_info_t *attr_addr_info) {
#ifdef ESPHOME_LOG_HAS_DEBUG
auto *rep_info =
zb_zcl_find_reporting_info_manuf(attr_addr_info->src_ep, attr_addr_info->cluster_id, attr_addr_info->cluster_role,
config_rep_req->attr_id, attr_addr_info->manuf_code);
if (rep_info == nullptr) {
ESP_LOGE(TAG,
"Failed to resolve reporting info (src_ep=%u cluster_id=0x%04x role=%u attr_id=0x%04x manuf_code=0x%04x)",
attr_addr_info->src_ep, attr_addr_info->cluster_id, attr_addr_info->cluster_role, config_rep_req->attr_id,
attr_addr_info->manuf_code);
return;
}
log_reporting_info(rep_info);
#endif
}
} // namespace esphome::zigbee
extern "C" void zboss_signal_handler(zb_uint8_t param) {
esphome::zigbee::global_zigbee->zboss_signal_handler_esphome(param);
extern "C" {
void zboss_signal_handler(zb_uint8_t param) { esphome::zigbee::global_zigbee->zboss_signal_handler_esphome(param); }
// NOLINTBEGIN(readability-identifier-naming,bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
extern zb_ret_t __real_zb_zcl_put_reporting_info_from_req(zb_zcl_configure_reporting_req_t *config_rep_req,
zb_zcl_attr_addr_info_t *attr_addr_info);
zb_ret_t __wrap_zb_zcl_put_reporting_info_from_req(zb_zcl_configure_reporting_req_t *config_rep_req,
zb_zcl_attr_addr_info_t *attr_addr_info) {
zb_ret_t ret = __real_zb_zcl_put_reporting_info_from_req(config_rep_req, attr_addr_info);
esphome::zigbee::global_zigbee->after_reporting_info(config_rep_req, attr_addr_info);
return ret;
}
// NOLINTEND(readability-identifier-naming,bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
}
#endif
@@ -76,6 +76,7 @@ class ZigbeeComponent : public Component {
}
template<typename F> void add_join_callback(F &&cb) { this->join_cb_.add(std::forward<F>(cb)); }
void zboss_signal_handler_esphome(zb_bufid_t bufid);
void after_reporting_info(zb_zcl_configure_reporting_req_t *config_rep_req, zb_zcl_attr_addr_info_t *attr_addr_info);
void factory_reset();
Trigger<> *get_join_trigger() { return &this->join_trigger_; };
void force_report();
@@ -166,6 +166,8 @@ async def zephyr_to_code(config: ConfigType) -> None:
zephyr_add_prj_conf("NET_IP_ADDR_CHECK", False)
zephyr_add_prj_conf("NET_UDP", False)
cg.add_build_flag("-Wl,--wrap=zb_zcl_put_reporting_info_from_req")
if CONF_IEEE802154_VENDOR_OUI in config:
zephyr_add_prj_conf("IEEE802154_VENDOR_OUI_ENABLE", True)
random_number = config[CONF_IEEE802154_VENDOR_OUI]
+2
View File
@@ -244,6 +244,8 @@ RESERVED_IDS = [
"open",
"setup",
"loop",
"spi0",
"spi1",
"uart0",
"uart1",
"uart2",
+4 -6
View File
@@ -294,12 +294,10 @@ void Component::disable_loop() {
App.disable_component_loop_(this);
}
}
void Component::enable_loop() {
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) {
ESP_LOGVV(TAG, "%s loop enabled", LOG_STR_ARG(this->get_component_log_str()));
this->set_component_state_(COMPONENT_STATE_LOOP);
App.enable_component_loop_(this);
}
void Component::enable_loop_slow_path_() {
ESP_LOGVV(TAG, "%s loop enabled", LOG_STR_ARG(this->get_component_log_str()));
this->set_component_state_(COMPONENT_STATE_LOOP);
App.enable_component_loop_(this);
}
void IRAM_ATTR HOT Component::enable_loop_soon_any_context() {
// This method is thread and ISR-safe because:
+6 -1
View File
@@ -242,7 +242,10 @@ class Component {
* @note Components should call this->enable_loop() on themselves, not on other components.
* This ensures the component's state is properly updated along with the loop partition.
*/
void enable_loop();
void enable_loop() {
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE)
this->enable_loop_slow_path_();
}
/** Thread and ISR-safe version of enable_loop() that can be called from any context.
*
@@ -344,6 +347,8 @@ class Component {
virtual void call_setup();
void call_dump_config_();
void enable_loop_slow_path_();
/// Helper to set component state (clears state bits and sets new state)
inline void set_component_state_(uint8_t state) {
this->component_state_ &= ~COMPONENT_STATE_MASK;
+1
View File
@@ -123,6 +123,7 @@
#define USE_NEXTION_MAX_COMMANDS_PER_LOOP
#define USE_NEXTION_MAX_QUEUE_SIZE
#define USE_NEXTION_TFT_UPLOAD
#define USE_NEXTION_WAVEFORM
#define USE_NUMBER
#define USE_OUTPUT
#define USE_POWER_SUPPLY

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