mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 08:55:36 +00:00
Merge branch 'dev' into api/peel-first-write-iteration
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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_))
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_();
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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")
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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, []):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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("_"))
|
||||
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -244,6 +244,8 @@ RESERVED_IDS = [
|
||||
"open",
|
||||
"setup",
|
||||
"loop",
|
||||
"spi0",
|
||||
"spi1",
|
||||
"uart0",
|
||||
"uart1",
|
||||
"uart2",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user