Compare commits

..

27 Commits

Author SHA1 Message Date
Jesse Hills
8ab3cb9d2b [CI] Auto close issues that do not follow the template/form correctly and dump logs outside the logs section 2025-07-21 09:38:57 +12:00
J. Nick Koston
5b3d61b4a6 [api] Fix missing ifdef guards for field_ifdef fields in protobuf base classes (#9693) 2025-07-20 15:41:00 +12:00
JonasB2497
727e8ca376 [sdl][mipi_spi] Respect clipping when drawing (#9722)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-07-19 22:29:02 +00:00
tmpeh
5ed77c10ae Fix format string error in ota_web_server.cpp (#9711) 2025-07-19 11:24:26 -10:00
J. Nick Koston
89b9bddf1b [CI] Fix clang-tidy not running when platformio.ini changes (#9678) 2025-07-19 20:55:21 +12:00
J. Nick Koston
65cbb0d741 [gpio] Auto-disable interrupts for shared GPIO pins in binary sensors (#9701) 2025-07-19 05:31:53 +00:00
Jesse Hills
9533d52d86 Merge branch 'release' into dev 2025-07-19 12:05:32 +12:00
Jesse Hills
6fe4ffa0cf Merge pull request #9691 from esphome/bump-2025.7.2
2025.7.2
2025-07-19 12:04:51 +12:00
Jesse Hills
19a68dc650 Add core team as codeowner of .github folder (#9663) 2025-07-19 10:55:22 +12:00
Jesse Hills
576ce7ee35 Bump version to 2025.7.2 2025-07-19 09:56:08 +12:00
J. Nick Koston
8a45e877bb [gpio] Disable interrupt mode by default for LibreTiny platforms (#9687)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-19 09:56:08 +12:00
Kevin Ahrendt
84607c1255 [voice_assistant] Use media player callbacks to track TTS response status (#9670) 2025-07-19 09:56:01 +12:00
Kevin Ahrendt
8664ec0a3b [speaker] Media player's pipeline properly returns playing state near end of file (#9668) 2025-07-19 09:54:15 +12:00
J. Nick Koston
32d8c60a0b Fix AsyncTCP version mismatch between platformio.ini and async_tcp component (#9676) 2025-07-19 09:54:00 +12:00
Jesse Hills
976a1e27b4 [lvgl] Prevent keyerror on min/max value widgets with no default (#9660) 2025-07-19 09:53:47 +12:00
J. Nick Koston
cc2c1b1d89 [libretiny] Remove unsupported lock-free queue and event pool implementations (#9653) 2025-07-19 09:53:47 +12:00
Clyde Stubbs
85495d38b7 [lvgl] Fix meter rotation (#9605)
Co-authored-by: clydeps <U5yx99dok9>
2025-07-19 09:53:47 +12:00
J. Nick Koston
84a77ee427 [scheduler] Fix DelayAction cancellation in restart mode scripts (#9646) 2025-07-19 09:53:47 +12:00
@RubenKelevra
11a4115e30 esp32_camera: deprecate i2c_pins; throw error if combined with i2c: block (#9615) 2025-07-19 09:53:47 +12:00
Samuel Sieb
121ed687f3 [logger] fix on_message (#9642)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-19 09:53:47 +12:00
J. Nick Koston
c602f3082e [scheduler] Fix cancellation of timers with empty string names (#9641) 2025-07-19 09:53:39 +12:00
J. Nick Koston
4a43f922c6 [wireguard] Fix boot loop when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled (#9637) 2025-07-19 09:50:36 +12:00
J. Nick Koston
21e66b76e4 [api] Fix compilation error with char* lambdas in HomeAssistant services (#9638)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-19 09:50:36 +12:00
Flo
cdeed7afa7 Fix template event web_server crash (#9618) 2025-07-19 09:50:36 +12:00
J. Nick Koston
6cefe943e9 [gpio] Disable interrupt mode by default for LibreTiny platforms (#9687)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-19 09:32:20 +12:00
Kevin Ahrendt
6f74decd79 [i2s_audio] Bugfix: cast adc_channel_t to adc1_channel_t (#9688) 2025-07-18 16:52:46 -04:00
dependabot[bot]
60350e8abd Bump aioesphomeapi from 37.0.0 to 37.0.1 (#9685)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-18 20:08:29 +00:00
15 changed files with 341 additions and 13 deletions

View File

@@ -1 +1 @@
07f621354fe1350ba51953c80273cd44a04aa44f15cc30bd7b8fe2a641427b7a
0c2acbc16bfb7d63571dbe7042f94f683be25e4ca8a0f158a960a94adac4b931

View File

@@ -21,6 +21,10 @@ body:
Provide a clear and concise description of what the problem is.
⚠️ **WARNING: Do NOT paste logs, stack traces, or error messages here!**
Use the "Logs" section below instead. Issues with logs
in this field will be automatically closed.
- type: markdown
attributes:
value: |
@@ -79,7 +83,7 @@ body:
- type: textarea
id: logs
attributes:
label: Anything in the logs that might be useful for us?
label: Logs
description: For example, error message, or stack traces. Serial or USB logs are much more useful than WiFi logs.
render: txt
- type: textarea

View File

@@ -0,0 +1,248 @@
name: Auto-close issues with logs in problem field
on:
issues:
types: [opened]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
issue_number:
description: 'Issue number to check for logs'
required: true
type: number
jobs:
check-logs-in-problem:
runs-on: ubuntu-latest
if: github.event.issue.state == 'open' || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@esphomebot reopen')) || github.event_name == 'workflow_dispatch'
steps:
- name: Check for logs and handle issue state
uses: actions/github-script@v7.0.1
with:
script: |
// Handle different trigger types
let issue, isReassessment;
if (context.eventName === 'workflow_dispatch') {
// Manual dispatch - get issue from input
const issueNumber = ${{ github.event.inputs.issue_number }};
console.log('Manual dispatch for issue:', issueNumber);
const issueResponse = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(issueNumber)
});
issue = issueResponse.data;
isReassessment = false; // Treat manual dispatch as initial check
} else {
// Normal event-driven flow
issue = context.payload.issue;
isReassessment = context.eventName === 'issue_comment' && context.payload.comment.body.includes('@esphomebot reopen');
}
console.log('Event type:', context.eventName);
console.log('Is reassessment:', isReassessment);
console.log('Issue state:', issue.state);
// Extract the problem section from the issue body
const body = issue.body || '';
// Look for the problem section between "### The problem" and the next section
const problemMatch = body.match(/### The problem\s*\n([\s\S]*?)(?=\n### |$)/i);
if (!problemMatch) {
console.log('Could not find problem section');
if (isReassessment) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: '❌ Could not find the "The problem" section in the issue template. Please make sure you are using the proper issue template format.'
});
}
return;
}
const problemText = problemMatch[1].trim();
console.log('Problem text length:', problemText.length);
// Function to check if text contains logs
function checkForLogs(text) {
// Patterns that indicate logs/stack traces/error messages
const logPatterns = [
// ESPHome specific log patterns with brackets
/^\[[DIWEVC]\]\[[^\]]+(?::\d+)?\]:/m, // [D][component:123]: message
/^\[\d{2}:\d{2}:\d{2}\]\[[DIWEVC]\]\[[^\]]+(?::\d+)?\]:/m, // [12:34:56][D][component:123]: message
/^\[\d{2}:\d{2}:\d{2}\.\d{3}\]\[[DIWEVC]\]\[[^\]]+(?::\d+)?\]:/m, // [12:34:56.123][D][component:123]: message
// Common log prefixes
/^\[[\d\s\-:\.]+\]/m, // [timestamp] format
/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/m, // YYYY-MM-DD HH:MM:SS
/^\w+\s+\d{2}:\d{2}:\d{2}/m, // INFO 12:34:56
// Error indicators
/^(ERROR|WARN|WARNING|FATAL|DEBUG|INFO|TRACE)[\s:]/mi,
/^(Exception|Error|Traceback|Stack trace)/mi,
/at\s+[\w\.]+\([^)]*:\d+:\d+\)/m, // Stack trace format
/^\s*File\s+"[^"]*",\s+line\s+\d+/m, // Python traceback
// Legacy ESPHome log patterns
/^\[\d{2}:\d{2}:\d{2}\]\[/m, // [12:34:56][component]
/^WARNING\s+[^:\s]+:/m, // WARNING component:
/^ERROR\s+[^:\s]+:/m, // ERROR component:
// Multiple consecutive lines starting with similar patterns
/(^(INFO|DEBUG|WARN|ERROR)[^\n]*\n){3,}/mi,
/(^\[[DIWEVC]\]\[[^\]]+\][^\n]*\n){3,}/mi, // Multiple ESPHome log lines
// Hex dumps or binary data
/0x[0-9a-f]{4,}/i,
/[0-9a-f]{8,}/,
// Compilation errors
/error:\s+/i,
/:\d+:\d+:\s+(error|warning):/i,
// Very long lines (often log output)
/.{200,}/
];
const hasLogs = logPatterns.some(pattern => {
const matches = pattern.test(text);
if (matches) {
console.log('Pattern matched:', pattern.toString());
}
return matches;
});
// Additional heuristics
const lineCount = text.split('\n').length;
const hasLotsOfLines = lineCount > 20; // More than 20 lines might be logs
const hasCodeBlocks = (text.match(/```/g) || []).length >= 2;
const longCodeBlock = hasCodeBlocks && text.length > 1000;
console.log(`Lines: ${lineCount}, Has logs: ${hasLogs}, Long code block: ${longCodeBlock}`);
return hasLogs || (hasLotsOfLines && longCodeBlock);
}
const hasLogsInProblem = checkForLogs(problemText);
// Handle reassessment (when @esphomebot is mentioned)
if (isReassessment) {
if (!hasLogsInProblem) {
// No logs found, check if issue was auto-closed and reopen it
if (issue.state === 'closed') {
// Check if it has the auto-closed label
const labels = issue.labels.map(label => label.name);
if (labels.includes('auto-closed')) {
console.log('Reopening issue - logs have been moved');
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'open'
});
// Remove auto-closed and invalid labels
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
name: 'auto-closed'
});
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
name: 'invalid'
});
// Find and edit the original auto-close comment
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number
});
const autoCloseComment = comments.data.find(comment =>
comment.user.login === 'github-actions[bot]' &&
comment.body.includes('automatically closed because it appears to contain logs')
);
if (autoCloseComment) {
const updatedComment = `✅ **ISSUE REOPENED**
Thank you for helping us maintain organized issue reports! 🙏`;
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: autoCloseComment.id,
body: updatedComment
});
}
}
}
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: '❌ Logs are still detected in the "The problem" section. Please move them to the "Logs" section and try again.'
});
}
return;
}
// Handle initial issue opening
if (!hasLogsInProblem) {
console.log('No logs detected in problem field');
return;
}
console.log('Logs detected in problem field, closing issue');
// Close the issue
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
state: 'closed'
});
// Add a comment explaining why it was closed
const comment = `This issue has been automatically closed because it appears to contain logs, stack traces, or error messages in the "The problem" field.
⚠️ **Please follow the issue template correctly:**
- Use the "The problem" field to **describe** your issue in plain English
- Put logs, error messages, and stack traces in the "Logs" section instead
To reopen this issue:
1. Edit your original issue description
2. Move any logs/error messages to the appropriate "Logs" section
3. Rewrite the "The problem" section with a clear description of what you were trying to do and what went wrong
4. Comment exactly \`@esphomebot reopen\` to reassess and automatically reopen if fixed
Thank you for helping us maintain organized issue reports! 🙏`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: comment
});
// Add labels
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['invalid', 'auto-closed']
});

View File

@@ -9,6 +9,7 @@
pyproject.toml @esphome/core
esphome/*.py @esphome/core
esphome/core/* @esphome/core
.github/** @esphome/core
# Integrations
esphome/components/a01nyub/* @MrSuicideParrot

View File

@@ -301,8 +301,10 @@ class APIConnection : public APIServerConnection {
if (entity->has_own_name())
msg.name = entity->get_name();
// Set common EntityBase properties
// Set common EntityBase properties
#ifdef USE_ENTITY_ICON
msg.icon = entity->get_icon();
#endif
msg.disabled_by_default = entity->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
#ifdef USE_DEVICES

View File

@@ -292,9 +292,13 @@ class InfoResponseProtoMessage : public ProtoMessage {
uint32_t key{0};
std::string name{};
bool disabled_by_default{false};
#ifdef USE_ENTITY_ICON
std::string icon{};
#endif
enums::EntityCategory entity_category{};
#ifdef USE_DEVICES
uint32_t device_id{0};
#endif
protected:
};
@@ -303,7 +307,9 @@ class StateResponseProtoMessage : public ProtoMessage {
public:
~StateResponseProtoMessage() override = default;
uint32_t key{0};
#ifdef USE_DEVICES
uint32_t device_id{0};
#endif
protected:
};
@@ -312,7 +318,9 @@ class CommandProtoMessage : public ProtoDecodableMessage {
public:
~CommandProtoMessage() override = default;
uint32_t key{0};
#ifdef USE_DEVICES
uint32_t device_id{0};
#endif
protected:
};

View File

@@ -4,7 +4,13 @@ from esphome import pins
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN
from esphome.const import (
CONF_ALLOW_OTHER_USES,
CONF_ID,
CONF_NAME,
CONF_NUMBER,
CONF_PIN,
)
from esphome.core import CORE
from .. import gpio_ns
@@ -29,7 +35,21 @@ CONFIG_SCHEMA = (
.extend(
{
cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean,
# Interrupts are disabled by default for bk72xx, ln882x, and rtl87xx platforms
# due to hardware limitations or lack of reliable interrupt support. This ensures
# stable operation on these platforms. Future maintainers should verify platform
# capabilities before changing this default behavior.
cv.SplitDefault(
CONF_USE_INTERRUPT,
bk72xx=False,
esp32=True,
esp8266=True,
host=True,
ln882x=False,
nrf52=True,
rp2040=True,
rtl87xx=False,
): cv.boolean,
cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum(
INTERRUPT_TYPES, upper=True
),
@@ -62,6 +82,18 @@ async def to_code(config):
)
use_interrupt = False
# Check if pin is shared with other components (allow_other_uses)
# When a pin is shared, interrupts can interfere with other components
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
_LOGGER.info(
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
"The sensor will use polling mode for compatibility with other pin uses.",
config.get(CONF_NAME, config[CONF_ID]),
config[CONF_PIN][CONF_NUMBER],
)
use_interrupt = False
cg.add(var.set_use_interrupt(use_interrupt))
if use_interrupt:
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))

View File

@@ -36,8 +36,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_ADC
void set_adc_channel(adc1_channel_t channel) {
this->adc_channel_ = channel;
void set_adc_channel(adc_channel_t channel) {
this->adc_channel_ = (adc1_channel_t) channel;
this->adc_ = true;
}
#endif

View File

@@ -534,6 +534,8 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
// Draw a pixel at the given coordinates.
void draw_pixel_at(int x, int y, Color color) override {
if (!this->get_clipping().inside(x, y))
return;
rotate_coordinates_(x, y);
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
return;

View File

@@ -48,6 +48,9 @@ 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))
return;
SDL_Rect rect{x, y, 1, 1};
auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB));
SDL_UpdateTexture(this->texture_, &rect, &data, 2);

View File

@@ -76,7 +76,7 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
} else {
ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_);
}
#ifdef USE_OTA_STATE_CALLBACK
// Report progress - use call_deferred since we're in web server task
@@ -171,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
// Finalize
if (final) {
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%u, contentLength=%zu", index, len,
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%" PRIu32 ", contentLength=%zu", index, len,
this->ota_read_length_, request->contentLength());
// For Arduino framework, the Update library tracks expected size from firmware header

View File

@@ -138,7 +138,7 @@ lib_deps =
WiFi ; wifi,web_server_base,ethernet (Arduino built-in)
Update ; ota,web_server_base (Arduino built-in)
${common:arduino.lib_deps}
ESP32Async/AsyncTCP@3.4.4 ; async_tcp
ESP32Async/AsyncTCP@3.4.5 ; async_tcp
NetworkClientSecure ; http_request,nextion (Arduino built-in)
HTTPClient ; http_request,nextion (Arduino built-in)
ESPmDNS ; mdns (Arduino built-in)

View File

@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==4.9.0
click==8.1.7
esphome-dashboard==20250514.0
aioesphomeapi==37.0.0
aioesphomeapi==37.0.1
zeroconf==0.147.0
puremagic==1.30
ruamel.yaml==0.18.14 # dashboard_import

View File

@@ -1491,6 +1491,28 @@ def find_common_fields(
return common_fields
def get_common_field_ifdef(
field_name: str, messages: list[descriptor.DescriptorProto]
) -> str | None:
"""Get the field_ifdef option if it's consistent across all messages.
Args:
field_name: Name of the field to check
messages: List of messages that contain this field
Returns:
The field_ifdef string if all messages have the same value, None otherwise
"""
field_ifdefs = {
get_field_opt(field, pb.field_ifdef)
for msg in messages
if (field := next((f for f in msg.field if f.name == field_name), None))
}
# Return the ifdef only if all messages agree on the same value
return field_ifdefs.pop() if len(field_ifdefs) == 1 else None
def build_base_class(
base_class_name: str,
common_fields: list[descriptor.FieldDescriptorProto],
@@ -1506,9 +1528,14 @@ def build_base_class(
for field in common_fields:
ti = create_field_type_info(field)
# Get field_ifdef if it's consistent across all messages
field_ifdef = get_common_field_ifdef(field.name, messages)
# Only add field declarations, not encode/decode logic
protected_content.extend(ti.protected_content)
public_content.extend(ti.public_content)
if ti.protected_content:
protected_content.extend(wrap_with_ifdef(ti.protected_content, field_ifdef))
if ti.public_content:
public_content.extend(wrap_with_ifdef(ti.public_content, field_ifdef))
# Determine if any message using this base class needs decoding
needs_decode = any(

View File

@@ -31,6 +31,7 @@ BASE = """
pyproject.toml @esphome/core
esphome/*.py @esphome/core
esphome/core/* @esphome/core
.github/** @esphome/core
# Integrations
""".strip()