Compare commits

..
Author SHA1 Message Date
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ab800dc09d Bump filelock from 3.32.4 to 3.32.5 (#18963)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-03 17:19:29 -04:00
J. Nick Kostonandpre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> f65ab5629e [esp8266] Drop Arduino framework versions before 3.0.0 (#18917) to
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-09-03 15:16:36 -04:00
esphome[bot]esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>Jonathan Swoboda
b84532d254 Bump bundled esphome-device-builder to 1.14.0 (#18960)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-09-03 12:15:06 +00:00
Keith Burzinski 6b11636491 [remote_transmitter] Fix BK7231N build by limiting the PWM path to BK7238 (#18958) 2026-09-03 08:12:36 -04:00
Jesse Hills 2bb98f2d64 Merge branch 'beta' into dev 2026-09-03 14:07:41 +12:00
Jesse Hills 25c29fd9e0 Merge pull request #18957 from esphome/bump-2026.9.0b1
2026.9.0b1
2026-09-03 14:07:19 +12:00
Jesse Hills f3c786c784 Bump version to 2026.10.0-dev 2026-09-03 13:07:21 +12:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2250430999 Bump zeroconf from 0.151.2 to 0.151.3 (#18951)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-02 20:56:51 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d1068d582f Bump ninja from 1.13.0 to 1.13.2 (#18952)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-02 20:56:42 -04:00
Jesse Hills 567a981078 Bump version to 2026.9.0b1 2026-09-03 12:16:25 +12:00
Jesse Hills 81ecb87253 Bump version to 2026.10.0-dev 2026-09-03 12:11:56 +12:00
f0e2eb96bd [snapshot][SDL] Display headless mode and snapshots (#17917)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-09-03 09:39:32 +12:00
Keith Burzinskiandpuddly 567f7f9196 [serial_proxy] Skip no-op reconfigure requests (#18953)
Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
2026-09-02 15:01:23 -05:00
Jesse Hills ecbde8ddf4 [uart] Migrate check_uart_settings to final validation (#18940) 2026-09-03 07:25:08 +12:00
147 changed files with 3364 additions and 1336 deletions
+13 -2
View File
@@ -374,8 +374,9 @@ jobs:
- name: Install apt packages (cached)
# ccache speeds up the host compiles. A cache hit never touches apt
# (mirror outages cannot hang the job); the timeout bounds the cold
# path. Packages and version must match seed-apt-cache exactly;
# libsdl2-dev is unused here and carried only for cache-key parity.
# path. Packages and version must match seed-apt-cache exactly.
# libsdl2-dev is needed by the headless display tests, which capture
# screenshots.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
@@ -438,6 +439,16 @@ jobs:
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \
--junitxml=junit-integration.xml "${test_files[@]}"
- name: Upload test artifacts
# Tests that compare rendered output write the image they actually got here, so a
# failure can be looked at without reproducing the whole build locally.
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: integration-test-artifacts-${{ matrix.bucket.name }}
path: test_artifacts/
if-no-files-found: ignore
retention-days: 7
- name: Upload junit timings
# Consumed by sync-integration-durations.yml through
# script/update_integration_test_durations.py; only full matrix dev
+2
View File
@@ -137,6 +137,8 @@ config/
!tests/component_tests/**/config/
tests/build/
tests/.esphome/
# Output kept by failing tests for inspection; uploaded by CI
test_artifacts/
/.temp-clang-tidy.cpp
/.temp/
.pio/
+1
View File
@@ -496,6 +496,7 @@ esphome/components/sm2335/* @Cossid
esphome/components/sml/* @alengwenus
esphome/components/smt100/* @piechade
esphome/components/sn74hc165/* @jesserockz
esphome/components/snapshot/* @clydebarrow
esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.9.0-dev
PROJECT_NUMBER = 2026.10.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.13.1
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.0
RUN \
platformio settings set enable_telemetry No \
+4 -9
View File
@@ -44,8 +44,7 @@ def get_arduino8266_tools_path() -> Path:
return tools_cache_path(*ARDUINO8266_TOOLS_CACHE)
# 3.1.1 rather than 3.1.0: the registry has no package for 3.1.0, and the
# encoder below cannot name 3.0.0/3.0.1 either (see its docstring)
# 3.1.1 rather than 3.1.0: the registry has no packages for 3.0.0, 3.0.1 or 3.1.0
MIN_FRAMEWORK_VERSION = Version(3, 1, 1)
@@ -53,20 +52,16 @@ def framework_package_version(ver: Version) -> str:
"""Map an Arduino core version to its registry package version (3.1.2 ->
3.30102.0; the leading 3 is the package major).
Exact registry names only for cores > 2.6.2 and >= 3.0.2; callers floor
at MIN_FRAMEWORK_VERSION.
Exact registry names for 3.x cores; callers floor at MIN_FRAMEWORK_VERSION.
"""
if ver.major > 3:
raise EsphomeError(
f"Arduino core {ver} is not supported yet; "
"the newest known core series is 3.x"
)
if ver <= Version(2, 6, 2):
# Cores <= 2.6.2 use the older 1.x/2.x package-major encodings (same
# boundary as _format_framework_arduino_version's era guard)
if ver.major < 3:
raise EsphomeError(
f"Arduino core {ver} uses an older package encoding than this "
"helper implements (newer than 2.6.2)"
f"Arduino core {ver} is not supported; ESPHome requires core 3.x"
)
return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
+2 -2
View File
@@ -368,8 +368,8 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
}
void Climate::save_state_(const ClimateTraits &traits) {
#if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
!defined(CLANG_TIDY)
#if (defined(USE_ESP32) || defined(USE_ESP8266)) && !defined(CLANG_TIDY)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wclass-memaccess"
#define TEMP_IGNORE_MEMACCESS
#endif
-1
View File
@@ -100,7 +100,6 @@ bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t comma
void CM1106Component::dump_config() {
ESP_LOGCONFIG(TAG, "CM1106:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
this->check_uart_settings(9600);
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
+8
View File
@@ -46,6 +46,14 @@ CONFIG_SCHEMA = (
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cm1106",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
"""Code generation entry point."""
-1
View File
@@ -58,7 +58,6 @@ void CSE7761Component::dump_config() {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
LOG_UPDATE_INTERVAL(this);
this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
}
void CSE7761Component::update() {
+7 -1
View File
@@ -68,7 +68,13 @@ CONFIG_SCHEMA = (
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cse7761", baud_rate=38400, require_rx=True, require_tx=True
"cse7761",
baud_rate=38400,
require_rx=True,
require_tx=True,
data_bits=8,
parity="EVEN",
stop_bits=1,
)
-1
View File
@@ -255,7 +255,6 @@ void CSE7766Component::dump_config() {
LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_);
LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN);
}
} // namespace esphome::cse7766
+6 -1
View File
@@ -84,7 +84,12 @@ CONFIG_SCHEMA = (
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cse7766", baud_rate=4800, parity="EVEN", require_rx=True
"cse7766",
baud_rate=4800,
require_rx=True,
data_bits=8,
parity="EVEN",
stop_bits=1,
)
+8
View File
@@ -26,6 +26,14 @@ CONFIG_SCHEMA = (
.extend(cv.polling_component_schema("30s"))
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"daly_bms",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
+1 -4
View File
@@ -22,10 +22,7 @@ static const uint8_t DALY_REQUEST_TEMPERATURE = 0x96;
void DalyBmsComponent::setup() { this->next_request_ = 1; }
void DalyBmsComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Daly BMS:");
this->check_uart_settings(9600);
}
void DalyBmsComponent::dump_config() { ESP_LOGCONFIG(TAG, "Daly BMS:"); }
void DalyBmsComponent::update() {
this->trigger_next_ = true;
+2 -2
View File
@@ -22,9 +22,9 @@ void DebugComponent::dump_config() {
LOG_SENSOR(" ", "Free space on heap", this->free_sensor_);
LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_);
LOG_SENSOR(" ", "CPU frequency", this->cpu_frequency_sensor_);
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
#ifdef USE_ESP8266
LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_);
#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
#endif // USE_ESP8266
#endif // USE_SENSOR
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
+2 -2
View File
@@ -35,7 +35,7 @@ class DebugComponent final : public PollingComponent {
#ifdef USE_SENSOR
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
#if defined(USE_ESP8266) || defined(USE_ESP32)
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
@@ -61,7 +61,7 @@ class DebugComponent final : public PollingComponent {
sensor::Sensor *free_sensor_{nullptr};
sensor::Sensor *block_sensor_{nullptr};
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
#if defined(USE_ESP8266) || defined(USE_ESP32)
sensor::Sensor *fragmentation_sensor_{nullptr};
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
@@ -159,12 +159,10 @@ void DebugComponent::update_platform_() {
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize());
}
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
if (this->fragmentation_sensor_ != nullptr) {
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation());
}
#endif
#endif
}
+2 -5
View File
@@ -52,12 +52,9 @@ CONFIG_SCHEMA = {
),
cv.Optional(CONF_FRAGMENTATION): cv.All(
cv.Any(
cv.All(
cv.only_on_esp8266,
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
),
cv.only_on_esp8266,
cv.only_on_esp32,
msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32",
msg="This feature is only available on ESP8266 and ESP32",
),
sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
+6 -1
View File
@@ -60,7 +60,12 @@ CONFIG_SCHEMA = cv.All(
).extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"dfplayer", baud_rate=9600, require_tx=True
"dfplayer",
baud_rate=9600,
require_tx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
)
+1 -4
View File
@@ -277,9 +277,6 @@ void DFPlayer::loop() {
}
}
}
void DFPlayer::dump_config() {
ESP_LOGCONFIG(TAG, "DFPlayer:");
this->check_uart_settings(9600);
}
void DFPlayer::dump_config() { ESP_LOGCONFIG(TAG, "DFPlayer:"); }
} // namespace esphome::dfplayer
+17 -40
View File
@@ -35,7 +35,7 @@ from esphome.platformio.toolchain import copy_ccache_script
from esphome.storage_json import StorageJSON
from esphome.types import ConfigType
from .boards import BOARDS, ESP8266_LD_SCRIPTS, board_ld_script
from .boards import BOARDS, board_ld_script
from .const import (
CONF_EARLY_PIN_INIT,
CONF_ENABLE_SERIAL,
@@ -43,8 +43,6 @@ from .const import (
CONF_RESTORE_FROM_FLASH,
KEY_BOARD,
KEY_ESP8266,
KEY_FLASH_SIZE,
KEY_LDSCRIPT,
KEY_PIN_INITIAL_STATES,
KEY_SERIAL1_REQUIRED,
KEY_SERIAL_REQUIRED,
@@ -133,10 +131,6 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
# format the given arduino (https://github.com/esp8266/Arduino/releases) version to
# a PIO platformio/framework-arduinoespressif8266 value
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
if ver <= cv.Version(2, 4, 1):
return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
if ver <= cv.Version(2, 6, 2):
return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
# Same encoding the native toolchain uses for its package download, so a
# version bump cannot drift between the two paths.
from esphome.arduino8266.framework import framework_package_version
@@ -159,11 +153,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
# - https://github.com/esp8266/Arduino/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 2)
# The platformio/espressif8266 version to use for arduino 2 framework versions
# The platformio/espressif8266 version to use for arduino 3 framework versions
# - https://github.com/platformio/platform-espressif8266/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266
ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 3)
# for arduino 3 framework versions
ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0)
# for arduino 4 framework versions
ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1)
@@ -188,6 +180,14 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
source = value.get(CONF_SOURCE, None)
if version < cv.Version(3, 0, 0):
raise cv.Invalid(
f"Arduino framework {version} is no longer supported; ESPHome requires "
f"C++20, which needs Arduino core 3.x. Use the recommended version "
f"({RECOMMENDED_ARDUINO_FRAMEWORK_VERSION}).",
path=[CONF_VERSION],
)
value[CONF_VERSION] = str(version)
value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
@@ -195,12 +195,8 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
if platform_version is None:
if version >= cv.Version(3, 1, 0):
platform_version = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION))
elif version >= cv.Version(3, 0, 0):
platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION))
elif version >= cv.Version(2, 5, 0):
platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION))
else:
platform_version = _parse_platform_version(str(cv.Version(1, 8, 0)))
platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION))
value[CONF_PLATFORM_VERSION] = platform_version
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
@@ -289,29 +285,11 @@ def check_rosetta() -> None:
)
def _choose_ld_script(board: str, ver: cv.Version) -> str | None:
"""The flash ld to pin for this board and core, or None for cores
without ld-script support."""
board_data = BOARDS[board]
ld_scripts = ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]]
if ver <= cv.Version(2, 3, 0):
# No ld script support
return None
if ver <= cv.Version(2, 4, 2):
# Old ld script path; the modern per-board override names do not
# exist in this core's SDK, so the override cannot be honored.
# Substituting the size default would move _FS_end and the
# preferences sector, wiping flash-backed state on flash.
if KEY_LDSCRIPT in board_data:
raise EsphomeError(
f"Board {board} requires its {board_data[KEY_LDSCRIPT]} "
f"flash layout, which Arduino core {ver} cannot honor; "
"use a core newer than 2.4.2"
)
return ld_scripts[0]
def _choose_ld_script(board: str) -> str:
"""The flash ld to pin for this board."""
# A per-board override preserves a layout the board shipped with
# (see d1_wroom_02 in boards.py)
return board_ld_script(board_data)
return board_ld_script(BOARDS[board])
@coroutine_with_priority(CoroPriority.PLATFORM)
@@ -435,10 +413,9 @@ async def to_code(config: ConfigType) -> None:
)
if config[CONF_BOARD] in BOARDS:
ld_script = _choose_ld_script(config[CONF_BOARD], ver)
if ld_script is not None:
cg.add_platformio_option("board_build.ldscript", ld_script)
cg.add_platformio_option(
"board_build.ldscript", _choose_ld_script(config[CONF_BOARD])
)
CORE.add_job(add_pin_initial_states_array)
CORE.add_job(finalize_waveform_config)
-1
View File
@@ -96,7 +96,6 @@ void HC8Component::dump_config() {
" Warmup time: %" PRIu32 " s",
this->warmup_seconds_);
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
this->check_uart_settings(9600);
}
} // namespace esphome::hc8
+3
View File
@@ -47,6 +47,9 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
baud_rate=9600,
require_rx=True,
require_tx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
)
-1
View File
@@ -38,7 +38,6 @@ CoverTraits HE60rCover::get_traits() {
void HE60rCover::dump_config() {
LOG_COVER("", "HE60R Cover", this);
this->check_uart_settings(1200, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
ESP_LOGCONFIG(TAG,
" Open Duration: %.1fs\n"
" Close Duration: %.1fs",
@@ -68,8 +68,6 @@ void HrxlMaxsonarWrComponent::check_buffer_() {
void HrxlMaxsonarWrComponent::dump_config() {
ESP_LOGCONFIG(TAG, "HRXL MaxSonar WR Sensor:");
LOG_SENSOR(" ", "Distance", this);
// As specified in the sensor's data sheet
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
}
} // namespace esphome::hrxl_maxsonar_wr
@@ -23,6 +23,14 @@ CONFIG_SCHEMA = sensor.sensor_schema(
state_class=STATE_CLASS_MEASUREMENT,
).extend(uart.UART_DEVICE_SCHEMA)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"hrxl_maxsonar_wr",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
@@ -11,7 +11,6 @@ static const char *const PROTOCOL_NAMES[] = {HYDREON_RGXX_PROTOCOL_LIST(, HYDREO
static const char *const IGNORE_STRINGS[] = {HYDREON_RGXX_IGNORE_LIST(, HYDREON_RGXX_COMMA)};
void HydreonRGxxComponent::dump_config() {
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
ESP_LOGCONFIG(TAG, "hydreon_rgxx:");
if (this->is_failed()) {
ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!");
@@ -130,6 +130,14 @@ CONFIG_SCHEMA = cv.All(
_validate,
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"hydreon_rgxx",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
@@ -26,8 +26,6 @@ void KamstrupKMPComponent::dump_config() {
LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]);
ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]);
}
this->check_uart_settings(1200, 2, uart::UART_CONFIG_PARITY_NONE, 8);
}
void KamstrupKMPComponent::update() {
+7 -1
View File
@@ -102,7 +102,13 @@ CONFIG_SCHEMA = (
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"kamstrup_kmp", baud_rate=1200, require_rx=True, require_tx=True
"kamstrup_kmp",
baud_rate=1200,
require_rx=True,
require_tx=True,
data_bits=8,
parity="NONE",
stop_bits=2,
)
-2
View File
@@ -143,8 +143,6 @@ void MHZ19Component::dump_config() {
ESP_LOGCONFIG(TAG, "MH-Z19:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
this->check_uart_settings(9600);
if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) {
ESP_LOGCONFIG(TAG, " Automatic baseline calibration enabled on boot");
} else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) {
+8
View File
@@ -80,6 +80,14 @@ CONFIG_SCHEMA = (
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"mhz19",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
@@ -163,10 +163,7 @@ void Mk2PVRouter::publish_value_(const char *tag, const char *val) {
#endif
}
void Mk2PVRouter::dump_config() {
ESP_LOGCONFIG(TAG, "Mk2PVRouter:");
this->check_uart_settings(BAUD_RATE, 1, uart::UART_CONFIG_PARITY_EVEN, 7);
}
void Mk2PVRouter::dump_config() { ESP_LOGCONFIG(TAG, "Mk2PVRouter:"); }
#ifdef MK2PVROUTER_LISTENER_COUNT
void Mk2PVRouter::register_mk2pvrouter_listener(Mk2PVRouterListener *listener) {
@@ -43,7 +43,6 @@ class Mk2PVRouter final : public Component, public uart::UARTDevice {
protected:
static constexpr size_t CRC_SUFFIX_LEN = 1;
static constexpr uint32_t BAUD_RATE = 9600;
enum class State : uint8_t {
WAITING_FOR_START,
@@ -209,14 +209,8 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
http_client.setTimeout(this->tft_upload_http_timeout_);
bool begin_status = false;
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0)
http_client.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
#elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
http_client.setFollowRedirects(true);
#endif
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
http_client.setRedirectLimit(3);
#endif
begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str());
if (!begin_status) {
this->connection_state_.is_updating_ = false;
-1
View File
@@ -16,7 +16,6 @@ void PM1006Component::dump_config() {
ESP_LOGCONFIG(TAG, "PM1006:");
LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_);
LOG_UPDATE_INTERVAL(this);
this->check_uart_settings(9600);
}
void PM1006Component::update() {
+3
View File
@@ -48,6 +48,9 @@ def validate_interval_uart(config: ConfigType) -> None:
baud_rate=9600,
require_rx=True,
require_tx=interval.total_milliseconds != SCHEDULER_DONT_RUN,
data_bits=8,
parity="NONE",
stop_bits=1,
)(config)
-2
View File
@@ -46,8 +46,6 @@ void PMSX003Component::dump_config() {
} else {
ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles");
}
this->check_uart_settings(9600);
}
void PMSX003Component::loop() {
+7 -1
View File
@@ -302,7 +302,13 @@ CONFIG_SCHEMA = cv.All(
def final_validate(config: ConfigType) -> None:
require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s")
schema = uart.final_validate_device_schema(
"pmsx003", baud_rate=9600, require_rx=True, require_tx=require_tx
"pmsx003",
baud_rate=9600,
require_rx=True,
require_tx=require_tx,
data_bits=8,
parity="NONE",
stop_bits=1,
)
schema(config)
+8
View File
@@ -41,6 +41,14 @@ CONFIG_SCHEMA = cv.All(
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"pylontech",
baud_rate=115200,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
@@ -33,7 +33,6 @@ static const uint8_t ASCII_LF = 0x0A;
PylontechComponent::PylontechComponent() {}
void PylontechComponent::dump_config() {
this->check_uart_settings(115200, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
ESP_LOGCONFIG(TAG, "pylontech:");
if (this->is_failed()) {
ESP_LOGE(TAG, "Connection with pylontech failed!");
@@ -4,11 +4,7 @@ from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import esp32, esp32_rmt, remote_base
from esphome.components.libretiny import get_libretiny_family
from esphome.components.libretiny.const import (
FAMILY_BK7231N,
FAMILY_BK7238,
FAMILY_RTL8720C,
)
from esphome.components.libretiny.const import FAMILY_BK7238, FAMILY_RTL8720C
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
@@ -49,7 +45,9 @@ DigitalWriteAction = remote_transmitter_ns.class_(
)
_NON_BLOCKING_LIBRETINY_FAMILIES = (FAMILY_RTL8720C, FAMILY_BK7231N, FAMILY_BK7238)
# Keep in sync with the USE_LIBRETINY_VARIANT_RTL8720C / REMOTE_TRANSMITTER_BK_PWM gates in
# remote_transmitter.h, which decide where set_non_blocking() is declared
_NON_BLOCKING_LIBRETINY_FAMILIES = (FAMILY_RTL8720C, FAMILY_BK7238)
def _validate_non_blocking_platform(value: bool) -> bool:
@@ -59,9 +57,7 @@ def _validate_non_blocking_platform(value: bool) -> bool:
return cv.boolean(value)
if CORE.is_libretiny and get_libretiny_family() in _NON_BLOCKING_LIBRETINY_FAMILIES:
return cv.boolean(value)
raise cv.Invalid(
"non_blocking is only supported on ESP32, RTL8720C, BK7231N and BK7238"
)
raise cv.Invalid("non_blocking is only supported on ESP32, RTL8720C and BK7238")
MULTI_CONF = True
@@ -12,10 +12,11 @@
#endif // SOC_RMT_SUPPORTED
#endif // USE_ESP32
// The BK7231N-style PWM block (hardware shadow-load duty updates) enables the ISR-driven
// transmitter on these families; family-level proxy for the SDK's CFG_SOC_NAME gate.
// See remote_transmitter_bk72xx.cpp.
#if defined(USE_LIBRETINY_VARIANT_BK7231N) || defined(USE_LIBRETINY_VARIANT_BK7238)
// Enables the ISR-driven transmitter on Beken. Gated on BK7238 alone: the shadow-load PWM
// block is shared with BK7231N, but LibreTiny builds that family against an older BDK whose
// PWM driver has no pwm_init_param()/pwm_start(). See remote_transmitter_bk72xx.cpp.
// Keep in sync with _NON_BLOCKING_LIBRETINY_FAMILIES in __init__.py.
#ifdef USE_LIBRETINY_VARIANT_BK7238
#define REMOTE_TRANSMITTER_BK_PWM
#endif
@@ -9,10 +9,13 @@
// with the core's fixes for type-name collisions between the two
#include <ArduinoPrivate.h>
// Only the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit)
// supports glitch-free per-edge duty updates; older SoCs compile the generic bit-bang
// implementation (remote_transmitter.cpp) instead, and this file compiles to nothing.
// REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h.
// Needs the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit)
// for glitch-free per-edge duty updates, and an SDK exposing pwm_init_param()/pwm_start().
// BK7231N has the block but LibreTiny builds it against an older BDK offering only the
// sddev_control API (CMD_PWM_INIT_PARAM), so it stays on the generic bit-bang path until
// someone can add and validate that path on real hardware. Every other Beken SoC lacks the
// block. REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h; when it is
// unset this file compiles to nothing and remote_transmitter.cpp is used instead.
namespace esphome::remote_transmitter {
@@ -3,11 +3,11 @@
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
// Envelope chain shared by the LibreTiny families that pace transmission from a hardware
// timer interrupt: RTL8720C (gtimer) and the BK7231N-style PWM block (BKTIMER1). Everything
// platform-specific sits behind five hooks implemented in the per-family files -- carrier
// setup, duty writes, one-shot arming and timer stop. Families without a usable timer keep
// the generic bit-bang implementation and compile none of this.
// Envelope chain shared by the LibreTiny families that pace transmission from a hardware timer
// interrupt: RTL8720C (gtimer) and BK7238 (BKTIMER1). Everything platform-specific sits behind
// five hooks implemented in the per-family files -- carrier setup, duty writes, one-shot arming
// and timer stop. Families without a usable timer keep the generic bit-bang implementation and
// compile none of this.
#if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(REMOTE_TRANSMITTER_BK_PWM)
namespace esphome::remote_transmitter {
+253
View File
@@ -1 +1,254 @@
import esphome.codegen as cg
CODEOWNERS = ["@clydebarrow"]
SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode")
SDL_KEYS = (
"SDLK_UNKNOWN",
"SDLK_RETURN",
"SDLK_ESCAPE",
"SDLK_BACKSPACE",
"SDLK_TAB",
"SDLK_SPACE",
"SDLK_EXCLAIM",
"SDLK_QUOTEDBL",
"SDLK_HASH",
"SDLK_PERCENT",
"SDLK_DOLLAR",
"SDLK_AMPERSAND",
"SDLK_QUOTE",
"SDLK_LEFTPAREN",
"SDLK_RIGHTPAREN",
"SDLK_ASTERISK",
"SDLK_PLUS",
"SDLK_COMMA",
"SDLK_MINUS",
"SDLK_PERIOD",
"SDLK_SLASH",
"SDLK_0",
"SDLK_1",
"SDLK_2",
"SDLK_3",
"SDLK_4",
"SDLK_5",
"SDLK_6",
"SDLK_7",
"SDLK_8",
"SDLK_9",
"SDLK_COLON",
"SDLK_SEMICOLON",
"SDLK_LESS",
"SDLK_EQUALS",
"SDLK_GREATER",
"SDLK_QUESTION",
"SDLK_AT",
"SDLK_LEFTBRACKET",
"SDLK_BACKSLASH",
"SDLK_RIGHTBRACKET",
"SDLK_CARET",
"SDLK_UNDERSCORE",
"SDLK_BACKQUOTE",
"SDLK_a",
"SDLK_b",
"SDLK_c",
"SDLK_d",
"SDLK_e",
"SDLK_f",
"SDLK_g",
"SDLK_h",
"SDLK_i",
"SDLK_j",
"SDLK_k",
"SDLK_l",
"SDLK_m",
"SDLK_n",
"SDLK_o",
"SDLK_p",
"SDLK_q",
"SDLK_r",
"SDLK_s",
"SDLK_t",
"SDLK_u",
"SDLK_v",
"SDLK_w",
"SDLK_x",
"SDLK_y",
"SDLK_z",
"SDLK_CAPSLOCK",
"SDLK_F1",
"SDLK_F2",
"SDLK_F3",
"SDLK_F4",
"SDLK_F5",
"SDLK_F6",
"SDLK_F7",
"SDLK_F8",
"SDLK_F9",
"SDLK_F10",
"SDLK_F11",
"SDLK_F12",
"SDLK_PRINTSCREEN",
"SDLK_SCROLLLOCK",
"SDLK_PAUSE",
"SDLK_INSERT",
"SDLK_HOME",
"SDLK_PAGEUP",
"SDLK_DELETE",
"SDLK_END",
"SDLK_PAGEDOWN",
"SDLK_RIGHT",
"SDLK_LEFT",
"SDLK_DOWN",
"SDLK_UP",
"SDLK_NUMLOCKCLEAR",
"SDLK_KP_DIVIDE",
"SDLK_KP_MULTIPLY",
"SDLK_KP_MINUS",
"SDLK_KP_PLUS",
"SDLK_KP_ENTER",
"SDLK_KP_1",
"SDLK_KP_2",
"SDLK_KP_3",
"SDLK_KP_4",
"SDLK_KP_5",
"SDLK_KP_6",
"SDLK_KP_7",
"SDLK_KP_8",
"SDLK_KP_9",
"SDLK_KP_0",
"SDLK_KP_PERIOD",
"SDLK_APPLICATION",
"SDLK_POWER",
"SDLK_KP_EQUALS",
"SDLK_F13",
"SDLK_F14",
"SDLK_F15",
"SDLK_F16",
"SDLK_F17",
"SDLK_F18",
"SDLK_F19",
"SDLK_F20",
"SDLK_F21",
"SDLK_F22",
"SDLK_F23",
"SDLK_F24",
"SDLK_EXECUTE",
"SDLK_HELP",
"SDLK_MENU",
"SDLK_SELECT",
"SDLK_STOP",
"SDLK_AGAIN",
"SDLK_UNDO",
"SDLK_CUT",
"SDLK_COPY",
"SDLK_PASTE",
"SDLK_FIND",
"SDLK_MUTE",
"SDLK_VOLUMEUP",
"SDLK_VOLUMEDOWN",
"SDLK_KP_COMMA",
"SDLK_KP_EQUALSAS400",
"SDLK_ALTERASE",
"SDLK_SYSREQ",
"SDLK_CANCEL",
"SDLK_CLEAR",
"SDLK_PRIOR",
"SDLK_RETURN2",
"SDLK_SEPARATOR",
"SDLK_OUT",
"SDLK_OPER",
"SDLK_CLEARAGAIN",
"SDLK_CRSEL",
"SDLK_EXSEL",
"SDLK_KP_00",
"SDLK_KP_000",
"SDLK_THOUSANDSSEPARATOR",
"SDLK_DECIMALSEPARATOR",
"SDLK_CURRENCYUNIT",
"SDLK_CURRENCYSUBUNIT",
"SDLK_KP_LEFTPAREN",
"SDLK_KP_RIGHTPAREN",
"SDLK_KP_LEFTBRACE",
"SDLK_KP_RIGHTBRACE",
"SDLK_KP_TAB",
"SDLK_KP_BACKSPACE",
"SDLK_KP_A",
"SDLK_KP_B",
"SDLK_KP_C",
"SDLK_KP_D",
"SDLK_KP_E",
"SDLK_KP_F",
"SDLK_KP_XOR",
"SDLK_KP_POWER",
"SDLK_KP_PERCENT",
"SDLK_KP_LESS",
"SDLK_KP_GREATER",
"SDLK_KP_AMPERSAND",
"SDLK_KP_DBLAMPERSAND",
"SDLK_KP_VERTICALBAR",
"SDLK_KP_DBLVERTICALBAR",
"SDLK_KP_COLON",
"SDLK_KP_HASH",
"SDLK_KP_SPACE",
"SDLK_KP_AT",
"SDLK_KP_EXCLAM",
"SDLK_KP_MEMSTORE",
"SDLK_KP_MEMRECALL",
"SDLK_KP_MEMCLEAR",
"SDLK_KP_MEMADD",
"SDLK_KP_MEMSUBTRACT",
"SDLK_KP_MEMMULTIPLY",
"SDLK_KP_MEMDIVIDE",
"SDLK_KP_PLUSMINUS",
"SDLK_KP_CLEAR",
"SDLK_KP_CLEARENTRY",
"SDLK_KP_BINARY",
"SDLK_KP_OCTAL",
"SDLK_KP_DECIMAL",
"SDLK_KP_HEXADECIMAL",
"SDLK_LCTRL",
"SDLK_LSHIFT",
"SDLK_LALT",
"SDLK_LGUI",
"SDLK_RCTRL",
"SDLK_RSHIFT",
"SDLK_RALT",
"SDLK_RGUI",
"SDLK_MODE",
"SDLK_AUDIONEXT",
"SDLK_AUDIOPREV",
"SDLK_AUDIOSTOP",
"SDLK_AUDIOPLAY",
"SDLK_AUDIOMUTE",
"SDLK_MEDIASELECT",
"SDLK_WWW",
"SDLK_MAIL",
"SDLK_CALCULATOR",
"SDLK_COMPUTER",
"SDLK_AC_SEARCH",
"SDLK_AC_HOME",
"SDLK_AC_BACK",
"SDLK_AC_FORWARD",
"SDLK_AC_STOP",
"SDLK_AC_REFRESH",
"SDLK_AC_BOOKMARKS",
"SDLK_BRIGHTNESSDOWN",
"SDLK_BRIGHTNESSUP",
"SDLK_DISPLAYSWITCH",
"SDLK_KBDILLUMTOGGLE",
"SDLK_KBDILLUMDOWN",
"SDLK_KBDILLUMUP",
"SDLK_EJECT",
"SDLK_SLEEP",
"SDLK_APP1",
"SDLK_APP2",
"SDLK_AUDIOREWIND",
"SDLK_AUDIOFASTFORWARD",
"SDLK_SOFTLEFT",
"SDLK_SOFTRIGHT",
"SDLK_CALL",
"SDLK_ENDCALL",
)
SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS}
+3 -250
View File
@@ -7,262 +7,15 @@ from esphome.core import Lambda
from esphome.cpp_generator import ExpressionStatement, RawExpression
from esphome.types import ConfigType
from .display import CONF_SDL_ID, Sdl
from . import SDL_KEYMAP
from .display import CONF_SDL_ID, Sdl, headless_final_validate
CODEOWNERS = ["@bdm310"]
STATE_ARG = "state"
SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode")
FINAL_VALIDATE_SCHEMA = headless_final_validate("binary_sensor")
SDL_KEYS = (
"SDLK_UNKNOWN",
"SDLK_RETURN",
"SDLK_ESCAPE",
"SDLK_BACKSPACE",
"SDLK_TAB",
"SDLK_SPACE",
"SDLK_EXCLAIM",
"SDLK_QUOTEDBL",
"SDLK_HASH",
"SDLK_PERCENT",
"SDLK_DOLLAR",
"SDLK_AMPERSAND",
"SDLK_QUOTE",
"SDLK_LEFTPAREN",
"SDLK_RIGHTPAREN",
"SDLK_ASTERISK",
"SDLK_PLUS",
"SDLK_COMMA",
"SDLK_MINUS",
"SDLK_PERIOD",
"SDLK_SLASH",
"SDLK_0",
"SDLK_1",
"SDLK_2",
"SDLK_3",
"SDLK_4",
"SDLK_5",
"SDLK_6",
"SDLK_7",
"SDLK_8",
"SDLK_9",
"SDLK_COLON",
"SDLK_SEMICOLON",
"SDLK_LESS",
"SDLK_EQUALS",
"SDLK_GREATER",
"SDLK_QUESTION",
"SDLK_AT",
"SDLK_LEFTBRACKET",
"SDLK_BACKSLASH",
"SDLK_RIGHTBRACKET",
"SDLK_CARET",
"SDLK_UNDERSCORE",
"SDLK_BACKQUOTE",
"SDLK_a",
"SDLK_b",
"SDLK_c",
"SDLK_d",
"SDLK_e",
"SDLK_f",
"SDLK_g",
"SDLK_h",
"SDLK_i",
"SDLK_j",
"SDLK_k",
"SDLK_l",
"SDLK_m",
"SDLK_n",
"SDLK_o",
"SDLK_p",
"SDLK_q",
"SDLK_r",
"SDLK_s",
"SDLK_t",
"SDLK_u",
"SDLK_v",
"SDLK_w",
"SDLK_x",
"SDLK_y",
"SDLK_z",
"SDLK_CAPSLOCK",
"SDLK_F1",
"SDLK_F2",
"SDLK_F3",
"SDLK_F4",
"SDLK_F5",
"SDLK_F6",
"SDLK_F7",
"SDLK_F8",
"SDLK_F9",
"SDLK_F10",
"SDLK_F11",
"SDLK_F12",
"SDLK_PRINTSCREEN",
"SDLK_SCROLLLOCK",
"SDLK_PAUSE",
"SDLK_INSERT",
"SDLK_HOME",
"SDLK_PAGEUP",
"SDLK_DELETE",
"SDLK_END",
"SDLK_PAGEDOWN",
"SDLK_RIGHT",
"SDLK_LEFT",
"SDLK_DOWN",
"SDLK_UP",
"SDLK_NUMLOCKCLEAR",
"SDLK_KP_DIVIDE",
"SDLK_KP_MULTIPLY",
"SDLK_KP_MINUS",
"SDLK_KP_PLUS",
"SDLK_KP_ENTER",
"SDLK_KP_1",
"SDLK_KP_2",
"SDLK_KP_3",
"SDLK_KP_4",
"SDLK_KP_5",
"SDLK_KP_6",
"SDLK_KP_7",
"SDLK_KP_8",
"SDLK_KP_9",
"SDLK_KP_0",
"SDLK_KP_PERIOD",
"SDLK_APPLICATION",
"SDLK_POWER",
"SDLK_KP_EQUALS",
"SDLK_F13",
"SDLK_F14",
"SDLK_F15",
"SDLK_F16",
"SDLK_F17",
"SDLK_F18",
"SDLK_F19",
"SDLK_F20",
"SDLK_F21",
"SDLK_F22",
"SDLK_F23",
"SDLK_F24",
"SDLK_EXECUTE",
"SDLK_HELP",
"SDLK_MENU",
"SDLK_SELECT",
"SDLK_STOP",
"SDLK_AGAIN",
"SDLK_UNDO",
"SDLK_CUT",
"SDLK_COPY",
"SDLK_PASTE",
"SDLK_FIND",
"SDLK_MUTE",
"SDLK_VOLUMEUP",
"SDLK_VOLUMEDOWN",
"SDLK_KP_COMMA",
"SDLK_KP_EQUALSAS400",
"SDLK_ALTERASE",
"SDLK_SYSREQ",
"SDLK_CANCEL",
"SDLK_CLEAR",
"SDLK_PRIOR",
"SDLK_RETURN2",
"SDLK_SEPARATOR",
"SDLK_OUT",
"SDLK_OPER",
"SDLK_CLEARAGAIN",
"SDLK_CRSEL",
"SDLK_EXSEL",
"SDLK_KP_00",
"SDLK_KP_000",
"SDLK_THOUSANDSSEPARATOR",
"SDLK_DECIMALSEPARATOR",
"SDLK_CURRENCYUNIT",
"SDLK_CURRENCYSUBUNIT",
"SDLK_KP_LEFTPAREN",
"SDLK_KP_RIGHTPAREN",
"SDLK_KP_LEFTBRACE",
"SDLK_KP_RIGHTBRACE",
"SDLK_KP_TAB",
"SDLK_KP_BACKSPACE",
"SDLK_KP_A",
"SDLK_KP_B",
"SDLK_KP_C",
"SDLK_KP_D",
"SDLK_KP_E",
"SDLK_KP_F",
"SDLK_KP_XOR",
"SDLK_KP_POWER",
"SDLK_KP_PERCENT",
"SDLK_KP_LESS",
"SDLK_KP_GREATER",
"SDLK_KP_AMPERSAND",
"SDLK_KP_DBLAMPERSAND",
"SDLK_KP_VERTICALBAR",
"SDLK_KP_DBLVERTICALBAR",
"SDLK_KP_COLON",
"SDLK_KP_HASH",
"SDLK_KP_SPACE",
"SDLK_KP_AT",
"SDLK_KP_EXCLAM",
"SDLK_KP_MEMSTORE",
"SDLK_KP_MEMRECALL",
"SDLK_KP_MEMCLEAR",
"SDLK_KP_MEMADD",
"SDLK_KP_MEMSUBTRACT",
"SDLK_KP_MEMMULTIPLY",
"SDLK_KP_MEMDIVIDE",
"SDLK_KP_PLUSMINUS",
"SDLK_KP_CLEAR",
"SDLK_KP_CLEARENTRY",
"SDLK_KP_BINARY",
"SDLK_KP_OCTAL",
"SDLK_KP_DECIMAL",
"SDLK_KP_HEXADECIMAL",
"SDLK_LCTRL",
"SDLK_LSHIFT",
"SDLK_LALT",
"SDLK_LGUI",
"SDLK_RCTRL",
"SDLK_RSHIFT",
"SDLK_RALT",
"SDLK_RGUI",
"SDLK_MODE",
"SDLK_AUDIONEXT",
"SDLK_AUDIOPREV",
"SDLK_AUDIOSTOP",
"SDLK_AUDIOPLAY",
"SDLK_AUDIOMUTE",
"SDLK_MEDIASELECT",
"SDLK_WWW",
"SDLK_MAIL",
"SDLK_CALCULATOR",
"SDLK_COMPUTER",
"SDLK_AC_SEARCH",
"SDLK_AC_HOME",
"SDLK_AC_BACK",
"SDLK_AC_FORWARD",
"SDLK_AC_STOP",
"SDLK_AC_REFRESH",
"SDLK_AC_BOOKMARKS",
"SDLK_BRIGHTNESSDOWN",
"SDLK_BRIGHTNESSUP",
"SDLK_DISPLAYSWITCH",
"SDLK_KBDILLUMTOGGLE",
"SDLK_KBDILLUMDOWN",
"SDLK_KBDILLUMUP",
"SDLK_EJECT",
"SDLK_SLEEP",
"SDLK_APP1",
"SDLK_APP2",
"SDLK_AUDIOREWIND",
"SDLK_AUDIOFASTFORWARD",
"SDLK_SOFTLEFT",
"SDLK_SOFTRIGHT",
"SDLK_CALL",
"SDLK_ENDCALL",
)
SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS}
CONFIG_SCHEMA = (
binary_sensor.binary_sensor_schema(BinarySensor)
+52 -1
View File
@@ -4,6 +4,7 @@ from typing import Any
import esphome.codegen as cg
from esphome.components import display
from esphome.components.snapshot import Snapshot, register_snapshot
import esphome.config_validation as cv
from esphome.const import (
CONF_DIMENSIONS,
@@ -16,14 +17,21 @@ from esphome.const import (
CONF_Y,
PLATFORM_HOST,
)
import esphome.final_validate as fv
from esphome.types import ConfigType
from . import SDL_KEYMAP
AUTO_LOAD = ["snapshot"]
sdl_ns = cg.esphome_ns.namespace("sdl")
Sdl = sdl_ns.class_("Sdl", display.Display, cg.Component)
Sdl = sdl_ns.class_("Sdl", display.Display, cg.Component, Snapshot)
sdl_window_flags = cg.global_ns.enum("SDL_WindowFlags")
CONF_CENTERED_ON_DISPLAY = "centered_on_display"
CONF_HEADLESS = "headless"
CONF_SNAPSHOT_KEY = "snapshot_key"
CONF_SDL_OPTIONS = "sdl_options"
CONF_SDL_ID = "sdl_id"
CONF_WINDOW_OPTIONS = "window_options"
@@ -67,12 +75,29 @@ def _validate_position(config: dict) -> dict:
raise cv.Invalid("Must specify either 'x' and 'y' or 'centered_on_display'")
def _validate_headless(config: ConfigType) -> ConfigType:
if not config[CONF_HEADLESS]:
return config
if CONF_WINDOW_OPTIONS in config:
raise cv.Invalid(
f"'{CONF_WINDOW_OPTIONS}' has no effect when '{CONF_HEADLESS}' is set - there is no window"
)
if CONF_SNAPSHOT_KEY in config:
raise cv.Invalid(
f"'{CONF_SNAPSHOT_KEY}' cannot be used when '{CONF_HEADLESS}' is set - "
f"there is no keyboard. Use the 'snapshot.take' action instead"
)
return config
CONFIG_SCHEMA = cv.All(
display.FULL_DISPLAY_SCHEMA.extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(Sdl),
cv.Optional(CONF_SDL_OPTIONS, default=""): get_sdl_options,
cv.Optional(CONF_HEADLESS, default=False): cv.boolean,
cv.Optional(CONF_SNAPSHOT_KEY): cv.enum(SDL_KEYMAP),
cv.Required(CONF_DIMENSIONS): cv.Any(
cv.dimensions,
cv.Schema(
@@ -99,16 +124,42 @@ CONFIG_SCHEMA = cv.All(
}
)
),
_validate_headless,
cv.only_on(PLATFORM_HOST),
)
def headless_final_validate(platform: str) -> cv.Schema:
"""Build a FINAL_VALIDATE_SCHEMA rejecting a platform whose sdl display is headless.
Mouse and keyboard platforms are driven by window events, so under a headless display they
would never report anything.
"""
def validate_display(display_config: ConfigType) -> ConfigType:
if display_config.get(CONF_HEADLESS):
raise cv.Invalid(
f"The sdl {platform} platform needs a window, but its display has "
f"'{CONF_HEADLESS}' set"
)
return display_config
return cv.Schema(
{cv.Required(CONF_SDL_ID): fv.id_declaration_match_schema(validate_display)},
extra=cv.ALLOW_EXTRA,
)
async def to_code(config: ConfigType) -> None:
for option in config[CONF_SDL_OPTIONS].split():
cg.add_build_flag(option)
cg.add_build_flag("-DSDL_BYTEORDER=4321")
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await register_snapshot(var, config)
cg.add(var.set_headless(config[CONF_HEADLESS]))
if (key := config.get(CONF_SNAPSHOT_KEY)) is not None:
cg.add(var.set_snapshot_key(key))
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
+228 -46
View File
@@ -2,8 +2,17 @@
#include "sdl_esphome.h"
#include "esphome/components/display/display_color_utils.h"
#include <cstdlib>
namespace esphome::sdl {
namespace {
// Key under which each window keeps a pointer back to its Sdl instance.
constexpr const char *const WINDOW_DATA_KEY = "esphome_sdl";
} // namespace
int Sdl::get_width() {
switch (this->rotation_) {
case display::DISPLAY_ROTATION_90_DEGREES:
@@ -28,17 +37,96 @@ int Sdl::get_height() {
}
}
void Sdl::setup() {
SDL_Init(SDL_INIT_VIDEO);
this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_,
this->window_options_);
this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE);
SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_);
void Sdl::destroy_renderer_() {
// Reverse order of creation: the renderer refers to the window or surface it was made from.
if (this->shot_target_ != nullptr) {
SDL_DestroyTexture(this->shot_target_);
this->shot_target_ = nullptr;
}
if (this->texture_ != nullptr) {
SDL_DestroyTexture(this->texture_);
this->texture_ = nullptr;
}
if (this->renderer_ != nullptr) {
SDL_DestroyRenderer(this->renderer_);
this->renderer_ = nullptr;
}
if (this->window_ != nullptr) {
SDL_DestroyWindow(this->window_);
this->window_ = nullptr;
}
if (this->surface_ != nullptr) {
SDL_FreeSurface(this->surface_);
this->surface_ = nullptr;
}
}
bool Sdl::setup_failed_(const char *what) {
ESP_LOGE(TAG, "%s: %s", what, SDL_GetError());
// Give back whatever was created before the failure. Without this a half set up display leaves an
// empty window on screen for the life of the process, still registered as an event target.
this->destroy_renderer_();
return false;
}
bool Sdl::setup_renderer_() {
SDL_SetMainReady();
if (this->headless_) {
// SDL_INIT_VIDEO is deliberately not requested: a software renderer bound to a surface needs no
// video device, so this works on a machine with no display server at all.
if (SDL_Init(0) != 0)
return this->setup_failed_("SDL_Init failed");
this->surface_ = SDL_CreateRGBSurfaceWithFormat(0, this->width_, this->height_, 16, SDL_PIXELFORMAT_RGB565);
if (this->surface_ == nullptr)
return this->setup_failed_("Could not create offscreen surface");
this->renderer_ = SDL_CreateSoftwareRenderer(this->surface_);
} else {
if (SDL_Init(SDL_INIT_VIDEO) != 0)
return this->setup_failed_("SDL_Init failed");
this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_,
this->window_options_);
if (this->window_ == nullptr)
return this->setup_failed_("Could not create window");
// Lets loop() find the display an event belongs to, so one display does not act on another's
// input when several windows are open.
SDL_SetWindowData(this->window_, WINDOW_DATA_KEY, this);
this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE);
}
if (this->renderer_ == nullptr)
return this->setup_failed_("Could not create renderer");
if (SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_) != 0)
return this->setup_failed_("Could not set renderer logical size");
this->texture_ =
SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STATIC, this->width_, this->height_);
SDL_SetTextureBlendMode(this->texture_, SDL_BLENDMODE_BLEND);
if (this->texture_ == nullptr)
return this->setup_failed_("Could not create texture");
// The texture has no alpha channel, so blending is pointless. Headless it would also force a
// different software blit path onto the 16 bit target surface.
if (SDL_SetTextureBlendMode(this->texture_, this->headless_ ? SDL_BLENDMODE_NONE : SDL_BLENDMODE_BLEND) != 0)
return this->setup_failed_("Could not set texture blend mode");
return true;
}
void Sdl::setup() {
if (!this->setup_renderer_()) {
this->mark_failed();
return;
}
if (this->headless_) {
// Nothing generates events, so there is nothing for loop() to do.
this->disable_loop();
} else if (this->snapshot_key_ != 0) {
this->add_key_listener(this->snapshot_key_, [this](bool down) {
if (down && !this->take_snapshot(nullptr)) {
ESP_LOGW(TAG, "snapshot key did not write a file");
}
});
}
}
void Sdl::update() {
if (this->texture_ == nullptr)
return;
this->do_update_();
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_))
return;
@@ -51,12 +139,19 @@ void Sdl::update() {
}
void Sdl::redraw_(SDL_Rect &rect) {
// Nothing to present when headless - a snapshot blits the whole texture when it needs it, so
// doing it here as well would just burn CPU. draw_pixels_at() calls this on every partial
// update, so it is worth skipping.
if (this->headless_)
return;
SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect);
SDL_RenderPresent(this->renderer_);
}
void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
if (this->texture_ == nullptr)
return;
SDL_Rect rect{x_start, y_start, w, h};
if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || big_endian) {
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
@@ -69,7 +164,7 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
}
void Sdl::draw_pixel_at(int x, int y, Color color) {
if (!this->get_clipping().inside(x, y))
if (this->texture_ == nullptr || !this->get_clipping().inside(x, y))
return;
if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) {
@@ -104,61 +199,148 @@ void Sdl::process_key(uint32_t keycode, bool down) {
callback->second(down);
}
Sdl *Sdl::instance_for_window_(uint32_t window_id) {
SDL_Window *window = SDL_GetWindowFromID(window_id);
if (window == nullptr)
return nullptr;
return static_cast<Sdl *>(SDL_GetWindowData(window, WINDOW_DATA_KEY));
}
void Sdl::handle_event_(const SDL_Event &event) {
switch (event.type) {
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
if (event.button.button == 1) {
this->mouse_x = event.button.x;
this->mouse_y = event.button.y;
this->mouse_down = event.button.state != 0;
}
break;
case SDL_MOUSEMOTION:
if (event.motion.state & 1) {
this->mouse_x = event.motion.x;
this->mouse_y = event.motion.y;
this->mouse_down = true;
} else {
this->mouse_down = false;
}
break;
case SDL_KEYDOWN:
// Ignore auto-repeat, otherwise holding a key floods the listeners.
if (event.key.repeat != 0)
break;
ESP_LOGD(TAG, "keydown %d", event.key.keysym.sym);
this->process_key(event.key.keysym.sym, true);
break;
case SDL_KEYUP:
ESP_LOGD(TAG, "keyup %d", event.key.keysym.sym);
this->process_key(event.key.keysym.sym, false);
break;
case SDL_WINDOWEVENT:
switch (event.window.event) {
case SDL_WINDOWEVENT_SIZE_CHANGED:
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_RESIZED: {
SDL_Rect rect{0, 0, this->width_, this->height_};
this->redraw_(rect);
break;
}
default:
break;
}
break;
default:
break;
}
}
void Sdl::loop() {
SDL_Event e;
if (SDL_PollEvent(&e)) {
switch (e.type) {
case SDL_QUIT:
exit(0);
// Take everything that is waiting, not one event per loop. A touch drag produces a burst of
// motion events, and consuming them one at a time lets the queue grow without bound, so the
// pointer ends up acting on input from further and further in the past. Draining collapses a
// burst to the position it ended at, which is the one the user is asking for anyway.
while (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT)
exit(0);
// Events carry the window they happened in, so send each one to the display that owns it.
uint32_t window_id;
switch (e.type) {
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
if (e.button.button == 1) {
this->mouse_x = e.button.x;
this->mouse_y = e.button.y;
this->mouse_down = e.button.state != 0;
}
window_id = e.button.windowID;
break;
case SDL_MOUSEMOTION:
if (e.motion.state & 1) {
this->mouse_x = e.button.x;
this->mouse_y = e.button.y;
this->mouse_down = true;
} else {
this->mouse_down = false;
}
window_id = e.motion.windowID;
break;
case SDL_KEYDOWN:
ESP_LOGD(TAG, "keydown %d", e.key.keysym.sym);
this->process_key(e.key.keysym.sym, true);
break;
case SDL_KEYUP:
ESP_LOGD(TAG, "keyup %d", e.key.keysym.sym);
this->process_key(e.key.keysym.sym, false);
window_id = e.key.windowID;
break;
case SDL_WINDOWEVENT:
switch (e.window.event) {
case SDL_WINDOWEVENT_SIZE_CHANGED:
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_RESIZED: {
SDL_Rect rect{0, 0, this->width_, this->height_};
this->redraw_(rect);
break;
}
default:
break;
}
window_id = e.window.windowID;
break;
default:
// Anything else, including the touch events SDL reports alongside the mouse events it
// synthesises from them, is not used here.
ESP_LOGV(TAG, "Event %d", e.type);
break;
continue;
}
Sdl *target = instance_for_window_(window_id);
if (target == nullptr) {
// Nothing to route this to: the window has gone, or it is not one of ours. Say so, otherwise
// input that stops working leaves no trace at all.
ESP_LOGV(TAG, "Event %d for unknown window %u", e.type, window_id);
continue;
}
target->handle_event_(e);
}
}
bool Sdl::capture_bgr(uint8_t *dest, size_t row_stride) {
if (this->texture_ == nullptr || this->renderer_ == nullptr) {
ESP_LOGE(TAG, "Snapshot requested but SDL is not set up");
return false;
}
if (this->shot_target_ == nullptr) {
this->shot_target_ = SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_TARGET,
this->width_, this->height_);
if (this->shot_target_ == nullptr) {
ESP_LOGE(TAG, "Could not create capture texture: %s", SDL_GetError());
return false;
}
SDL_SetTextureBlendMode(this->shot_target_, SDL_BLENDMODE_NONE);
}
// Render into an offscreen target first. SDL_RenderReadPixels works in physical output pixels and
// ignores the logical size, so reading straight off a resizable window would read more pixels than
// there is room for.
// Every step is checked: a failed clear or copy would otherwise be read back as a blank or stale
// picture, written out, and reported as a snapshot that worked.
bool ok = false;
if (SDL_SetRenderTarget(this->renderer_, this->shot_target_) == 0) {
ok = SDL_SetRenderDrawColor(this->renderer_, 0, 0, 0, SDL_ALPHA_OPAQUE) == 0 &&
SDL_RenderClear(this->renderer_) == 0 &&
SDL_RenderCopy(this->renderer_, this->texture_, nullptr, nullptr) == 0 &&
SDL_RenderReadPixels(this->renderer_, nullptr, SDL_PIXELFORMAT_BGR24, dest, static_cast<int>(row_stride)) == 0;
if (SDL_SetRenderTarget(this->renderer_, nullptr) != 0) {
// Stuck rendering into shot_target_ from here on, so there's no point continuing.
ESP_LOGE(TAG, "Could not restore the render target: %s", SDL_GetError());
this->mark_failed();
return false;
}
}
if (!ok) {
ESP_LOGE(TAG, "Could not capture the screen: %s", SDL_GetError());
}
return ok;
}
} // namespace esphome::sdl
+30 -5
View File
@@ -1,10 +1,12 @@
#pragma once
#ifdef USE_HOST
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/components/display/display.h"
#include "esphome/components/snapshot/snapshot.h"
#define SDL_MAIN_HANDLED
#include "SDL.h"
#include <map>
@@ -13,7 +15,7 @@ namespace esphome::sdl {
constexpr static const char *const TAG = "sdl";
class Sdl final : public display::Display {
class Sdl final : public display::Display, public snapshot::Snapshot {
public:
display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; }
void update() override;
@@ -32,6 +34,9 @@ class Sdl final : public display::Display {
this->pos_x_ = pos_x;
this->pos_y_ = pos_y;
}
void set_headless(bool headless) { this->headless_ = headless; }
void set_snapshot_key(int32_t keycode) { this->snapshot_key_ = keycode; }
int get_width() override;
int get_height() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
@@ -51,20 +56,40 @@ class Sdl final : public display::Display {
int get_width_internal() override { return this->width_; }
int get_height_internal() override { return this->height_; }
void redraw_(SDL_Rect &rect);
bool setup_renderer_();
/// Release the window, surface, renderer and textures, and forget them.
void destroy_renderer_();
/// Log an SDL failure during setup, release anything already created, and return false.
bool setup_failed_(const char *what);
int snapshot_width() override { return this->width_; }
int snapshot_height() override { return this->height_; }
bool capture_bgr(uint8_t *dest, size_t row_stride) override;
void handle_event_(const SDL_Event &event);
/// The display owning the given window, or nullptr if it is not one of ours.
static Sdl *instance_for_window_(uint32_t window_id);
SDL_Renderer *renderer_{};
SDL_Window *window_{};
SDL_Texture *texture_{};
// Offscreen render target used when headless. SDL_CreateSoftwareRenderer only borrows the
// surface, and the renderer goes back to using it as its output whenever the capture target is
// released, so it has to stay alive as long as the renderer does.
SDL_Surface *surface_{};
// Capture target, created on first snapshot.
SDL_Texture *shot_target_{};
std::map<int32_t, CallbackManager<void(bool)>> key_callbacks_{};
int width_{};
int height_{};
uint32_t window_options_{0};
int32_t pos_x_{SDL_WINDOWPOS_UNDEFINED};
int32_t pos_y_{SDL_WINDOWPOS_UNDEFINED};
SDL_Renderer *renderer_{};
SDL_Window *window_{};
SDL_Texture *texture_{};
int32_t snapshot_key_{0};
uint16_t x_low_{0};
uint16_t y_low_{0};
uint16_t x_high_{0};
uint16_t y_high_{0};
std::map<int32_t, CallbackManager<void(bool)>> key_callbacks_{};
bool headless_{false};
};
} // namespace esphome::sdl
#endif
@@ -4,10 +4,12 @@ import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
from ..display import CONF_SDL_ID, Sdl, sdl_ns
from ..display import CONF_SDL_ID, Sdl, headless_final_validate, sdl_ns
SdlTouchscreen = sdl_ns.class_("SdlTouchscreen", touchscreen.Touchscreen)
FINAL_VALIDATE_SCHEMA = headless_final_validate("touchscreen")
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
{
@@ -31,6 +31,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
require_tx=True,
require_rx=True,
baud_rate=115200,
data_bits=8,
parity="NONE",
stop_bits=1,
)
@@ -33,8 +33,6 @@ void MR60FDA2Component::dump_config() {
// Initialisation functions
void MR60FDA2Component::setup() {
this->check_uart_settings(115200);
this->current_frame_locate_ = LOCATE_FRAME_HEADER;
this->current_frame_id_ = 0;
this->current_frame_len_ = 0;
@@ -130,17 +130,26 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
}
// Apply validated parameters
uart_comp->set_baud_rate(baudrate);
uart_comp->set_stop_bits(stop_bits);
uart_comp->set_data_bits(data_size);
// Map parity value to UARTParityOptions
// Skip a no-op reconfigure. Clients routinely re-send identical settings on every
// port open, and on a USB UART each apply is a CDC SET_LINE_CODING control transfer.
// Some bridges watch line-coding changes as a signalling channel (a magic baud
// sequence to enter a bootloader, say), so redundant applies are not harmless.
static const uart::UARTParityOptions PARITY_MAP[] = {
uart::UART_CONFIG_PARITY_NONE,
uart::UART_CONFIG_PARITY_EVEN,
uart::UART_CONFIG_PARITY_ODD,
};
if (uart_comp->get_baud_rate() == baudrate && uart_comp->get_stop_bits() == stop_bits &&
uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity]) {
ESP_LOGV(TAG, "Settings unchanged, skipping reconfigure [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
// Apply validated parameters
uart_comp->set_baud_rate(baudrate);
uart_comp->set_stop_bits(stop_bits);
uart_comp->set_data_bits(data_size);
uart_comp->set_parity(PARITY_MAP[parity]);
// load_settings() is available on ESP8266 and ESP32 platforms
+7 -1
View File
@@ -68,7 +68,13 @@ CONFIG_SCHEMA = (
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"smt100", baud_rate=9600, require_rx=True, require_tx=True
"smt100",
baud_rate=9600,
require_rx=True,
require_tx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
)
-1
View File
@@ -65,7 +65,6 @@ void SMT100Component::dump_config() {
LOG_SENSOR(TAG, "Temperature", this->temperature_sensor_);
LOG_SENSOR(TAG, "Moisture", this->moisture_sensor_);
LOG_UPDATE_INTERVAL(this);
this->check_uart_settings(9600);
}
int SMT100Component::readline_(int readch, char *buffer, int len) {
+76
View File
@@ -0,0 +1,76 @@
"""Shared support for writing what a display is showing out to an image file.
The component itself has no configuration. It provides the ``snapshot.take`` action and the C++
base class behind it, so any display that can hand over its pixels - the in memory display in this
component, or an SDL window - saves files the same way, under the same directory, with the same
rules about names.
"""
from dataclasses import dataclass
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType, TemplateArgsType
CODEOWNERS = ["@clydebarrow"]
DOMAIN = "snapshot"
CONF_FILENAME = "filename"
snapshot_ns = cg.esphome_ns.namespace("snapshot")
Snapshot = snapshot_ns.class_("Snapshot")
SnapshotAction = snapshot_ns.class_("SnapshotAction", automation.Action)
@automation.register_action(
"snapshot.take",
SnapshotAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Snapshot),
cv.Optional(CONF_FILENAME): cv.templatable(cv.string),
}
),
synchronous=True,
)
async def snapshot_take_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
if (filename := config.get(CONF_FILENAME)) is not None:
cg.add(var.set_filename(await cg.templatable(filename, args, cg.std_string)))
return var
@dataclass
class SnapshotData:
directory_defined: bool = False
def _get_data() -> SnapshotData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = SnapshotData()
return CORE.data[DOMAIN]
async def register_snapshot(var: MockObj, config: ConfigType) -> None:
"""Set up a component so that the snapshot action can write its picture to a file."""
data = _get_data()
# Only once, however many displays there are: two defines that say the same thing do not
# compare equal, so asking for this per display repeats the line in defines.h.
if not data.directory_defined:
data.directory_defined = True
cg.add_define(
"ESPHOME_SNAPSHOT_DIR",
(CORE.data_dir / "snapshots" / CORE.name).as_posix(),
)
cg.add(var.set_snapshot_prefix(str(config[CONF_ID])))
@@ -0,0 +1,61 @@
import esphome.codegen as cg
from esphome.components import display
import esphome.config_validation as cv
from esphome.const import (
CONF_DIMENSIONS,
CONF_HEIGHT,
CONF_ID,
CONF_LAMBDA,
CONF_WIDTH,
PLATFORM_HOST,
)
from esphome.types import ConfigType
from .. import Snapshot, register_snapshot, snapshot_ns
# The base class and the file writing live in the parent component, which nothing else in a
# configuration using only this platform would pull in.
AUTO_LOAD = ["snapshot"]
SnapshotDisplay = snapshot_ns.class_(
"SnapshotDisplay", display.DisplayBuffer, cg.Component, Snapshot
)
CONFIG_SCHEMA = cv.All(
display.FULL_DISPLAY_SCHEMA.extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SnapshotDisplay),
cv.Required(CONF_DIMENSIONS): cv.Any(
cv.dimensions,
cv.Schema(
{
cv.Required(CONF_WIDTH): cv.positive_not_null_int,
cv.Required(CONF_HEIGHT): cv.positive_not_null_int,
}
),
),
}
)
),
cv.only_on(PLATFORM_HOST),
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await register_snapshot(var, config)
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT]))
else:
(width, height) = dimensions
cg.add(var.set_dimensions(width, height))
if lamb := config.get(CONF_LAMBDA):
lambda_ = await cg.process_lambda(
lamb, [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
@@ -0,0 +1,80 @@
#ifdef USE_HOST
#include "snapshot_display.h"
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/log.h"
#include <cstring>
namespace esphome::snapshot {
static const char *const TAG = "snapshot.display";
namespace {
/// Spread a channel that only goes up to `max` over the whole 0 to 255 range, so that the
/// brightest value stays the brightest. This is the same arithmetic SDL uses, which is what makes
/// a picture taken here come out identical to the same picture taken from an SDL window.
constexpr uint8_t expand_channel(uint16_t value, uint16_t max) { return static_cast<uint8_t>(value * 255 / max); }
constexpr uint16_t RED_MAX = 0x1F;
constexpr uint16_t GREEN_MAX = 0x3F;
constexpr uint16_t BLUE_MAX = 0x1F;
} // namespace
void SnapshotDisplay::setup() {
this->init_internal_(static_cast<uint32_t>(this->width_) * this->height_ * 2);
if (this->buffer_ == nullptr) {
this->mark_failed(LOG_STR("Could not allocate display buffer"));
}
}
void SnapshotDisplay::dump_config() { LOG_DISPLAY("", "Snapshot", this); }
void SnapshotDisplay::draw_absolute_pixel_internal(int x, int y, Color color) {
if (this->buffer_ == nullptr || x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return;
this->pixels_()[y * this->width_ + x] = display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB);
}
void SnapshotDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr,
display::ColorOrder order, display::ColorBitness bitness, bool big_endian,
int x_offset, int y_offset, int x_pad) {
if (this->buffer_ == nullptr)
return;
// Anything that is not already laid out the way the buffer is, or that would reach outside it,
// goes through the base class, which turns it into one call per pixel with the bounds checked.
const bool copyable = this->rotation_ == display::DISPLAY_ROTATION_0_DEGREES &&
bitness == display::COLOR_BITNESS_565 && !big_endian && x_start >= 0 && y_start >= 0 &&
x_start + w <= this->width_ && y_start + h <= this->height_;
if (!copyable) {
DisplayBuffer::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
return;
}
const size_t stride = static_cast<size_t>(x_offset) + w + x_pad;
const uint8_t *src = ptr + (stride * y_offset + x_offset) * 2;
for (int y = 0; y != h; y++) {
memcpy(&this->pixels_()[(y_start + y) * this->width_ + x_start], src + y * stride * 2, w * 2);
}
}
bool SnapshotDisplay::capture_bgr(uint8_t *dest, size_t row_stride) {
if (this->buffer_ == nullptr) {
ESP_LOGE(TAG, "Snapshot requested but there is no buffer to read");
return false;
}
const uint16_t *src = this->pixels_();
for (int y = 0; y != this->height_; y++) {
uint8_t *out = dest + y * row_stride;
for (int x = 0; x != this->width_; x++) {
const uint16_t pixel = *src++;
*out++ = expand_channel(pixel & BLUE_MAX, BLUE_MAX);
*out++ = expand_channel((pixel >> 5) & GREEN_MAX, GREEN_MAX);
*out++ = expand_channel(pixel >> 11, RED_MAX);
}
}
return true;
}
} // namespace esphome::snapshot
#endif
@@ -0,0 +1,48 @@
#pragma once
#ifdef USE_HOST
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/snapshot/snapshot.h"
#include "esphome/core/component.h"
namespace esphome::snapshot {
/// A display with nowhere to show anything: it keeps the picture in memory, where the snapshot
/// action can pick it up. That makes it a way to see what a configuration draws on a machine with
/// no screen, and to check the result in a test.
class SnapshotDisplay final : public display::DisplayBuffer, public Snapshot {
public:
void setup() override;
void update() override { this->do_update_(); }
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; }
void set_dimensions(uint16_t width, uint16_t height) {
this->width_ = width;
this->height_ = height;
}
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
int get_width_internal() override { return this->width_; }
int get_height_internal() override { return this->height_; }
int snapshot_width() override { return this->width_; }
int snapshot_height() override { return this->height_; }
bool capture_bgr(uint8_t *dest, size_t row_stride) override;
/// The picture, one 16 bit RGB565 value per pixel, topmost row first. Owned by DisplayBuffer as
/// a byte pointer; this is the same memory seen as what is actually stored in it.
uint16_t *pixels_() { return reinterpret_cast<uint16_t *>(this->buffer_); }
int width_{};
int height_{};
};
} // namespace esphome::snapshot
#endif
+248
View File
@@ -0,0 +1,248 @@
#ifdef USE_HOST
#include "snapshot.h"
#include "esphome/core/log.h"
#include <fcntl.h>
#include <strings.h>
#include <unistd.h>
#include <cctype>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <filesystem>
#include <memory>
namespace esphome::snapshot {
namespace {
constexpr const char *const TAG = "snapshot";
// Longest name we will build a path from. NAME_MAX is 255 and we may append a collision suffix.
constexpr size_t MAX_NAME_LENGTH = 200;
// Give up rather than spin forever if every candidate name is taken.
constexpr unsigned MAX_NAME_ATTEMPTS = 1000;
// A BMP file header followed by a BITMAPINFOHEADER, which is where the pixels start.
constexpr size_t BMP_HEADER_SIZE = 54;
constexpr size_t BMP_INFO_HEADER_SIZE = 40;
constexpr int BMP_BITS_PER_PIXEL = 24;
/// True if the name already ends in ".bmp". The comparison ignores case, so "shot.BMP" is left
/// alone rather than turned into "shot.BMP.bmp".
bool has_bmp_suffix(const std::string &name) {
return name.size() >= 4 && strcasecmp(name.c_str() + name.size() - 4, ".bmp") == 0;
}
/// Reduce a user supplied name to a single safe path component. Everything outside the allowed set
/// is replaced, so "..", "/" and absolute paths cannot escape the snapshot directory.
/// Returns an empty string if nothing usable is left.
std::string sanitise_filename(const char *const name, bool *name_changed) {
std::string result;
bool all_dots = true;
bool changed = false;
for (const char *p = name; *p != '\0'; p++) {
if (result.size() >= MAX_NAME_LENGTH) {
changed = true;
break;
}
char c = *p;
if (!(std::isalnum(static_cast<unsigned char>(c)) || c == '.' || c == '_' || c == '-')) {
c = '_';
changed = true;
}
if (c != '.')
all_dots = false;
result.push_back(c);
}
if (all_dots) {
*name_changed = true;
return "";
}
if (!has_bmp_suffix(result))
result += ".bmp";
*name_changed = changed;
return result;
}
/// Insert "-<attempt>" before the file extension, e.g. "shot.bmp" -> "shot-1.bmp".
std::string add_suffix(const std::string &name, unsigned attempt) {
char suffix[12];
snprintf(suffix, sizeof(suffix), "-%u", attempt);
auto dot = name.rfind('.');
if (dot == std::string::npos)
return name + suffix;
return name.substr(0, dot) + suffix + name.substr(dot);
}
/// Directory snapshots are written to. The environment variable lets a test redirect output
/// without rebuilding, matching how the host platform handles ESPHOME_PREFDIR.
const char *snapshot_dir() {
const char *dir = getenv("ESPHOME_SNAPSHOT_DIR"); // NOLINT(concurrency-mt-unsafe)
return dir != nullptr && dir[0] != '\0' ? dir : ESPHOME_SNAPSHOT_DIR;
}
/// Store a value in as many bytes, least significant first, and step the pointer past it.
/// BMP is a little endian format whatever the machine writing it uses.
void put_le(uint8_t *&dest, uint32_t value, size_t bytes) {
for (size_t i = 0; i != bytes; i++)
*dest++ = static_cast<uint8_t>(value >> (8 * i));
}
/// The number of bytes one row of `width` pixels takes up in the file. Rows are padded out to a
/// multiple of four bytes.
size_t bmp_row_size(int width) { return (static_cast<size_t>(width) * 3 + 3) & ~size_t{3}; }
/// Write pixels out as a 24 bit BMP. The rows given start with the topmost and are `row_stride`
/// bytes apart, which must leave room for a whole padded row; a BMP holds its rows the other way
/// up, so they go out last first.
bool write_bmp(FILE *file, const uint8_t *pixels, int width, int height, size_t row_stride) {
const size_t row_size = bmp_row_size(width);
const size_t pixel_bytes = row_size * height;
uint8_t header[BMP_HEADER_SIZE];
uint8_t *pos = header;
*pos++ = 'B';
*pos++ = 'M';
put_le(pos, static_cast<uint32_t>(BMP_HEADER_SIZE + pixel_bytes), 4);
put_le(pos, 0, 4); // reserved
put_le(pos, BMP_HEADER_SIZE, 4);
put_le(pos, BMP_INFO_HEADER_SIZE, 4);
put_le(pos, static_cast<uint32_t>(width), 4);
put_le(pos, static_cast<uint32_t>(height), 4);
put_le(pos, 1, 2); // one plane
put_le(pos, BMP_BITS_PER_PIXEL, 2);
put_le(pos, 0, 4); // not compressed
put_le(pos, static_cast<uint32_t>(pixel_bytes), 4);
put_le(pos, 0, 4); // pixels per metre across, unspecified
put_le(pos, 0, 4); // pixels per metre down, unspecified
put_le(pos, 0, 4); // no palette
put_le(pos, 0, 4); // so no palette entry matters more than another
if (fwrite(header, 1, sizeof(header), file) != sizeof(header))
return false;
for (int y = height - 1; y >= 0; y--) {
if (fwrite(pixels + static_cast<size_t>(y) * row_stride, 1, row_size, file) != row_size)
return false;
}
return true;
}
/// Reserve a name in the snapshot directory and write the picture to it.
/// With `exact` set the given name is the only one tried; otherwise a number is added on
/// collision. Returns true if a file was written.
bool write_snapshot_file(const uint8_t *pixels, int width, int height, size_t row_stride, const std::string &name,
bool exact) {
const std::string dir = snapshot_dir();
std::error_code ec;
std::filesystem::create_directories(dir, ec);
if (ec) {
ESP_LOGE(TAG, "Could not create snapshot directory %s: %s", dir.c_str(), ec.message().c_str());
return false;
}
// O_EXCL guarantees we never write over a file that is already there.
std::string path;
int fd = -1;
for (unsigned attempt = 0; attempt < MAX_NAME_ATTEMPTS; attempt++) {
path = dir + "/" + (attempt == 0 ? name : add_suffix(name, attempt));
fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0644);
if (fd >= 0)
break;
if (errno != EEXIST) {
ESP_LOGE(TAG, "Could not create %s: %s", path.c_str(), strerror(errno));
return false;
}
if (exact) {
// The caller asked for this exact name, so silently writing somewhere else would be worse
// than failing - a test asserting on the path would pick up a stale file.
ESP_LOGE(TAG, "Snapshot %s already exists, not overwriting", path.c_str());
return false;
}
}
if (fd < 0) {
ESP_LOGE(TAG, "Could not find an unused name for %s in %s", name.c_str(), dir.c_str());
return false;
}
FILE *file = fdopen(fd, "wb");
if (file == nullptr) {
ESP_LOGE(TAG, "Could not open %s: %s", path.c_str(), strerror(errno));
::close(fd);
::unlink(path.c_str());
return false;
}
bool ok = write_bmp(file, pixels, width, height, row_stride);
int saved_errno = ok ? 0 : errno;
// Closing can fail in its own right - the last of the data is still on its way out.
if (fclose(file) != 0) {
if (ok)
saved_errno = errno;
ok = false;
}
if (!ok) {
ESP_LOGE(TAG, "Could not write %s: %s", path.c_str(), strerror(saved_errno));
// Leave no truncated file behind - it would block a retry under the same name.
::unlink(path.c_str());
return false;
}
ESP_LOGI(TAG, "Snapshot written to %s", path.c_str());
return true;
}
} // namespace
// helper function since ESP_LOGW is disallowed in a header file
void Snapshot::log_action_failed() { ESP_LOGW(TAG, "snapshot.take did not write a file"); }
bool Snapshot::take_snapshot(const char *filename) {
const int width = this->snapshot_width();
const int height = this->snapshot_height();
if (width <= 0 || height <= 0) {
ESP_LOGE(TAG, "Snapshot requested but the display is %dx%d", width, height);
return false;
}
std::string name;
bool exact = false;
if (filename != nullptr) {
bool name_changed = false;
name = sanitise_filename(filename, &name_changed);
exact = !name.empty();
if (name_changed) {
ESP_LOGW(TAG, "Requested snapshot name '%s' is not an acceptable file name, using '%s' instead", filename,
name.empty() ? "a name made from the time" : name.c_str());
}
}
if (name.empty()) {
struct timespec now {};
if (clock_gettime(CLOCK_REALTIME, &now) != 0)
now = {};
struct tm tm_buf {};
if (localtime_r(&now.tv_sec, &tm_buf) == nullptr)
tm_buf = {};
char stamp[32]{};
// ::strftime to be sure of the one from <ctime>; display has an unrelated member of that name
if (::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", &tm_buf) == 0)
snprintf(stamp, sizeof(stamp), "unknown-time");
char buffer[MAX_NAME_LENGTH];
int written =
snprintf(buffer, sizeof(buffer), "%s-%s-%03ld.bmp", this->snapshot_prefix_, stamp, now.tv_nsec / 1000000);
if (written < 0 || static_cast<size_t>(written) >= sizeof(buffer)) {
ESP_LOGW(TAG, "Could not build a timestamped snapshot name, using a fallback");
snprintf(buffer, sizeof(buffer), "snapshot.bmp");
}
name = buffer;
}
// Rows are padded out to a multiple of four bytes, as the file wants them, so each one can be
// written straight from the buffer. Zeroed on allocation, which is what the padding must be.
const size_t row_stride = bmp_row_size(width);
auto pixels = std::make_unique<uint8_t[]>(row_stride * height);
if (!this->capture_bgr(pixels.get(), row_stride))
return false;
return write_snapshot_file(pixels.get(), width, height, row_stride, name, exact);
}
} // namespace esphome::snapshot
#endif
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#ifdef USE_HOST
#include "esphome/core/automation.h"
#include <cstddef>
#include <cstdint>
#include <string>
// Directory snapshots are written to. Normally set by codegen to a folder under .esphome; the
// fallback keeps the component compiling for static analysis, where no defines.h is generated.
#ifndef ESPHOME_SNAPSHOT_DIR
#define ESPHOME_SNAPSHOT_DIR "."
#endif
namespace esphome::snapshot {
/// Base for anything that can hand over the picture it is showing so it can be written to a file.
///
/// A subclass says how big the picture is and fills in the pixels. Everything else - picking a
/// name, staying inside the snapshot directory, not writing over anything, and encoding the file -
/// is done here, so every component that can take a snapshot behaves the same way.
class Snapshot {
public:
virtual ~Snapshot() = default;
/// Set the word generated names start with. Codegen passes the component id, so with more than
/// one display in a device it is clear which one a file came from.
void set_snapshot_prefix(const char *prefix) { this->snapshot_prefix_ = prefix; }
/// Write the current picture to a BMP file in the snapshot directory.
///
/// Pass nullptr to have a name made up from the prefix and the current time. A file that is
/// already there is never written over. Returns true if a file was written.
bool take_snapshot(const char *filename);
/// Log that an action-triggered snapshot did not write a file.
static void log_action_failed();
protected:
/// Width of the picture in pixels.
virtual int snapshot_width() = 0;
/// Height of the picture in pixels.
virtual int snapshot_height() = 0;
/// Fill in the picture: three bytes per pixel in blue, green, red order, topmost row first, with
/// `row_stride` bytes from the start of one row to the start of the next. Returns false, having
/// logged why, if the picture could not be read.
virtual bool capture_bgr(uint8_t *dest, size_t row_stride) = 0;
const char *snapshot_prefix_{"snapshot"};
};
template<typename... Ts> class SnapshotAction final : public Action<Ts...>, public Parented<Snapshot> {
public:
TEMPLATABLE_VALUE(std::string, filename)
protected:
void play(const Ts &...x) override {
bool ok;
if (this->filename_.has_value()) {
ok = this->parent_->take_snapshot(this->filename_.value(x...).c_str());
} else {
ok = this->parent_->take_snapshot(nullptr);
}
if (!ok)
this->parent_->log_action_failed();
}
};
} // namespace esphome::snapshot
#endif
+7 -1
View File
@@ -33,7 +33,13 @@ CONFIG_SCHEMA = (
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"t6615", baud_rate=19200, require_rx=True, require_tx=True
"t6615",
baud_rate=19200,
require_rx=True,
require_tx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
)
-1
View File
@@ -88,7 +88,6 @@ void T6615Component::query_ppm_() {
void T6615Component::dump_config() {
ESP_LOGCONFIG(TAG, "T6615:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
this->check_uart_settings(19200);
}
} // namespace esphome::t6615
+16
View File
@@ -35,6 +35,22 @@ CONFIG_SCHEMA = (
)
def _final_validate(config: ConfigType) -> ConfigType:
# Historical mode runs at 1200 baud, standard mode at 9600 baud.
baud_rate = 1200 if config[CONF_HISTORICAL_MODE] else 9600
uart.final_validate_device_schema(
"teleinfo",
baud_rate=baud_rate,
data_bits=7,
parity="EVEN",
stop_bits=1,
)(config)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE])
await cg.register_component(var, config)
+1 -6
View File
@@ -184,10 +184,7 @@ void TeleInfo::publish_value_(const std::string &tag, const std::string &val) {
element->publish_val(val);
}
}
void TeleInfo::dump_config() {
ESP_LOGCONFIG(TAG, "TeleInfo:");
this->check_uart_settings(baud_rate_, 1, uart::UART_CONFIG_PARITY_EVEN, 7);
}
void TeleInfo::dump_config() { ESP_LOGCONFIG(TAG, "TeleInfo:"); }
TeleInfo::TeleInfo(bool historical_mode) {
if (historical_mode) {
/*
@@ -195,11 +192,9 @@ TeleInfo::TeleInfo(bool historical_mode) {
*/
checksum_area_end_ = 2;
separator_ = 0x20;
baud_rate_ = 1200;
} else {
checksum_area_end_ = 1;
separator_ = 0x9;
baud_rate_ = 9600;
}
}
void TeleInfo::register_teleinfo_listener(TeleInfoListener *listener) { teleinfo_listeners_.push_back(listener); }
-1
View File
@@ -31,7 +31,6 @@ class TeleInfo final : public PollingComponent, public uart::UARTDevice {
std::vector<TeleInfoListener *> teleinfo_listeners_{};
protected:
uint32_t baud_rate_;
int checksum_area_end_;
int separator_;
char buf_[MAX_BUF_SIZE];
@@ -36,8 +36,6 @@ cover::CoverTraits Tormatic::get_traits() {
void Tormatic::dump_config() {
LOG_COVER("", "Tormatic Cover", this);
this->check_uart_settings(9600, 1, uart::UART_CONFIG_PARITY_NONE, 8);
ESP_LOGCONFIG(TAG,
" Open Duration: %.1fs\n"
" Close Duration: %.1fs",
+2
View File
@@ -3,6 +3,7 @@
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "uart_component.h"
@@ -66,6 +67,7 @@ class UARTDevice {
}
/// Check that the configuration of the UART bus matches the provided values and otherwise print a warning
ESPDEPRECATED("Use uart.final_validate_device_schema() in Python instead. Removed in 2027.3.0", "2026.9.0")
void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1,
UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t data_bits = 8);
+1
View File
@@ -30,6 +30,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
require_tx=True,
require_rx=True,
baud_rate=2400,
data_bits=8,
parity="EVEN",
stop_bits=1,
)
-1
View File
@@ -213,7 +213,6 @@ void UFM01Component::dump_config() {
LOG_BINARY_SENSOR(" ", "Empty Tube", this->empty_tube_binary_sensor_);
LOG_BINARY_SENSOR(" ", "Flow Rate Out Of Range", this->flow_rate_out_of_range_binary_sensor_);
#endif
this->check_uart_settings(2400, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
}
void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE]) {
@@ -50,7 +50,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
require_tx=True,
require_rx=True,
data_bits=8,
parity=None,
parity="NONE",
stop_bits=1,
)
@@ -29,8 +29,6 @@ void UponorSmatrixComponent::dump_config() {
}
#endif
this->check_uart_settings(19200);
if (!this->unknown_devices_.empty()) {
ESP_LOGCONFIG(TAG, " Detected unknown device addresses:");
for (auto device_address : this->unknown_devices_) {
+8
View File
@@ -29,6 +29,14 @@ CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend(
}
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"vbus",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
+1 -4
View File
@@ -11,10 +11,7 @@ static const char *const TAG = "vbus";
// Maximum bytes to log in verbose hex output (16 frames * 4 bytes = 64 bytes typical)
static constexpr size_t VBUS_MAX_LOG_BYTES = 64;
void VBus::dump_config() {
ESP_LOGCONFIG(TAG, "VBus:");
check_uart_settings(9600);
}
void VBus::dump_config() { ESP_LOGCONFIG(TAG, "VBus:"); }
static void septet_spread(uint8_t *data, int start, int count, uint8_t septet) {
for (int i = 0; i < count; i++, septet >>= 1) {
-5
View File
@@ -40,11 +40,6 @@
#include <ESP8266WiFi.h>
#include <ESP8266WiFiType.h>
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0)
extern "C" {
#include <user_interface.h>
};
#endif
#endif
#ifdef USE_RP2
@@ -21,7 +21,6 @@ extern "C" {
#include "lwip/apps/sntp.h"
#include "lwip/netif.h" // struct netif
#include <AddrList.h>
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0)
#include "LwipDhcpServer.h"
#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
#include <ESP8266WiFi.h>
@@ -30,7 +29,6 @@ extern "C" {
#define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time)
#define wifi_softap_set_dhcps_offer_option(offer, mode) dhcpSoftAP.set_dhcps_offer_option(offer, mode)
#endif
#endif
}
#include "esphome/core/application.h"
@@ -293,7 +291,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
conf.bssid_set = 0;
}
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
if (ap.password_.empty()) {
conf.threshold.authmode = AUTH_OPEN;
} else {
@@ -310,7 +307,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
}
}
conf.threshold.rssi = -127;
#endif
ETS_UART_INTR_DISABLE();
bool ret = wifi_station_set_config_current(&conf);
@@ -602,7 +598,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
#endif
break;
}
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
case EVENT_OPMODE_CHANGED: {
auto it = event->event_info.opmode_changed;
ESP_LOGV(TAG, "Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)),
@@ -620,7 +615,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
#endif
break;
}
#endif
default:
break;
}
@@ -705,7 +699,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
config.bssid = nullptr;
config.channel = 0;
config.show_hidden = 1;
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE;
// Use shorter dwell times for roaming scans - we only need to detect strong
// nearby APs, not do a thorough survey. This also reduces off-channel time
@@ -724,7 +717,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
config.scan_time.active.min = roaming ? SCAN_ACTIVE_MIN_ROAMING_MS : SCAN_ACTIVE_MIN_DEFAULT_MS;
config.scan_time.active.max = roaming ? SCAN_ACTIVE_MAX_ROAMING_MS : SCAN_ACTIVE_MAX_DEFAULT_MS;
}
#endif
bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback);
if (!ret) {
ESP_LOGV(TAG, "wifi_station_scan failed");
@@ -830,7 +822,7 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
return false;
}
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
dhcpSoftAP.begin(&info);
#endif
+8
View File
@@ -21,6 +21,14 @@ CONFIG_SCHEMA = (
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"wl_134",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
var = await text_sensor.new_text_sensor(config)
-2
View File
@@ -110,7 +110,5 @@ uint64_t Wl134Component::hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t t
void Wl134Component::dump_config() {
ESP_LOGCONFIG(TAG, "WL-134 Sensor:");
LOG_TEXT_SENSOR("", "Tag", this);
// As specified in the sensor's data sheet
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
}
} // namespace esphome::wl_134
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.9.0-dev"
__version__ = "2026.10.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
+1
View File
@@ -13,6 +13,7 @@
#define ESPHOME_PROJECT_VERSION "v2"
#define ESPHOME_PROJECT_VERSION_30 "v2"
#define ESPHOME_VARIANT "ESP32"
#define ESPHOME_SNAPSHOT_DIR "."
#define ESPHOME_NAME_ADD_MAC_SUFFIX
#define ESPHOME_DEBUG_SCHEDULER
#define ESPHOME_DEBUG_API
-14
View File
@@ -18,7 +18,6 @@
#ifdef USE_STORE_LOG_STR_IN_FLASH
#include "WString.h"
#include "esphome/core/defines.h" // for USE_ARDUINO_VERSION_CODE
#endif
// Include ESP-IDF/Arduino based logging methods here so they don't undefine ours later
@@ -177,20 +176,7 @@ struct LogString;
#include <pgmspace.h>
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 0)
#define LOG_STR_ARG(s) ((PGM_P) (s))
#else
// Pre-Arduino 2.5, we can't pass a PSTR() to printf(). Emulate support by copying the message to a
// local buffer first. String length is limited to 63 characters.
// https://github.com/esp8266/Arduino/commit/6280e98b0360f85fdac2b8f10707fffb4f6e6e31
#define LOG_STR_ARG(s) \
({ \
char __buf[64]; \
__buf[63] = '\0'; \
strncpy_P(__buf, (PGM_P) (s), 63); \
__buf; \
})
#endif
#define LOG_STR(s) (reinterpret_cast<const LogString *>(PSTR(s)))
#define LOG_STR_LITERAL(s) LOG_STR_ARG(LOG_STR(s))
+3 -3
View File
@@ -14,7 +14,7 @@ esptool==5.3.1
click==8.3.3
aioesphomeapi==46.3.0
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
zeroconf==0.151.2
zeroconf==0.151.3
puremagic==2.2.0
ruamel.yaml==0.19.1 # dashboard_import
ruamel.yaml.clib==0.2.15 # dashboard_import
@@ -28,8 +28,8 @@ smpclient==7.2.0
requests==2.34.2
py7zr==1.1.3
platformdirs==4.11.5 # native esp-idf toolchain global cache dir
ninja==1.13.0 # native esp8266 arduino toolchain build driver
filelock==3.32.4 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg
ninja==1.13.2 # native esp8266 arduino toolchain build driver
filelock==3.32.5 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg
# esp-idf >= 5.0 requires this
pyparsing >= 3.3.2
-17
View File
@@ -1104,10 +1104,6 @@ def get_components_per_integration_fixture() -> dict[str, set[str]]:
_TEST_FUNC_RE = re.compile(r"async def (test_\w+)")
# Any usage form (decorator, pytestmark assignment or list element); only
# test_*.py files are scanned, so the marker docs elsewhere cannot false-hit
_SHARED_YAML_USE_RE = re.compile(r"\bmark\.shared_yaml")
_SHARED_YAML_ARG_RE = re.compile(r"\(\s*[\"'](\w+)[\"']\s*\)")
@cache
@@ -1127,19 +1123,6 @@ def get_fixture_to_test_files() -> dict[str, frozenset[str]]:
for func in _TEST_FUNC_RE.findall(content):
base_name = func.replace("test_", "").partition("[")[0]
result.setdefault(base_name, set()).add(rel_path)
# Shared fixtures are named by marker, not by a test function; each
# decorator must carry a string literal or its fixture would silently
# map to no tests
for use in _SHARED_YAML_USE_RE.finditer(content):
arg = _SHARED_YAML_ARG_RE.match(content, use.end())
if arg is None:
line = content.count("\n", 0, use.start()) + 1
raise ValueError(
f"{rel_path}:{line}: shared_yaml marker must take a "
"single-line string literal so CI test selection can map "
"its fixture"
)
result.setdefault(arg.group(1), set()).add(rel_path)
return {k: frozenset(v) for k, v in result.items()}
@@ -26,7 +26,7 @@ from ..types import SetCoreConfigCallable
(PlatformFramework.ESP32_IDF, None, True),
(PlatformFramework.RTL87XX_ARDUINO, FAMILY_RTL8720C, True),
(PlatformFramework.RTL87XX_ARDUINO, FAMILY_RTL8710B, False),
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231N, True),
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231N, False),
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7238, True),
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231T, False),
(PlatformFramework.ESP8266_ARDUINO, None, False),
+101
View File
@@ -0,0 +1,101 @@
"""Tests for the sdl display schema, in particular the headless option."""
from __future__ import annotations
import pytest
from esphome import config_validation as cv
from esphome.components.sdl.display import (
CONF_SDL_ID,
CONFIG_SCHEMA,
headless_final_validate,
)
from esphome.config import Config
from esphome.const import PlatformFramework
from esphome.core import ID
from esphome.final_validate import full_config
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
@pytest.fixture(autouse=True)
def _host_platform(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(PlatformFramework.HOST_NATIVE)
def _config(**extra: object) -> ConfigType:
config: ConfigType = {
"dimensions": {"width": 320, "height": 240},
# sdl2-config is not necessarily installed in the test environment
"sdl_options": "-lSDL2",
}
config.update(extra)
return config
def test_defaults_to_windowed() -> None:
"""A display without the option is not headless."""
assert CONFIG_SCHEMA(_config())["headless"] is False
def test_headless_accepted() -> None:
"""A headless display needs nothing beyond the dimensions."""
assert CONFIG_SCHEMA(_config(headless=True))["headless"] is True
def test_headless_rejects_window_options() -> None:
"""Window options are meaningless without a window."""
with pytest.raises(cv.Invalid, match="has no effect"):
CONFIG_SCHEMA(
_config(headless=True, window_options={"position": {"x": 0, "y": 0}})
)
def test_headless_rejects_snapshot_key() -> None:
"""A headless display has no keyboard, so the action is the only way in."""
with pytest.raises(cv.Invalid, match="snapshot.take"):
CONFIG_SCHEMA(_config(headless=True, snapshot_key="SDLK_F12"))
def test_snapshot_key_accepted_when_windowed() -> None:
"""The key is only valid alongside a window."""
config = CONFIG_SCHEMA(_config(snapshot_key="SDLK_F12"))
assert str(config["snapshot_key"]) == "SDLK_F12"
def _declare_sdl_display(headless: bool) -> ID:
"""Register a full_config with a single sdl display declaration and return a reference to it.
Mirrors what the real config pipeline leaves behind: a "display" domain entry plus a
declare_ids record id_declaration_match_schema uses to find it again.
"""
declared_id = ID("my_sdl", is_declaration=True)
fc = Config()
fc["display"] = [
{
"platform": "sdl",
"id": declared_id,
"headless": headless,
"dimensions": {"width": 320, "height": 240},
}
]
fc.declare_ids.append((declared_id, ["display", 0, "id"]))
full_config.set(fc)
return ID("my_sdl")
@pytest.mark.parametrize("platform", ["binary_sensor", "touchscreen"])
def test_headless_final_validate_rejects_headless_display(platform: str) -> None:
"""binary_sensor and touchscreen both need a window, so a headless display is rejected."""
sdl_ref = _declare_sdl_display(headless=True)
schema = headless_final_validate(platform)
with pytest.raises(cv.Invalid, match="needs a window"):
schema({CONF_SDL_ID: sdl_ref})
@pytest.mark.parametrize("platform", ["binary_sensor", "touchscreen"])
def test_headless_final_validate_accepts_windowed_display(platform: str) -> None:
"""The same platforms are accepted once the display has a window."""
sdl_ref = _declare_sdl_display(headless=False)
schema = headless_final_validate(platform)
schema({CONF_SDL_ID: sdl_ref}) # Should not raise.
+1 -1
View File
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO14
packages:
uart_38400: !include ../../test_build_components/common/uart_38400/esp32-idf.yaml
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/esp32-idf.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO3
packages:
uart_38400: !include ../../test_build_components/common/uart_38400/esp8266-ard.yaml
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/esp8266-ard.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart_38400: !include ../../test_build_components/common/uart_38400/rp2040-ard.yaml
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/rp2040-ard.yaml
<<: !include common.yaml
@@ -1,4 +1,4 @@
packages:
uart_1200: !include ../../test_build_components/common/uart_1200/esp32-idf.yaml
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp32-idf.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
uart_rx_pin: GPIO3
packages:
uart_1200: !include ../../test_build_components/common/uart_1200/esp8266-ard.yaml
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp8266-ard.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO2
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
uart_115200: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml
<<: !include common.yaml
@@ -2,7 +2,7 @@ remote_transmitter:
id: xmitr
pin: GPIO26
carrier_duty_percent: 50%
# non_blocking is bk7231n/bk7238-only; the CI board is a BK7252
# non_blocking is bk7238-only; the CI board is a BK7252, so this builds the bit-bang path
packages:
buttons: !include common-buttons.yaml
+27
View File
@@ -14,6 +14,15 @@ display:
position:
x: 100
y: 100
snapshot_key: SDLK_F12
- platform: sdl
id: headless_display
headless: true
show_test_card: true
dimensions:
width: 320
height: 240
- platform: sdl
id: second_display
@@ -46,3 +55,21 @@ binary_sensor:
sdl_id: sdl_sdl_display
id: key_enter
key: SDLK_RETURN
esphome:
# A name of your own is only good for one snapshot - a second one under the same name fails
# rather than writing over the first - so these run once rather than on a repeating interval.
on_boot:
- delay: 2s
- snapshot.take:
id: headless_display
filename: test_card.bmp
- snapshot.take:
id: headless_display
filename: !lambda 'return "shot.bmp";'
interval:
# A generated name has the time in it, so this one can repeat.
- interval: 10s
then:
- snapshot.take: sdl_sdl_display
+29
View File
@@ -0,0 +1,29 @@
# Config-only test for the headless and screenshot options. The combinations that must be
# rejected are covered by tests/component_tests/sdl/test_sdl.py; this file checks that the
# accepted forms validate together.
host:
mac_address: "62:23:45:AF:B3:DD"
display:
- platform: sdl
id: headless_display
headless: true
dimensions: 320x240
- platform: sdl
id: windowed_display
dimensions: 320x240
snapshot_key: SDLK_F12
binary_sensor:
- platform: sdl
sdl_id: windowed_display
id: key_up
key: SDLK_UP
interval:
- interval: 10s
then:
- snapshot.take:
id: headless_display
filename: periodic.bmp
+34
View File
@@ -0,0 +1,34 @@
display:
- platform: snapshot
id: snapshot_display
update_interval: 1s
show_test_card: true
# An odd width exercises the row padding in the BMP writer
dimensions:
width: 101
height: 64
- platform: snapshot
id: snapshot_rotated
rotation: 90
dimensions: 320x240
lambda: |-
it.filled_rectangle(0, 0, 40, 20, Color(0xFF, 0x80, 0x00));
esphome:
# A name of your own is only good for one snapshot - a second one under the same name fails
# rather than writing over the first - so these run once rather than on a repeating interval.
on_boot:
- delay: 2s
- snapshot.take:
id: snapshot_display
filename: test_card.bmp
- snapshot.take:
id: snapshot_rotated
filename: !lambda 'return "rotated.bmp";'
interval:
# A generated name has the time in it, so this one can repeat.
- interval: 10s
then:
- snapshot.take: snapshot_display
+5
View File
@@ -0,0 +1,5 @@
host:
mac_address: "62:23:45:AF:B3:DE"
packages:
snapshot: !include common.yaml

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