mirror of
https://github.com/esphome/esphome.git
synced 2026-09-30 16:30:22 +00:00
Merge branch 'dev' into esp32_touch_new_driver
This commit is contained in:
@@ -3,10 +3,13 @@ esp_ldo:
|
||||
channel: 3
|
||||
voltage: 2.5V
|
||||
adjustable: true
|
||||
- id: ldo_4
|
||||
- id: ldo_4_passthrough
|
||||
channel: 4
|
||||
voltage: 2.0V
|
||||
setup_priority: 900
|
||||
voltage: passthrough
|
||||
- id: ldo_1_internal
|
||||
channel: 1
|
||||
voltage: 1.8V
|
||||
allow_internal_channel: true
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -1,9 +1,21 @@
|
||||
from esphome.components import socket
|
||||
from esphome.const import (
|
||||
KEY_CORE,
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def _setup_platform(platform=PLATFORM_ESP8266) -> None:
|
||||
"""Set up CORE.data with a platform for testing."""
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__first_call() -> None:
|
||||
"""Test that first call sets up define and consumes socket."""
|
||||
_setup_platform()
|
||||
CORE.config = {"wifi": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
@@ -32,6 +44,7 @@ def test_require_wake_loop_threadsafe__idempotent() -> None:
|
||||
|
||||
def test_require_wake_loop_threadsafe__multiple_calls() -> None:
|
||||
"""Test that multiple calls only set up once."""
|
||||
_setup_platform()
|
||||
# Call three times
|
||||
CORE.config = {"openthread": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
@@ -75,3 +88,29 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -
|
||||
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
assert "socket.wake_loop_threadsafe" not in udp_consumers
|
||||
assert udp_consumers == initial_udp
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None:
|
||||
"""Test that ESP32 uses task notifications instead of UDP socket."""
|
||||
_setup_platform(PLATFORM_ESP32)
|
||||
CORE.config = {"wifi": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify the define was added
|
||||
assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
|
||||
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
|
||||
|
||||
# Verify no UDP socket was consumed (ESP32 uses FreeRTOS task notifications)
|
||||
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
assert "socket.wake_loop_threadsafe" not in udp_consumers
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__non_esp32_consumes_udp_socket() -> None:
|
||||
"""Test that non-ESP32 platforms consume a UDP socket for wake notifications."""
|
||||
_setup_platform(PLATFORM_ESP8266)
|
||||
CORE.config = {"wifi": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify UDP socket was consumed
|
||||
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
assert udp_consumers.get("socket.wake_loop_threadsafe") == 1
|
||||
|
||||
@@ -28,6 +28,11 @@ sensor:
|
||||
id: source_sensor_4
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Sensor 5"
|
||||
id: source_sensor_5
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor_1
|
||||
name: "Filter Min"
|
||||
@@ -69,6 +74,13 @@ sensor:
|
||||
filters:
|
||||
- delta: 0
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor_5
|
||||
name: "Filter Percentage"
|
||||
id: filter_percentage
|
||||
filters:
|
||||
- delta: 50%
|
||||
|
||||
script:
|
||||
- id: test_filter_min
|
||||
then:
|
||||
@@ -154,6 +166,28 @@ script:
|
||||
id: source_sensor_4
|
||||
state: 2.0
|
||||
|
||||
- id: test_filter_percentage
|
||||
then:
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: 100.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: 120.0 # Filtered out (delta=20, need >50)
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: 160.0 # Passes (delta=60 > 50% of 100=50)
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: 200.0 # Filtered out (delta=40, need >50% of 160=80)
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_5
|
||||
state: 250.0 # Passes (delta=90 > 80)
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test Filter Min"
|
||||
@@ -178,3 +212,9 @@ button:
|
||||
id: btn_filter_zero_delta
|
||||
on_press:
|
||||
- script.execute: test_filter_zero_delta
|
||||
|
||||
- platform: template
|
||||
name: "Test Filter Percentage"
|
||||
id: btn_filter_percentage
|
||||
on_press:
|
||||
- script.execute: test_filter_percentage
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
esphome:
|
||||
name: varint-5byte-test
|
||||
# Define areas and devices - device_ids will be FNV hashes > 2^28,
|
||||
# requiring 5-byte varint encoding that exercises the 32-bit parse boundary.
|
||||
areas:
|
||||
- id: test_area
|
||||
name: Test Area
|
||||
devices:
|
||||
- id: sub_device_one
|
||||
name: Sub Device One
|
||||
area_id: test_area
|
||||
- id: sub_device_two
|
||||
name: Sub Device Two
|
||||
area_id: test_area
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
|
||||
# Switches on sub-devices so we can send commands with large device_id varints
|
||||
switch:
|
||||
- platform: template
|
||||
name: Device Switch
|
||||
device_id: sub_device_one
|
||||
id: device_switch_one
|
||||
optimistic: true
|
||||
turn_on_action:
|
||||
- logger.log: "Switch one on"
|
||||
turn_off_action:
|
||||
- logger.log: "Switch one off"
|
||||
|
||||
- platform: template
|
||||
name: Device Switch
|
||||
device_id: sub_device_two
|
||||
id: device_switch_two
|
||||
optimistic: true
|
||||
turn_on_action:
|
||||
- logger.log: "Switch two on"
|
||||
turn_off_action:
|
||||
- logger.log: "Switch two off"
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
name: Device Sensor
|
||||
device_id: sub_device_one
|
||||
lambda: return 42.0;
|
||||
update_interval: 0.1s
|
||||
@@ -24,12 +24,14 @@ async def test_sensor_filters_delta(
|
||||
"filter_max": [],
|
||||
"filter_baseline_max": [],
|
||||
"filter_zero_delta": [],
|
||||
"filter_percentage": [],
|
||||
}
|
||||
|
||||
filter_min_done = loop.create_future()
|
||||
filter_max_done = loop.create_future()
|
||||
filter_baseline_max_done = loop.create_future()
|
||||
filter_zero_delta_done = loop.create_future()
|
||||
filter_percentage_done = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if not isinstance(state, SensorState) or state.missing_state:
|
||||
@@ -66,6 +68,12 @@ async def test_sensor_filters_delta(
|
||||
and not filter_zero_delta_done.done()
|
||||
):
|
||||
filter_zero_delta_done.set_result(True)
|
||||
elif (
|
||||
sensor_name == "filter_percentage"
|
||||
and len(sensor_values[sensor_name]) == 3
|
||||
and not filter_percentage_done.done()
|
||||
):
|
||||
filter_percentage_done.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
@@ -80,6 +88,7 @@ async def test_sensor_filters_delta(
|
||||
"filter_max": "Filter Max",
|
||||
"filter_baseline_max": "Filter Baseline Max",
|
||||
"filter_zero_delta": "Filter Zero Delta",
|
||||
"filter_percentage": "Filter Percentage",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -98,13 +107,14 @@ async def test_sensor_filters_delta(
|
||||
"Test Filter Max": "filter_max",
|
||||
"Test Filter Baseline Max": "filter_baseline_max",
|
||||
"Test Filter Zero Delta": "filter_zero_delta",
|
||||
"Test Filter Percentage": "filter_percentage",
|
||||
}
|
||||
buttons = {}
|
||||
for entity in entities:
|
||||
if isinstance(entity, ButtonInfo) and entity.name in button_name_map:
|
||||
buttons[button_name_map[entity.name]] = entity.key
|
||||
|
||||
assert len(buttons) == 4, f"Expected 3 buttons, found {len(buttons)}"
|
||||
assert len(buttons) == 5, f"Expected 5 buttons, found {len(buttons)}"
|
||||
|
||||
# Test 1: Min
|
||||
sensor_values["filter_min"].clear()
|
||||
@@ -161,3 +171,18 @@ async def test_sensor_filters_delta(
|
||||
assert sensor_values["filter_zero_delta"] == pytest.approx(expected), (
|
||||
f"Test 4 failed: expected {expected}, got {sensor_values['filter_zero_delta']}"
|
||||
)
|
||||
|
||||
# Test 5: Percentage (delta: 50%)
|
||||
sensor_values["filter_percentage"].clear()
|
||||
client.button_command(buttons["filter_percentage"])
|
||||
try:
|
||||
await asyncio.wait_for(filter_percentage_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Test 5 timed out. Values: {sensor_values['filter_percentage']}"
|
||||
)
|
||||
|
||||
expected = [100.0, 160.0, 250.0]
|
||||
assert sensor_values["filter_percentage"] == pytest.approx(expected), (
|
||||
f"Test 5 failed: expected {expected}, got {sensor_values['filter_percentage']}"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Integration test for 5-byte varint parsing of device_id fields.
|
||||
|
||||
Device IDs are FNV hashes (uint32) that frequently exceed 2^28 (268435456),
|
||||
requiring 5 varint bytes. This test verifies that:
|
||||
1. The firmware correctly decodes 5-byte varint device_id in incoming commands
|
||||
2. The firmware correctly encodes large device_id values in state responses
|
||||
3. Switch commands with large device_id reach the correct entity
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import EntityState, SwitchInfo, SwitchState
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_varint_five_byte_device_id(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that device_id values requiring 5-byte varints parse correctly."""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
device_info = await client.device_info()
|
||||
devices = device_info.devices
|
||||
assert len(devices) >= 2, f"Expected at least 2 devices, got {len(devices)}"
|
||||
|
||||
# Verify at least one device_id exceeds the 4-byte varint boundary (2^28)
|
||||
large_ids = [d for d in devices if d.device_id >= (1 << 28)]
|
||||
assert len(large_ids) > 0, (
|
||||
"Expected at least one device_id >= 2^28 to exercise 5-byte varint path. "
|
||||
f"Got device_ids: {[d.device_id for d in devices]}"
|
||||
)
|
||||
|
||||
# Get entities
|
||||
all_entities, _ = await client.list_entities_services()
|
||||
switch_entities = [e for e in all_entities if isinstance(e, SwitchInfo)]
|
||||
|
||||
# Find switches named "Device Switch" — one per sub-device
|
||||
device_switches = [e for e in switch_entities if e.name == "Device Switch"]
|
||||
assert len(device_switches) == 2, (
|
||||
f"Expected 2 'Device Switch' entities, got {len(device_switches)}"
|
||||
)
|
||||
|
||||
# Verify switches have different device_ids matching the sub-devices
|
||||
switch_device_ids = {s.device_id for s in device_switches}
|
||||
assert len(switch_device_ids) == 2, "Switches should have different device_ids"
|
||||
|
||||
# Subscribe to states and wait for initial states
|
||||
loop = asyncio.get_running_loop()
|
||||
states: dict[tuple[int, int], EntityState] = {}
|
||||
switch_futures: dict[tuple[int, int], asyncio.Future[EntityState]] = {}
|
||||
initial_done: asyncio.Future[bool] = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
key = (state.device_id, state.key)
|
||||
states[key] = state
|
||||
|
||||
if len(states) >= 3 and not initial_done.done():
|
||||
initial_done.set_result(True)
|
||||
|
||||
if initial_done.done() and key in switch_futures:
|
||||
fut = switch_futures[key]
|
||||
if not fut.done() and isinstance(state, SwitchState):
|
||||
fut.set_result(state)
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(initial_done, timeout=10.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timed out waiting for initial states. Got {len(states)} states"
|
||||
)
|
||||
|
||||
# Verify state responses contain correct large device_id values
|
||||
for device in devices:
|
||||
device_states = [
|
||||
s for (did, _), s in states.items() if did == device.device_id
|
||||
]
|
||||
assert len(device_states) > 0, (
|
||||
f"No states received for device '{device.name}' "
|
||||
f"(device_id={device.device_id})"
|
||||
)
|
||||
|
||||
# Test switch commands with large device_id varints —
|
||||
# this is the critical path: the client encodes device_id as a varint
|
||||
# in the SwitchCommandRequest, and the firmware must decode it correctly.
|
||||
for switch in device_switches:
|
||||
state_key = (switch.device_id, switch.key)
|
||||
|
||||
# Turn on
|
||||
switch_futures[state_key] = loop.create_future()
|
||||
client.switch_command(switch.key, True, device_id=switch.device_id)
|
||||
try:
|
||||
await asyncio.wait_for(switch_futures[state_key], timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timed out waiting for switch ON state "
|
||||
f"(device_id={switch.device_id}, key={switch.key}). "
|
||||
f"This likely means the firmware failed to decode the "
|
||||
f"5-byte varint device_id in SwitchCommandRequest."
|
||||
)
|
||||
assert states[state_key].state is True
|
||||
|
||||
# Turn off
|
||||
switch_futures[state_key] = loop.create_future()
|
||||
client.switch_command(switch.key, False, device_id=switch.device_id)
|
||||
try:
|
||||
await asyncio.wait_for(switch_futures[state_key], timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timed out waiting for switch OFF state "
|
||||
f"(device_id={switch.device_id}, key={switch.key})"
|
||||
)
|
||||
assert states[state_key].state is False
|
||||
Reference in New Issue
Block a user