1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-20 10:43:48 +01:00

Merge remote-tracking branch 'upstream/dev' into ci_impact_analysis

This commit is contained in:
J. Nick Koston
2025-10-17 13:19:47 -10:00
214 changed files with 4367 additions and 839 deletions

View File

@@ -1 +1 @@
049d60eed541730efaa4c0dc5d337b4287bf29b6daa350b5dfc1f23915f1c52f
d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248

View File

@@ -114,8 +114,7 @@ jobs:
matrix:
python-version:
- "3.11"
- "3.12"
- "3.13"
- "3.14"
os:
- ubuntu-latest
- macOS-latest
@@ -124,13 +123,9 @@ jobs:
# Minimize CI resource usage
# by only running the Python version
# version used for docker images on Windows and macOS
- python-version: "3.13"
- python-version: "3.14"
os: windows-latest
- python-version: "3.12"
os: windows-latest
- python-version: "3.13"
os: macOS-latest
- python-version: "3.12"
- python-version: "3.14"
os: macOS-latest
runs-on: ${{ matrix.os }}
needs:
@@ -178,6 +173,7 @@ jobs:
python-linters: ${{ steps.determine.outputs.python-linters }}
changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
component-test-count: ${{ steps.determine.outputs.component-test-count }}
memory_impact: ${{ steps.determine.outputs.memory-impact }}
steps:
@@ -207,6 +203,7 @@ jobs:
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
@@ -360,48 +357,13 @@ jobs:
# yamllint disable-line rule:line-length
if: always()
test-build-components:
name: Component test ${{ matrix.file }}
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 && fromJSON(needs.determine-jobs.outputs.component-test-count) < 100
strategy:
fail-fast: false
max-parallel: 2
matrix:
file: ${{ fromJson(needs.determine-jobs.outputs.changed-components-with-tests) }}
steps:
- name: Cache apt packages
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
with:
packages: libsdl2-dev
version: 1.0
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Validate config for ${{ matrix.file }}
run: |
. venv/bin/activate
python3 script/test_build_components.py -e config -c ${{ matrix.file }}
- name: Compile config for ${{ matrix.file }}
run: |
. venv/bin/activate
python3 script/test_build_components.py -e compile -c ${{ matrix.file }}
test-build-components-splitter:
name: Split components for intelligent grouping (40 weighted per batch)
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
outputs:
matrix: ${{ steps.split.outputs.components }}
steps:
@@ -420,8 +382,18 @@ jobs:
# Use intelligent splitter that groups components with same bus configs
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
# Only isolate directly changed components when targeting dev branch
# For beta/release branches, group everything for faster CI
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
directly_changed='[]'
echo "Target branch: ${{ github.base_ref }} - grouping all components"
else
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
fi
echo "Splitting components intelligently..."
output=$(python3 script/split_components_for_ci.py --components "$components" --batch-size 40 --output github)
output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
echo "$output" >> $GITHUB_OUTPUT
@@ -432,10 +404,10 @@ jobs:
- common
- determine-jobs
- test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
strategy:
fail-fast: false
max-parallel: ${{ (github.base_ref == 'beta' || github.base_ref == 'release') && 8 || 4 }}
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
matrix:
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
steps:
@@ -463,41 +435,80 @@ jobs:
- name: Validate and compile components with intelligent grouping
run: |
. venv/bin/activate
# Use /mnt for build files (70GB available vs ~29GB on /)
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
sudo mkdir -p /mnt/platformio
sudo chown $USER:$USER /mnt/platformio
mkdir -p ~/.platformio
sudo mount --bind /mnt/platformio ~/.platformio
# Bind mount test build directory to /mnt
sudo mkdir -p /mnt/test_build_components_build
sudo chown $USER:$USER /mnt/test_build_components_build
mkdir -p tests/test_build_components/build
sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
# Check if /mnt has more free space than / before bind mounting
# Extract available space in KB for comparison
root_avail=$(df -k / | awk 'NR==2 {print $4}')
mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}')
echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB"
# Only use /mnt if it has more space than /
if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then
echo "Using /mnt for build files (more space available)"
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
sudo mkdir -p /mnt/platformio
sudo chown $USER:$USER /mnt/platformio
mkdir -p ~/.platformio
sudo mount --bind /mnt/platformio ~/.platformio
# Bind mount test build directory to /mnt
sudo mkdir -p /mnt/test_build_components_build
sudo chown $USER:$USER /mnt/test_build_components_build
mkdir -p tests/test_build_components/build
sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
else
echo "Using / for build files (more space available than /mnt or /mnt unavailable)"
fi
# Convert space-separated components to comma-separated for Python script
components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
echo "Testing components: $components_csv"
# Only isolate directly changed components when targeting dev branch
# For beta/release branches, group everything for faster CI
#
# WHY ISOLATE DIRECTLY CHANGED COMPONENTS?
# - Isolated tests run WITHOUT --testing-mode, enabling full validation
# - This catches pin conflicts and other issues in directly changed code
# - Grouped tests use --testing-mode to allow config merging (disables some checks)
# - Dependencies are safe to group since they weren't modified in this PR
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
directly_changed_csv=""
echo "Testing components: $components_csv"
echo "Target branch: ${{ github.base_ref }} - grouping all components"
else
directly_changed_csv=$(echo '${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}' | jq -r 'join(",")')
echo "Testing components: $components_csv"
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components: $directly_changed_csv"
fi
echo ""
# Run config validation with grouping
python3 script/test_build_components.py -e config -c "$components_csv" -f
# Show disk space before validation (after bind mounts setup)
echo "Disk space before config validation:"
df -h
echo ""
# Run config validation with grouping and isolation
python3 script/test_build_components.py -e config -c "$components_csv" -f --isolate "$directly_changed_csv"
echo ""
echo "Config validation passed! Starting compilation..."
echo ""
# Run compilation with grouping
python3 script/test_build_components.py -e compile -c "$components_csv" -f
# Show disk space before compilation
echo "Disk space before compilation:"
df -h
echo ""
# Run compilation with grouping and isolation
python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv"
pre-commit-ci-lite:
name: pre-commit.ci lite
runs-on: ubuntu-latest
needs:
- common
if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -635,7 +646,6 @@ jobs:
- integration-tests
- clang-tidy
- determine-jobs
- test-build-components
- test-build-components-splitter
- test-build-components-split
- pre-commit-ci-lite

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with:
category: "/language:${{matrix.language}}"

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.0
rev: v0.14.1
hooks:
# Run the linter.
- id: ruff

View File

@@ -62,6 +62,7 @@ esphome/components/bedjet/fan/* @jhansche
esphome/components/bedjet/sensor/* @javawizard @jhansche
esphome/components/beken_spi_led_strip/* @Mat931
esphome/components/bh1750/* @OttoWinter
esphome/components/bh1900nux/* @B48D81EFCC
esphome/components/binary_sensor/* @esphome/core
esphome/components/bk72xx/* @kuba2k2
esphome/components/bl0906/* @athom-tech @jesserockz @tarontop

View File

@@ -117,6 +117,17 @@ class Purpose(StrEnum):
LOGGING = "logging"
class PortType(StrEnum):
SERIAL = "SERIAL"
NETWORK = "NETWORK"
MQTT = "MQTT"
MQTTIP = "MQTTIP"
# Magic MQTT port types that require special handling
_MQTT_PORT_TYPES = frozenset({PortType.MQTT, PortType.MQTTIP})
def _resolve_with_cache(address: str, purpose: Purpose) -> list[str]:
"""Resolve an address using cache if available, otherwise return the address itself."""
if CORE.address_cache and (cached := CORE.address_cache.get_addresses(address)):
@@ -280,16 +291,67 @@ def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str
return mqtt.get_esphome_device_ip(config, username, password, client_id)
_PORT_TO_PORT_TYPE = {
"MQTT": "MQTT",
"MQTTIP": "MQTTIP",
}
def _resolve_network_devices(
devices: list[str], config: ConfigType, args: ArgsProtocol
) -> list[str]:
"""Resolve device list, converting MQTT magic strings to actual IP addresses.
This function filters the devices list to:
- Replace MQTT/MQTTIP magic strings with actual IP addresses via MQTT lookup
- Deduplicate addresses while preserving order
- Only resolve MQTT once even if multiple MQTT strings are present
- If MQTT resolution fails, log a warning and continue with other devices
Args:
devices: List of device identifiers (IPs, hostnames, or magic strings)
config: ESPHome configuration
args: Command-line arguments containing MQTT credentials
Returns:
List of network addresses suitable for connection attempts
"""
network_devices: list[str] = []
mqtt_resolved: bool = False
for device in devices:
port_type = get_port_type(device)
if port_type in _MQTT_PORT_TYPES:
# Only resolve MQTT once, even if multiple MQTT entries
if not mqtt_resolved:
try:
mqtt_ips = mqtt_get_ip(
config, args.username, args.password, args.client_id
)
network_devices.extend(mqtt_ips)
except EsphomeError as err:
_LOGGER.warning(
"MQTT IP discovery failed (%s), will try other devices if available",
err,
)
mqtt_resolved = True
elif device not in network_devices:
# Regular network address or IP - add if not already present
network_devices.append(device)
return network_devices
def get_port_type(port: str) -> str:
def get_port_type(port: str) -> PortType:
"""Determine the type of port/device identifier.
Returns:
PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.)
PortType.MQTT for MQTT logging
PortType.MQTTIP for MQTT IP lookup
PortType.NETWORK for IP addresses, hostnames, or mDNS names
"""
if port.startswith("/") or port.startswith("COM"):
return "SERIAL"
return _PORT_TO_PORT_TYPE.get(port, "NETWORK")
return PortType.SERIAL
if port == "MQTT":
return PortType.MQTT
if port == "MQTTIP":
return PortType.MQTTIP
return PortType.NETWORK
def run_miniterm(config: ConfigType, port: str, args) -> int:
@@ -489,7 +551,7 @@ def upload_using_platformio(config: ConfigType, port: str):
def check_permissions(port: str):
if os.name == "posix" and get_port_type(port) == "SERIAL":
if os.name == "posix" and get_port_type(port) == PortType.SERIAL:
# Check if we can open selected serial port
if not os.access(port, os.F_OK):
raise EsphomeError(
@@ -517,7 +579,7 @@ def upload_program(
except AttributeError:
pass
if get_port_type(host) == "SERIAL":
if get_port_type(host) == PortType.SERIAL:
check_permissions(host)
exit_code = 1
@@ -544,17 +606,16 @@ def upload_program(
from esphome import espota2
remote_port = int(ota_conf[CONF_PORT])
password = ota_conf.get(CONF_PASSWORD, "")
password = ota_conf.get(CONF_PASSWORD)
if getattr(args, "file", None) is not None:
binary = Path(args.file)
else:
binary = CORE.firmware_bin
# MQTT address resolution
if get_port_type(host) in ("MQTT", "MQTTIP"):
devices = mqtt_get_ip(config, args.username, args.password, args.client_id)
# Resolve MQTT magic strings to actual IP addresses
network_devices = _resolve_network_devices(devices, config, args)
return espota2.run_ota(devices, remote_port, password, binary)
return espota2.run_ota(network_devices, remote_port, password, binary)
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
@@ -569,33 +630,22 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
raise EsphomeError("Logger is not configured!")
port = devices[0]
port_type = get_port_type(port)
if get_port_type(port) == "SERIAL":
if port_type == PortType.SERIAL:
check_permissions(port)
return run_miniterm(config, port, args)
port_type = get_port_type(port)
# Check if we should use API for logging
if has_api():
addresses_to_use: list[str] | None = None
# Resolve MQTT magic strings to actual IP addresses
if has_api() and (
network_devices := _resolve_network_devices(devices, config, args)
):
from esphome.components.api.client import run_logs
if port_type == "NETWORK":
# Network addresses (IPs, mDNS names, or regular DNS hostnames) can be used
# The resolve_ip_address() function in helpers.py handles all types
addresses_to_use = devices
elif port_type in ("MQTT", "MQTTIP") and has_mqtt_ip_lookup():
# Use MQTT IP lookup for MQTT/MQTTIP types
addresses_to_use = mqtt_get_ip(
config, args.username, args.password, args.client_id
)
return run_logs(config, network_devices)
if addresses_to_use is not None:
from esphome.components.api.client import run_logs
return run_logs(config, addresses_to_use)
if port_type in ("NETWORK", "MQTT") and has_mqtt_logging():
if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging():
from esphome import mqtt
return mqtt.show_logs(

View File

@@ -380,12 +380,19 @@ async def homeassistant_service_to_code(
var = cg.new_Pvariable(action_id, template_arg, serv, False)
templ = await cg.templatable(config[CONF_ACTION], args, None)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
@@ -458,15 +465,23 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg, serv, True)
templ = await cg.templatable(config[CONF_EVENT], args, None)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
return var
@@ -489,6 +504,8 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
cg.add(var.set_service("esphome.tag_scanned"))
# Initialize FixedVector with exact size (1 data field)
cg.add(var.init_data(1))
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
cg.add(var.add_data("tag_id", templ))
return var

View File

@@ -776,9 +776,9 @@ message HomeassistantActionRequest {
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
string service = 1;
repeated HomeassistantServiceMap data = 2;
repeated HomeassistantServiceMap data_template = 3;
repeated HomeassistantServiceMap variables = 4;
repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true];
repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true];
repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true];
bool is_event = 5;
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
@@ -866,7 +866,7 @@ message ListEntitiesServicesResponse {
string name = 1;
fixed32 key = 2;
repeated ListEntitiesServicesArgument args = 3;
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
}
message ExecuteServiceArgument {
option (ifdef) = "USE_API_SERVICES";
@@ -876,10 +876,10 @@ message ExecuteServiceArgument {
string string_ = 4;
// ESPHome 1.14 (api v1.3) make int a signed value
sint32 int_ = 5;
repeated bool bool_array = 6 [packed=false];
repeated sint32 int_array = 7 [packed=false];
repeated float float_array = 8 [packed=false];
repeated string string_array = 9;
repeated bool bool_array = 6 [packed=false, (fixed_vector) = true];
repeated sint32 int_array = 7 [packed=false, (fixed_vector) = true];
repeated float float_array = 8 [packed=false, (fixed_vector) = true];
repeated string string_array = 9 [(fixed_vector) = true];
}
message ExecuteServiceRequest {
option (id) = 42;
@@ -888,7 +888,7 @@ message ExecuteServiceRequest {
option (ifdef) = "USE_API_SERVICES";
fixed32 key = 1;
repeated ExecuteServiceArgument args = 2;
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
}
// ==================== CAMERA ====================
@@ -987,8 +987,8 @@ message ListEntitiesClimateResponse {
string name = 3;
reserved 4; // Deprecated: was string unique_id
bool supports_current_temperature = 5;
bool supports_two_point_target_temperature = 6;
bool supports_current_temperature = 5; // Deprecated: use feature_flags
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"];
float visual_min_temperature = 8;
float visual_max_temperature = 9;
@@ -997,7 +997,7 @@ message ListEntitiesClimateResponse {
// is if CLIMATE_PRESET_AWAY exists is supported_presets
// Deprecated in API version 1.5
bool legacy_supports_away = 11 [deprecated=true];
bool supports_action = 12;
bool supports_action = 12; // Deprecated: use feature_flags
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"];
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"];
repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"];
@@ -1007,11 +1007,12 @@ message ListEntitiesClimateResponse {
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20;
float visual_current_temperature_step = 21;
bool supports_current_humidity = 22;
bool supports_target_humidity = 23;
bool supports_current_humidity = 22; // Deprecated: use feature_flags
bool supports_target_humidity = 23; // Deprecated: use feature_flags
float visual_min_humidity = 24;
float visual_max_humidity = 25;
uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
uint32 feature_flags = 27;
}
message ClimateStateResponse {
option (id) = 47;

View File

@@ -27,6 +27,9 @@
#ifdef USE_BLUETOOTH_PROXY
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
#endif
#ifdef USE_CLIMATE
#include "esphome/components/climate/climate_mode.h"
#endif
#ifdef USE_VOICE_ASSISTANT
#include "esphome/components/voice_assistant/voice_assistant.h"
#endif
@@ -623,9 +626,10 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
auto traits = climate->get_traits();
resp.mode = static_cast<enums::ClimateMode>(climate->mode);
resp.action = static_cast<enums::ClimateAction>(climate->action);
if (traits.get_supports_current_temperature())
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE))
resp.current_temperature = climate->current_temperature;
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
resp.target_temperature_low = climate->target_temperature_low;
resp.target_temperature_high = climate->target_temperature_high;
} else {
@@ -644,9 +648,9 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
}
if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
if (traits.get_supports_current_humidity())
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY))
resp.current_humidity = climate->current_humidity;
if (traits.get_supports_target_humidity())
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY))
resp.target_humidity = climate->target_humidity;
return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size,
is_single);
@@ -656,10 +660,14 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
auto *climate = static_cast<climate::Climate *>(entity);
ListEntitiesClimateResponse msg;
auto traits = climate->get_traits();
// Flags set for backward compatibility, deprecated in 2025.11.0
msg.supports_current_temperature = traits.get_supports_current_temperature();
msg.supports_current_humidity = traits.get_supports_current_humidity();
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
msg.supports_target_humidity = traits.get_supports_target_humidity();
msg.supports_action = traits.get_supports_action();
// Current feature flags and other supported parameters
msg.feature_flags = traits.get_feature_flags();
msg.supported_modes = &traits.get_supported_modes_for_api_();
msg.visual_min_temperature = traits.get_visual_min_temperature();
msg.visual_max_temperature = traits.get_visual_max_temperature();
@@ -667,7 +675,6 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.supports_action = traits.get_supports_action();
msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
msg.supported_presets = &traits.get_supported_presets_for_api_();
@@ -1406,7 +1413,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
HelloResponse resp;
resp.api_version_major = 1;
resp.api_version_minor = 12;
resp.api_version_minor = 13;
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
resp.set_server_info(ESPHOME_VERSION_REF);
resp.set_name(StringRef(App.get_name()));

View File

@@ -242,7 +242,6 @@ APIError APINoiseFrameHelper::state_action_() {
const std::string &name = App.get_name();
const std::string &mac = get_mac_address();
std::vector<uint8_t> msg;
// Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator
size_t mac_len = mac.size() + 1; // including null terminator
@@ -250,17 +249,17 @@ APIError APINoiseFrameHelper::state_action_() {
size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + mac_len;
msg.resize(total_size);
auto msg = std::make_unique<uint8_t[]>(total_size);
// chosen proto
msg[0] = 0x01;
// node name, terminated by null byte
std::memcpy(msg.data() + name_offset, name.c_str(), name_len);
std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte
std::memcpy(msg.data() + mac_offset, mac.c_str(), mac_len);
std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len);
aerr = write_frame_(msg.data(), msg.size());
aerr = write_frame_(msg.get(), total_size);
if (aerr != APIError::OK)
return aerr;
@@ -339,32 +338,32 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
#ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
std::vector<uint8_t> data;
data.resize(reason_len + 1);
size_t data_size = reason_len + 1;
auto data = std::make_unique<uint8_t[]>(data_size);
data[0] = 0x01; // failure
// Copy error message from PROGMEM
if (reason_len > 0) {
memcpy_P(data.data() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
memcpy_P(data.get() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
}
#else
// Normal memory access
const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str);
std::vector<uint8_t> data;
data.resize(reason_len + 1);
size_t data_size = reason_len + 1;
auto data = std::make_unique<uint8_t[]>(data_size);
data[0] = 0x01; // failure
// Copy error message in bulk
if (reason_len > 0) {
std::memcpy(data.data() + 1, reason_str, reason_len);
std::memcpy(data.get() + 1, reason_str, reason_len);
}
#endif
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
write_frame_(data.data(), data.size());
write_frame_(data.get(), data_size);
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {

View File

@@ -1064,6 +1064,17 @@ bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, Proto32Bit value) {
}
return true;
}
void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
uint32_t count_bool_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 6);
this->bool_array.init(count_bool_array);
uint32_t count_int_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 7);
this->int_array.init(count_int_array);
uint32_t count_float_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 8);
this->float_array.init(count_float_array);
uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9);
this->string_array.init(count_string_array);
ProtoDecodableMessage::decode(buffer, length);
}
bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2:
@@ -1085,6 +1096,11 @@ bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
}
return true;
}
void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
uint32_t count_args = ProtoDecodableMessage::count_repeated_field(buffer, length, 2);
this->args.init(count_args);
ProtoDecodableMessage::decode(buffer, length);
}
#endif
#ifdef USE_CAMERA
void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
@@ -1185,6 +1201,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
#ifdef USE_DEVICES
buffer.encode_uint32(26, this->device_id);
#endif
buffer.encode_uint32(27, this->feature_flags);
}
void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->object_id_ref_.size());
@@ -1239,6 +1256,7 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
#ifdef USE_DEVICES
size.add_uint32(2, this->device_id);
#endif
size.add_uint32(2, this->feature_flags);
}
void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);

View File

@@ -1110,9 +1110,9 @@ class HomeassistantActionRequest final : public ProtoMessage {
#endif
StringRef service_ref_{};
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
std::vector<HomeassistantServiceMap> data{};
std::vector<HomeassistantServiceMap> data_template{};
std::vector<HomeassistantServiceMap> variables{};
FixedVector<HomeassistantServiceMap> data{};
FixedVector<HomeassistantServiceMap> data_template{};
FixedVector<HomeassistantServiceMap> variables{};
bool is_event{false};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
uint32_t call_id{0};
@@ -1263,7 +1263,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
uint32_t key{0};
std::vector<ListEntitiesServicesArgument> args{};
FixedVector<ListEntitiesServicesArgument> args{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1279,10 +1279,11 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
float float_{0.0f};
std::string string_{};
int32_t int_{0};
std::vector<bool> bool_array{};
std::vector<int32_t> int_array{};
std::vector<float> float_array{};
std::vector<std::string> string_array{};
FixedVector<bool> bool_array{};
FixedVector<int32_t> int_array{};
FixedVector<float> float_array{};
FixedVector<std::string> string_array{};
void decode(const uint8_t *buffer, size_t length) override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1300,7 +1301,8 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage {
const char *message_name() const override { return "execute_service_request"; }
#endif
uint32_t key{0};
std::vector<ExecuteServiceArgument> args{};
FixedVector<ExecuteServiceArgument> args{};
void decode(const uint8_t *buffer, size_t length) override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1369,7 +1371,7 @@ class CameraImageRequest final : public ProtoDecodableMessage {
class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 46;
static constexpr uint8_t ESTIMATED_SIZE = 145;
static constexpr uint8_t ESTIMATED_SIZE = 150;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_climate_response"; }
#endif
@@ -1390,6 +1392,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
bool supports_target_humidity{false};
float visual_min_humidity{0.0f};
float visual_max_humidity{0.0f};
uint32_t feature_flags{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP

View File

@@ -1292,6 +1292,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "feature_flags", this->feature_flags);
}
void ClimateStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ClimateStateResponse");

View File

@@ -201,9 +201,9 @@ class CustomAPIDevice {
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantActionRequest resp;
resp.set_service(StringRef(service_name));
resp.data.init(data.size());
for (auto &it : data) {
resp.data.emplace_back();
auto &kv = resp.data.back();
auto &kv = resp.data.emplace_back();
kv.set_key(StringRef(it.first));
kv.value = it.second;
}
@@ -244,9 +244,9 @@ class CustomAPIDevice {
HomeassistantActionRequest resp;
resp.set_service(StringRef(service_name));
resp.is_event = true;
resp.data.init(data.size());
for (auto &it : data) {
resp.data.emplace_back();
auto &kv = resp.data.back();
auto &kv = resp.data.emplace_back();
kv.set_key(StringRef(it.first));
kv.value = it.second;
}

View File

@@ -41,10 +41,14 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
template<typename... Ts> class TemplatableKeyValuePair {
public:
// Default constructor needed for FixedVector::emplace_back()
TemplatableKeyValuePair() = default;
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
std::string key;
TemplatableStringValue<Ts...> value;
};
@@ -93,15 +97,22 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
template<typename T> void set_service(T service) { this->service_ = service; }
// Initialize FixedVector members - called from Python codegen with compile-time known sizes.
// Must be called before any add_* methods; capacity must match the number of subsequent add_* calls.
void init_data(size_t count) { this->data_.init(count); }
void init_data_template(size_t count) { this->data_template_.init(count); }
void init_variables(size_t count) { this->variables_.init(count); }
// Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
// The value parameter can be a lambda/template, but keys are never templatable.
// Using pass-by-value allows the compiler to optimize for both lvalues and rvalues.
template<typename T> void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); }
template<typename T> void add_data_template(std::string key, T value) {
this->data_template_.emplace_back(std::move(key), value);
template<typename K, typename V> void add_data(K &&key, V &&value) {
this->add_kv_(this->data_, std::forward<K>(key), std::forward<V>(value));
}
template<typename T> void add_variable(std::string key, T value) {
this->variables_.emplace_back(std::move(key), value);
template<typename K, typename V> void add_data_template(K &&key, V &&value) {
this->add_kv_(this->data_template_, std::forward<K>(key), std::forward<V>(value));
}
template<typename K, typename V> void add_variable(K &&key, V &&value) {
this->add_kv_(this->variables_, std::forward<K>(key), std::forward<V>(value));
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -127,24 +138,9 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value));
resp.is_event = this->flags_.is_event;
for (auto &it : this->data_) {
resp.data.emplace_back();
auto &kv = resp.data.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
for (auto &it : this->data_template_) {
resp.data_template.emplace_back();
auto &kv = resp.data_template.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
for (auto &it : this->variables_) {
resp.variables.emplace_back();
auto &kv = resp.variables.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
this->populate_service_map(resp.data, this->data_, x...);
this->populate_service_map(resp.data_template, this->data_template_, x...);
this->populate_service_map(resp.variables, this->variables_, x...);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
if (this->flags_.wants_status) {
@@ -189,11 +185,28 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
}
protected:
// Helper to add key-value pairs to FixedVectors with perfect forwarding to avoid copies
template<typename K, typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, K &&key, V &&value) {
auto &kv = vec.emplace_back();
kv.key = std::forward<K>(key);
kv.value = std::forward<V>(value);
}
template<typename VectorType, typename SourceType>
static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) {
dest.init(source.size());
for (auto &it : source) {
auto &kv = dest.emplace_back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
}
APIServer *parent_;
TemplatableStringValue<Ts...> service_{};
std::vector<TemplatableKeyValuePair<Ts...>> data_;
std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
FixedVector<TemplatableKeyValuePair<Ts...>> data_;
FixedVector<TemplatableKeyValuePair<Ts...>> data_template_;
FixedVector<TemplatableKeyValuePair<Ts...>> variables_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
TemplatableStringValue<Ts...> response_template_{""};

View File

@@ -7,6 +7,69 @@ namespace esphome::api {
static const char *const TAG = "api.proto";
uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) {
uint32_t count = 0;
const uint8_t *ptr = buffer;
const uint8_t *end = buffer + length;
while (ptr < end) {
uint32_t consumed;
// Parse field header (tag)
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
break; // Invalid data, stop counting
}
uint32_t tag = res->as_uint32();
uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3;
ptr += consumed;
// Count if this is the target field
if (field_id == target_field_id) {
count++;
}
// Skip field data based on wire type
switch (field_type) {
case WIRE_TYPE_VARINT: { // VarInt - parse and skip
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
return count; // Invalid data, return what we have
}
ptr += consumed;
break;
}
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
return count;
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (ptr + field_length > end) {
return count; // Out of bounds
}
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
if (ptr + 4 > end) {
return count;
}
ptr += 4;
break;
}
default:
// Unknown wire type, can't continue
return count;
}
}
return count;
}
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
const uint8_t *ptr = buffer;
const uint8_t *end = buffer + length;
@@ -22,12 +85,12 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
}
uint32_t tag = res->as_uint32();
uint32_t field_type = tag & 0b111;
uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3;
ptr += consumed;
switch (field_type) {
case 0: { // VarInt
case WIRE_TYPE_VARINT: { // VarInt
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
@@ -39,7 +102,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
ptr += consumed;
break;
}
case 2: { // Length-delimited
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
@@ -57,7 +120,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
ptr += field_length;
break;
}
case 5: { // 32-bit
case WIRE_TYPE_FIXED32: { // 32-bit
if (ptr + 4 > end) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;

View File

@@ -15,6 +15,13 @@
namespace esphome::api {
// Protocol Buffer wire type constants
// See https://protobuf.dev/programming-guides/encoding/#structure
constexpr uint8_t WIRE_TYPE_VARINT = 0; // int32, int64, uint32, uint64, sint32, sint64, bool, enum
constexpr uint8_t WIRE_TYPE_LENGTH_DELIMITED = 2; // string, bytes, embedded messages, packed repeated fields
constexpr uint8_t WIRE_TYPE_FIXED32 = 5; // fixed32, sfixed32, float
constexpr uint8_t WIRE_TYPE_MASK = 0b111; // Mask to extract wire type from tag
// Helper functions for ZigZag encoding/decoding
inline constexpr uint32_t encode_zigzag32(int32_t value) {
return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
@@ -241,7 +248,7 @@ class ProtoWriteBuffer {
* Following https://protobuf.dev/programming-guides/encoding/#structure
*/
void encode_field_raw(uint32_t field_id, uint32_t type) {
uint32_t val = (field_id << 3) | (type & 0b111);
uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK);
this->encode_varint_raw(val);
}
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
@@ -354,7 +361,18 @@ class ProtoMessage {
// Base class for messages that support decoding
class ProtoDecodableMessage : public ProtoMessage {
public:
void decode(const uint8_t *buffer, size_t length);
virtual void decode(const uint8_t *buffer, size_t length);
/**
* Count occurrences of a repeated field in a protobuf buffer.
* This is a lightweight scan that only parses tags and skips field data.
*
* @param buffer Pointer to the protobuf buffer
* @param length Length of the buffer in bytes
* @param target_field_id The field ID to count
* @return Number of times the field appears in the buffer
*/
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
protected:
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
@@ -482,7 +500,7 @@ class ProtoSize {
* @return The number of bytes needed to encode the field ID and wire type
*/
static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
uint32_t tag = (field_id << 3) | (type & 0b111);
uint32_t tag = (field_id << 3) | (type & WIRE_TYPE_MASK);
return varint(tag);
}

View File

@@ -12,16 +12,16 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
return arg.bool_array;
return std::vector<bool>(arg.bool_array.begin(), arg.bool_array.end());
}
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
return arg.int_array;
return std::vector<int32_t>(arg.int_array.begin(), arg.int_array.end());
}
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
return arg.float_array;
return std::vector<float>(arg.float_array.begin(), arg.float_array.end());
}
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
return arg.string_array;
return std::vector<std::string>(arg.string_array.begin(), arg.string_array.end());
}
template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; }

View File

@@ -35,9 +35,9 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
msg.set_name(StringRef(this->name_));
msg.key = this->key_;
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
msg.args.init(sizeof...(Ts));
for (size_t i = 0; i < sizeof...(Ts); i++) {
msg.args.emplace_back();
auto &arg = msg.args.back();
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.set_name(StringRef(this->arg_names_[i]));
}
@@ -55,7 +55,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
protected:
virtual void execute(Ts... x) = 0;
template<int... S> void execute_(const std::vector<ExecuteServiceArgument> &args, seq<S...> type) {
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...);
}

View File

View File

@@ -0,0 +1,54 @@
#include "esphome/core/log.h"
#include "bh1900nux.h"
namespace esphome {
namespace bh1900nux {
static const char *const TAG = "bh1900nux.sensor";
// I2C Registers
static const uint8_t TEMPERATURE_REG = 0x00;
static const uint8_t CONFIG_REG = 0x01; // Not used and supported yet
static const uint8_t TEMPERATURE_LOW_REG = 0x02; // Not used and supported yet
static const uint8_t TEMPERATURE_HIGH_REG = 0x03; // Not used and supported yet
static const uint8_t SOFT_RESET_REG = 0x04;
// I2C Command payloads
static const uint8_t SOFT_RESET_PAYLOAD = 0x01; // Soft Reset value
static const float SENSOR_RESOLUTION = 0.0625f; // Sensor resolution per bit in degrees celsius
void BH1900NUXSensor::setup() {
// Initialize I2C device
i2c::ErrorCode result_code =
this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication
if (result_code != i2c::ERROR_OK) {
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
return;
}
}
void BH1900NUXSensor::update() {
uint8_t temperature_raw[2];
if (this->read_register(TEMPERATURE_REG, temperature_raw, 2) != i2c::ERROR_OK) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
return;
}
// Combined raw value, unsigned and unaligned 16 bit
// Temperature is represented in just 12 bits, shift needed
int16_t raw_temperature_register_value = encode_uint16(temperature_raw[0], temperature_raw[1]);
raw_temperature_register_value >>= 4;
float temperature_value = raw_temperature_register_value * SENSOR_RESOLUTION; // Apply sensor resolution
this->publish_state(temperature_value);
}
void BH1900NUXSensor::dump_config() {
LOG_SENSOR("", "BH1900NUX", this);
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
}
} // namespace bh1900nux
} // namespace esphome

View File

@@ -0,0 +1,18 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace bh1900nux {
class BH1900NUXSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void update() override;
void dump_config() override;
};
} // namespace bh1900nux
} // namespace esphome

View File

@@ -0,0 +1,34 @@
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
DEPENDENCIES = ["i2c"]
CODEOWNERS = ["@B48D81EFCC"]
sensor_ns = cg.esphome_ns.namespace("bh1900nux")
BH1900NUXSensor = sensor_ns.class_(
"BH1900NUXSensor", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
BH1900NUXSensor,
accuracy_decimals=1,
unit_of_measurement=UNIT_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x48))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

View File

@@ -16,7 +16,9 @@
#include "bluetooth_connection.h"
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <esp_bt_device.h>
namespace esphome::bluetooth_proxy {

View File

@@ -8,17 +8,30 @@ namespace cap1188 {
static const char *const TAG = "cap1188";
void CAP1188Component::setup() {
// Reset device using the reset pin
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
delay(100); // NOLINT
this->reset_pin_->digital_write(true);
delay(100); // NOLINT
this->reset_pin_->digital_write(false);
delay(100); // NOLINT
this->disable_loop();
// no reset pin
if (this->reset_pin_ == nullptr) {
this->finish_setup_();
return;
}
// reset pin configured so reset before finishing setup
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
// delay after reset pin write
this->set_timeout(100, [this]() {
this->reset_pin_->digital_write(true);
// delay after reset pin write
this->set_timeout(100, [this]() {
this->reset_pin_->digital_write(false);
// delay after reset pin write
this->set_timeout(100, [this]() { this->finish_setup_(); });
});
});
}
void CAP1188Component::finish_setup_() {
// Check if CAP1188 is actually connected
this->read_byte(CAP1188_PRODUCT_ID, &this->cap1188_product_id_);
this->read_byte(CAP1188_MANUFACTURE_ID, &this->cap1188_manufacture_id_);
@@ -44,6 +57,9 @@ void CAP1188Component::setup() {
// Speed up a bit
this->write_byte(CAP1188_STAND_BY_CONFIGURATION, 0x30);
// Setup successful, so enable loop
this->enable_loop();
}
void CAP1188Component::dump_config() {

View File

@@ -49,6 +49,8 @@ class CAP1188Component : public Component, public i2c::I2CDevice {
void loop() override;
protected:
void finish_setup_();
std::vector<CAP1188Channel *> channels_{};
uint8_t touch_threshold_{0x20};
uint8_t allow_multiple_touches_{0x80};

View File

@@ -96,7 +96,8 @@ void ClimateCall::validate_() {
}
if (this->target_temperature_.has_value()) {
auto target = *this->target_temperature_;
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGW(TAG, " Cannot set target temperature for climate device "
"with two-point target temperature!");
this->target_temperature_.reset();
@@ -106,7 +107,8 @@ void ClimateCall::validate_() {
}
}
if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) {
if (!traits.get_supports_two_point_target_temperature()) {
if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGW(TAG, " Cannot set low/high target temperature for this device!");
this->target_temperature_low_.reset();
this->target_temperature_high_.reset();
@@ -350,13 +352,14 @@ void Climate::save_state_() {
state.mode = this->mode;
auto traits = this->get_traits();
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
state.target_temperature_low = this->target_temperature_low;
state.target_temperature_high = this->target_temperature_high;
} else {
state.target_temperature = this->target_temperature;
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
state.target_humidity = this->target_humidity;
}
if (traits.get_supports_fan_modes() && fan_mode.has_value()) {
@@ -400,7 +403,7 @@ void Climate::publish_state() {
auto traits = this->get_traits();
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode)));
if (traits.get_supports_action()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
ESP_LOGD(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action)));
}
if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
@@ -418,19 +421,20 @@ void Climate::publish_state() {
if (traits.get_supports_swing_modes()) {
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
}
if (traits.get_supports_current_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature);
}
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low,
this->target_temperature_high);
} else {
ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature);
}
if (traits.get_supports_current_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity);
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity);
}
@@ -485,13 +489,14 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
auto call = climate->make_call();
auto traits = climate->get_traits();
call.set_mode(this->mode);
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
call.set_target_temperature_low(this->target_temperature_low);
call.set_target_temperature_high(this->target_temperature_high);
} else {
call.set_target_temperature(this->target_temperature);
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
call.set_target_humidity(this->target_humidity);
}
if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
@@ -508,13 +513,14 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
void ClimateDeviceRestoreState::apply(Climate *climate) {
auto traits = climate->get_traits();
climate->mode = this->mode;
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
climate->target_temperature_low = this->target_temperature_low;
climate->target_temperature_high = this->target_temperature_high;
} else {
climate->target_temperature = this->target_temperature;
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
climate->target_humidity = this->target_humidity;
}
if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
@@ -580,28 +586,30 @@ void Climate::dump_traits_(const char *tag) {
" Target: %.1f",
traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
traits.get_visual_target_temperature_step());
if (traits.get_supports_current_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
}
if (traits.get_supports_target_humidity() || traits.get_supports_current_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY |
climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGCONFIG(tag,
" - Min humidity: %.0f\n"
" - Max humidity: %.0f",
traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
}
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
}
if (traits.get_supports_current_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
ESP_LOGCONFIG(tag, " [x] Supports target humidity");
}
if (traits.get_supports_current_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
}
if (traits.get_supports_action()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
ESP_LOGCONFIG(tag, " [x] Supports action");
}
if (!traits.get_supported_modes().empty()) {

View File

@@ -98,6 +98,21 @@ enum ClimatePreset : uint8_t {
CLIMATE_PRESET_ACTIVITY = 7,
};
enum ClimateFeature : uint32_t {
// Reporting current temperature is supported
CLIMATE_SUPPORTS_CURRENT_TEMPERATURE = 1 << 0,
// Setting two target temperatures is supported (used in conjunction with CLIMATE_MODE_HEAT_COOL)
CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE = 1 << 1,
// Single-point mode is NOT supported (UI always displays two handles, setting 'target_temperature' is not supported)
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE = 1 << 2,
// Reporting current humidity is supported
CLIMATE_SUPPORTS_CURRENT_HUMIDITY = 1 << 3,
// Setting a target humidity is supported
CLIMATE_SUPPORTS_TARGET_HUMIDITY = 1 << 4,
// Reporting current climate action is supported
CLIMATE_SUPPORTS_ACTION = 1 << 5,
};
/// Convert the given ClimateMode to a human-readable string.
const LogString *climate_mode_to_string(ClimateMode mode);

View File

@@ -21,48 +21,92 @@ namespace climate {
* - Target Temperature
*
* All other properties and modes are optional and the integration must mark
* each of them as supported by setting the appropriate flag here.
* each of them as supported by setting the appropriate flag(s) here.
*
* - supports current temperature - if the climate device supports reporting a current temperature
* - supports two point target temperature - if the climate device's target temperature should be
* split in target_temperature_low and target_temperature_high instead of just the single target_temperature
* - feature flags: see ClimateFeatures enum in climate_mode.h
* - supports modes:
* - auto mode (automatic control)
* - cool mode (lowers current temperature)
* - heat mode (increases current temperature)
* - dry mode (removes humidity from air)
* - fan mode (only turns on fan)
* - supports action - if the climate device supports reporting the active
* current action of the device with the action property.
* - supports fan modes - optionally, if it has a fan which can be configured in different ways:
* - on, off, auto, high, medium, low, middle, focus, diffuse, quiet
* - supports swing modes - optionally, if it has a swing which can be configured in different ways:
* - off, both, vertical, horizontal
*
* This class also contains static data for the climate device display:
* - visual min/max temperature - tells the frontend what range of temperatures the climate device
* should display (gauge min/max values)
* - visual min/max temperature/humidity - tells the frontend what range of temperature/humidity the
* climate device should display (gauge min/max values)
* - temperature step - the step with which to increase/decrease target temperature.
* This also affects with how many decimal places the temperature is shown
*/
class ClimateTraits {
public:
bool get_supports_current_temperature() const { return this->supports_current_temperature_; }
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
uint32_t get_feature_flags() const { return this->feature_flags_; }
void add_feature_flags(uint32_t feature_flags) { this->feature_flags_ |= feature_flags; }
void clear_feature_flags(uint32_t feature_flags) { this->feature_flags_ &= ~feature_flags; }
bool has_feature_flags(uint32_t feature_flags) const { return this->feature_flags_ & feature_flags; }
void set_feature_flags(uint32_t feature_flags) { this->feature_flags_ = feature_flags; }
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_current_temperature() const {
return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
}
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_current_temperature(bool supports_current_temperature) {
this->supports_current_temperature_ = supports_current_temperature;
if (supports_current_temperature) {
this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
} else {
this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
}
}
bool get_supports_current_humidity() const { return this->supports_current_humidity_; }
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_current_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY); }
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_current_humidity(bool supports_current_humidity) {
this->supports_current_humidity_ = supports_current_humidity;
if (supports_current_humidity) {
this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
} else {
this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
}
}
bool get_supports_two_point_target_temperature() const { return this->supports_two_point_target_temperature_; }
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_two_point_target_temperature() const {
return this->has_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
}
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) {
this->supports_two_point_target_temperature_ = supports_two_point_target_temperature;
if (supports_two_point_target_temperature)
// Use CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE to mimic previous behavior
{
this->add_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
} else {
this->clear_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
}
}
bool get_supports_target_humidity() const { return this->supports_target_humidity_; }
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_target_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY); }
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_target_humidity(bool supports_target_humidity) {
this->supports_target_humidity_ = supports_target_humidity;
if (supports_target_humidity) {
this->add_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY);
} else {
this->clear_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY);
}
}
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_action() const { return this->has_feature_flags(CLIMATE_SUPPORTS_ACTION); }
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_action(bool supports_action) {
if (supports_action) {
this->add_feature_flags(CLIMATE_SUPPORTS_ACTION);
} else {
this->clear_feature_flags(CLIMATE_SUPPORTS_ACTION);
}
}
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
@@ -82,9 +126,6 @@ class ClimateTraits {
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
void set_supports_action(bool supports_action) { this->supports_action_ = supports_action; }
bool get_supports_action() const { return this->supports_action_; }
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
@@ -219,24 +260,20 @@ class ClimateTraits {
}
}
bool supports_current_temperature_{false};
bool supports_current_humidity_{false};
bool supports_two_point_target_temperature_{false};
bool supports_target_humidity_{false};
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
bool supports_action_{false};
std::set<climate::ClimateFanMode> supported_fan_modes_;
std::set<climate::ClimateSwingMode> supported_swing_modes_;
std::set<climate::ClimatePreset> supported_presets_;
std::set<std::string> supported_custom_fan_modes_;
std::set<std::string> supported_custom_presets_;
uint32_t feature_flags_{0};
float visual_min_temperature_{10};
float visual_max_temperature_{30};
float visual_target_temperature_step_{0.1};
float visual_current_temperature_step_{0.1};
float visual_min_humidity_{30};
float visual_max_humidity_{99};
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
std::set<climate::ClimateFanMode> supported_fan_modes_;
std::set<climate::ClimateSwingMode> supported_swing_modes_;
std::set<climate::ClimatePreset> supported_presets_;
std::set<std::string> supported_custom_fan_modes_;
std::set<std::string> supported_custom_presets_;
};
} // namespace climate

View File

@@ -30,14 +30,12 @@ class DateTimeBase : public EntityBase {
#endif
};
#ifdef USE_TIME
class DateTimeStateTrigger : public Trigger<ESPTime> {
public:
explicit DateTimeStateTrigger(DateTimeBase *parent) {
parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); });
}
};
#endif
} // namespace datetime
} // namespace esphome

View File

@@ -11,8 +11,6 @@
#include <esp_chip_info.h>
#include <esp_partition.h>
#include <map>
#ifdef USE_ARDUINO
#include <Esp.h>
#endif
@@ -125,7 +123,12 @@ void DebugComponent::log_partition_info_() {
uint32_t DebugComponent::get_free_heap_() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); }
static const std::map<int, const char *> CHIP_FEATURES = {
struct ChipFeature {
int bit;
const char *name;
};
static constexpr ChipFeature CHIP_FEATURES[] = {
{CHIP_FEATURE_BLE, "BLE"},
{CHIP_FEATURE_BT, "BT"},
{CHIP_FEATURE_EMB_FLASH, "EMB Flash"},
@@ -170,11 +173,13 @@ void DebugComponent::get_device_info_(std::string &device_info) {
esp_chip_info(&info);
const char *model = ESPHOME_VARIANT;
std::string features;
for (auto feature : CHIP_FEATURES) {
if (info.features & feature.first) {
features += feature.second;
// Check each known feature bit
for (const auto &feature : CHIP_FEATURES) {
if (info.features & feature.bit) {
features += feature.name;
features += ", ";
info.features &= ~feature.first;
info.features &= ~feature.bit;
}
}
if (info.features != 0)

View File

@@ -25,10 +25,37 @@ static void show_reset_reason(std::string &reset_reason, bool set, const char *r
reset_reason += reason;
}
inline uint32_t read_mem_u32(uintptr_t addr) {
static inline uint32_t read_mem_u32(uintptr_t addr) {
return *reinterpret_cast<volatile uint32_t *>(addr); // NOLINT(performance-no-int-to-ptr)
}
static inline uint8_t read_mem_u8(uintptr_t addr) {
return *reinterpret_cast<volatile uint8_t *>(addr); // NOLINT(performance-no-int-to-ptr)
}
// defines from https://github.com/adafruit/Adafruit_nRF52_Bootloader which prints those information
constexpr uint32_t SD_MAGIC_NUMBER = 0x51B1E5DB;
constexpr uintptr_t MBR_SIZE = 0x1000;
constexpr uintptr_t SOFTDEVICE_INFO_STRUCT_OFFSET = 0x2000;
constexpr uintptr_t SD_ID_OFFSET = SOFTDEVICE_INFO_STRUCT_OFFSET + 0x10;
constexpr uintptr_t SD_VERSION_OFFSET = SOFTDEVICE_INFO_STRUCT_OFFSET + 0x14;
static inline bool is_sd_present() {
return read_mem_u32(SOFTDEVICE_INFO_STRUCT_OFFSET + MBR_SIZE + 4) == SD_MAGIC_NUMBER;
}
static inline uint32_t sd_id_get() {
if (read_mem_u8(MBR_SIZE + SOFTDEVICE_INFO_STRUCT_OFFSET) > (SD_ID_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) {
return read_mem_u32(MBR_SIZE + SD_ID_OFFSET);
}
return 0;
}
static inline uint32_t sd_version_get() {
if (read_mem_u8(MBR_SIZE + SOFTDEVICE_INFO_STRUCT_OFFSET) > (SD_VERSION_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) {
return read_mem_u32(MBR_SIZE + SD_VERSION_OFFSET);
}
return 0;
}
std::string DebugComponent::get_reset_reason_() {
uint32_t cause;
auto ret = hwinfo_get_reset_cause(&cause);
@@ -271,6 +298,29 @@ void DebugComponent::get_device_info_(std::string &device_info) {
NRF_UICR->NRFFW[0]);
ESP_LOGD(TAG, "MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR),
NRF_UICR->NRFFW[1]);
if (is_sd_present()) {
uint32_t const sd_id = sd_id_get();
uint32_t const sd_version = sd_version_get();
uint32_t ver[3];
ver[0] = sd_version / 1000000;
ver[1] = (sd_version - ver[0] * 1000000) / 1000;
ver[2] = (sd_version - ver[0] * 1000000 - ver[1] * 1000);
ESP_LOGD(TAG, "SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]);
#ifdef USE_SOFTDEVICE_ID
#ifdef USE_SOFTDEVICE_VERSION
if (USE_SOFTDEVICE_ID != sd_id || USE_SOFTDEVICE_VERSION != ver[0]) {
ESP_LOGE(TAG, "Built for SoftDevice S%u %u.x.y. It may crash due to mismatch of bootloader version.",
USE_SOFTDEVICE_ID, USE_SOFTDEVICE_VERSION);
}
#else
if (USE_SOFTDEVICE_ID != sd_id) {
ESP_LOGE(TAG, "Built for SoftDevice S%u. It may crash due to mismatch of bootloader version.", USE_SOFTDEVICE_ID);
}
#endif
#endif
}
#endif
}

View File

@@ -775,7 +775,7 @@ void Display::test_card() {
int shift_y = (h - image_h) / 2;
int line_w = (image_w - 6) / 6;
int image_c = image_w / 2;
for (auto i = 0; i <= image_h; i++) {
for (auto i = 0; i != image_h; i++) {
int c = esp_scale(i, image_h);
this->horizontal_line(shift_x + 0, shift_y + i, line_w, r.fade_to_white(c));
this->horizontal_line(shift_x + line_w, shift_y + i, line_w, r.fade_to_black(c)); //
@@ -809,8 +809,11 @@ void Display::test_card() {
}
}
}
this->rectangle(0, 0, w, h, Color(127, 0, 127));
this->filled_rectangle(0, 0, 10, 10, Color(255, 0, 255));
this->filled_rectangle(w - 10, 0, 10, 10, Color(255, 0, 255));
this->filled_rectangle(0, h - 10, 10, 10, Color(255, 0, 255));
this->filled_rectangle(w - 10, h - 10, 10, 10, Color(255, 0, 255));
this->rectangle(0, 0, w, h, Color(255, 255, 255));
this->stop_poller();
}

View File

@@ -324,7 +324,7 @@ def _is_framework_url(source: str) -> str:
# The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 2, 1),
"recommended": cv.Version(3, 3, 2),
"latest": cv.Version(3, 3, 2),
"dev": cv.Version(3, 3, 2),
}
@@ -343,7 +343,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
# The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(5, 4, 2),
"recommended": cv.Version(5, 5, 1),
"latest": cv.Version(5, 5, 1),
"dev": cv.Version(5, 5, 1),
}
@@ -363,7 +363,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(54, 3, 21, "2"),
"recommended": cv.Version(55, 3, 31, "1"),
"latest": cv.Version(55, 3, 31, "1"),
"dev": cv.Version(55, 3, 31, "1"),
}
@@ -544,6 +544,7 @@ CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries"
CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking"
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety"
CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
def _validate_idf_component(config: ConfigType) -> ConfigType:
@@ -606,6 +607,9 @@ FRAMEWORK_SCHEMA = cv.All(
cv.Optional(
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
): cv.boolean,
cv.Optional(
CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True
): cv.boolean,
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
}
),
@@ -824,6 +828,9 @@ async def to_code(config):
# Disable dynamic log level control to save memory
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
# Reduce PHY TX power in the event of a brownout
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
# Set default CPU frequency
add_idf_sdkconfig_option(
f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{config[CONF_CPU_FREQUENCY][:-3]}", True
@@ -864,6 +871,12 @@ async def to_code(config):
if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True)
# Disable placing libc locks in IRAM to save RAM
# This is safe for ESPHome since no IRAM ISRs (interrupts that run while cache is disabled)
# use libc lock APIs. Saves approximately 1.3KB (1,356 bytes) of IRAM.
if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True):
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
add_extra_build_file(

View File

@@ -1564,6 +1564,10 @@ BOARDS = {
"name": "DFRobot Beetle ESP32-C3",
"variant": VARIANT_ESP32C3,
},
"dfrobot_firebeetle2_esp32c6": {
"name": "DFRobot FireBeetle 2 ESP32-C6",
"variant": VARIANT_ESP32C6,
},
"dfrobot_firebeetle2_esp32e": {
"name": "DFRobot Firebeetle 2 ESP32-E",
"variant": VARIANT_ESP32,
@@ -1604,6 +1608,22 @@ BOARDS = {
"name": "Ai-Thinker ESP-C3-M1-I-Kit",
"variant": VARIANT_ESP32C3,
},
"esp32-c5-devkitc-1": {
"name": "Espressif ESP32-C5-DevKitC-1 4MB no PSRAM",
"variant": VARIANT_ESP32C5,
},
"esp32-c5-devkitc1-n16r4": {
"name": "Espressif ESP32-C5-DevKitC-1 N16R4 (16 MB Flash Quad, 4 MB PSRAM Quad)",
"variant": VARIANT_ESP32C5,
},
"esp32-c5-devkitc1-n4": {
"name": "Espressif ESP32-C5-DevKitC-1 N4 (4MB no PSRAM)",
"variant": VARIANT_ESP32C5,
},
"esp32-c5-devkitc1-n8r4": {
"name": "Espressif ESP32-C5-DevKitC-1 N8R4 (8 MB Flash Quad, 4 MB PSRAM Quad)",
"variant": VARIANT_ESP32C5,
},
"esp32-c6-devkitc-1": {
"name": "Espressif ESP32-C6-DevKitC-1",
"variant": VARIANT_ESP32C6,
@@ -2048,6 +2068,10 @@ BOARDS = {
"name": "M5Stack Station",
"variant": VARIANT_ESP32,
},
"m5stack-tab5-p4": {
"name": "M5STACK Tab5 esp32-p4 Board",
"variant": VARIANT_ESP32P4,
},
"m5stack-timer-cam": {
"name": "M5Stack Timer CAM",
"variant": VARIANT_ESP32,
@@ -2476,6 +2500,10 @@ BOARDS = {
"name": "YelloByte YB-ESP32-S3-AMP (Rev.3)",
"variant": VARIANT_ESP32S3,
},
"yb_esp32s3_drv": {
"name": "YelloByte YB-ESP32-S3-DRV",
"variant": VARIANT_ESP32S3,
},
"yb_esp32s3_eth": {
"name": "YelloByte YB-ESP32-S3-ETH",
"variant": VARIANT_ESP32S3,

View File

@@ -108,8 +108,13 @@ class BTLoggers(Enum):
"""ESP32 WiFi provisioning over Bluetooth"""
# Set to track which loggers are needed by components
_required_loggers: set[BTLoggers] = set()
# Key for storing required loggers in CORE.data
ESP32_BLE_REQUIRED_LOGGERS_KEY = "esp32_ble_required_loggers"
def _get_required_loggers() -> set[BTLoggers]:
"""Get the set of required Bluetooth loggers from CORE.data."""
return CORE.data.setdefault(ESP32_BLE_REQUIRED_LOGGERS_KEY, set())
# Dataclass for handler registration counts
@@ -170,12 +175,13 @@ def register_bt_logger(*loggers: BTLoggers) -> None:
Args:
*loggers: One or more BTLoggers enum members
"""
required_loggers = _get_required_loggers()
for logger in loggers:
if not isinstance(logger, BTLoggers):
raise TypeError(
f"Logger must be a BTLoggers enum member, got {type(logger)}"
)
_required_loggers.add(logger)
required_loggers.add(logger)
CONF_BLE_ID = "ble_id"
@@ -387,6 +393,15 @@ def final_validation(config):
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
validate_connection_slots(max_connections)
# Check if hosted bluetooth is being used
if "esp32_hosted" in full_config:
add_idf_sdkconfig_option("CONFIG_BT_CLASSIC_ENABLED", False)
add_idf_sdkconfig_option("CONFIG_BT_BLE_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLUEDROID_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_CONTROLLER_DISABLED", True)
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True)
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True)
# Check if BLE Server is needed
has_ble_server = "esp32_ble_server" in full_config
@@ -479,8 +494,9 @@ async def to_code(config):
# Apply logger settings if log disabling is enabled
if config.get(CONF_DISABLE_BT_LOGS, False):
# Disable all Bluetooth loggers that are not required
required_loggers = _get_required_loggers()
for logger in BTLoggers:
if logger not in _required_loggers:
if logger not in required_loggers:
add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
# Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector

View File

@@ -6,7 +6,15 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#else
extern "C" {
#include <esp_hosted.h>
#include <esp_hosted_misc.h>
#include <esp_hosted_bluedroid.h>
}
#endif
#include <esp_bt_device.h>
#include <esp_bt_main.h>
#include <esp_gap_ble_api.h>
@@ -136,6 +144,7 @@ void ESP32BLE::advertising_init_() {
bool ESP32BLE::ble_setup_() {
esp_err_t err;
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#ifdef USE_ARDUINO
if (!btStart()) {
ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status());
@@ -169,6 +178,28 @@ bool ESP32BLE::ble_setup_() {
#endif
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
#else
esp_hosted_connect_to_slave(); // NOLINT
if (esp_hosted_bt_controller_init() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed");
return false;
}
if (esp_hosted_bt_controller_enable() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed");
return false;
}
hosted_hci_bluedroid_open();
esp_bluedroid_hci_driver_operations_t operations = {
.send = hosted_hci_bluedroid_send,
.check_send_available = hosted_hci_bluedroid_check_send_available,
.register_host_callback = hosted_hci_bluedroid_register_host_callback,
};
esp_bluedroid_attach_hci_driver(&operations);
#endif
err = esp_bluedroid_init();
if (err != ESP_OK) {
@@ -257,6 +288,7 @@ bool ESP32BLE::ble_dismantle_() {
return false;
}
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#ifdef USE_ARDUINO
if (!btStop()) {
ESP_LOGE(TAG, "btStop failed: %d", esp_bt_controller_get_status());
@@ -286,6 +318,19 @@ bool ESP32BLE::ble_dismantle_() {
return false;
}
}
#endif
#else
if (esp_hosted_bt_controller_disable() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed");
return false;
}
if (esp_hosted_bt_controller_deinit(false) != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed");
return false;
}
hosted_hci_bluedroid_close();
#endif
return true;
}

View File

@@ -10,7 +10,9 @@
#ifdef USE_ESP32
#ifdef USE_ESP32_BLE_ADVERTISING
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <esp_gap_ble_api.h>
#include <esp_gatts_api.h>

View File

@@ -3,7 +3,9 @@
#ifdef USE_ESP32
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <esp_bt_main.h>
#include <esp_gap_ble_api.h>
#include <freertos/FreeRTOS.h>

View File

@@ -5,7 +5,9 @@
#ifdef USE_ESP32
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <esp_gap_ble_api.h>
namespace esphome {

View File

@@ -10,7 +10,9 @@
#include <nvs_flash.h>
#include <freertos/FreeRTOSConfig.h>
#include <esp_bt_main.h>
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <freertos/task.h>
#include <esp_gap_ble_api.h>

View File

@@ -60,11 +60,21 @@ class RegistrationCounts:
clients: int = 0
# Set to track which features are needed by components
_required_features: set[BLEFeatures] = set()
# CORE.data keys for state management
ESP32_BLE_TRACKER_REQUIRED_FEATURES_KEY = "esp32_ble_tracker_required_features"
ESP32_BLE_TRACKER_REGISTRATION_COUNTS_KEY = "esp32_ble_tracker_registration_counts"
# Track registration counts for StaticVector sizing
_registration_counts = RegistrationCounts()
def _get_required_features() -> set[BLEFeatures]:
"""Get the set of required BLE features from CORE.data."""
return CORE.data.setdefault(ESP32_BLE_TRACKER_REQUIRED_FEATURES_KEY, set())
def _get_registration_counts() -> RegistrationCounts:
"""Get the registration counts from CORE.data."""
return CORE.data.setdefault(
ESP32_BLE_TRACKER_REGISTRATION_COUNTS_KEY, RegistrationCounts()
)
def register_ble_features(features: set[BLEFeatures]) -> None:
@@ -73,7 +83,7 @@ def register_ble_features(features: set[BLEFeatures]) -> None:
Args:
features: Set of BLEFeatures enum members
"""
_required_features.update(features)
_get_required_features().update(features)
esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker")
@@ -267,15 +277,17 @@ async def to_code(config):
):
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
registration_counts = _get_registration_counts()
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
_registration_counts.listeners += 1
registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_MAC_ADDRESS in conf:
addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]]
cg.add(trigger.set_addresses(addr_list))
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
_registration_counts.listeners += 1
registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format):
cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID])))
@@ -288,7 +300,7 @@ async def to_code(config):
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []):
_registration_counts.listeners += 1
registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format):
cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID])))
@@ -301,7 +313,7 @@ async def to_code(config):
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
for conf in config.get(CONF_ON_SCAN_END, []):
_registration_counts.listeners += 1
registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
@@ -331,19 +343,21 @@ async def to_code(config):
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_ble_features():
# Add feature-specific defines based on what's needed
if BLEFeatures.ESP_BT_DEVICE in _required_features:
required_features = _get_required_features()
if BLEFeatures.ESP_BT_DEVICE in required_features:
cg.add_define("USE_ESP32_BLE_DEVICE")
cg.add_define("USE_ESP32_BLE_UUID")
# Add defines for StaticVector sizing based on registration counts
# Only define if count > 0 to avoid allocating unnecessary memory
if _registration_counts.listeners > 0:
registration_counts = _get_registration_counts()
if registration_counts.listeners > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT", _registration_counts.listeners
"ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT", registration_counts.listeners
)
if _registration_counts.clients > 0:
if registration_counts.clients > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT", _registration_counts.clients
"ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT", registration_counts.clients
)
@@ -395,7 +409,7 @@ async def register_ble_device(
var: cg.SafeExpType, config: ConfigType
) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
_registration_counts.listeners += 1
_get_registration_counts().listeners += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var))
return var
@@ -403,7 +417,7 @@ async def register_ble_device(
async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
_registration_counts.clients += 1
_get_registration_counts().clients += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var))
return var
@@ -417,7 +431,7 @@ async def register_raw_ble_device(
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
_registration_counts.listeners += 1
_get_registration_counts().listeners += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var))
return var
@@ -431,7 +445,7 @@ async def register_raw_client(
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
_registration_counts.clients += 1
_get_registration_counts().clients += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var))
return var

View File

@@ -7,7 +7,9 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <esp_bt_defs.h>
#include <esp_bt_main.h>
#include <esp_gap_ble_api.h>
@@ -845,6 +847,7 @@ void ESP32BLETracker::log_unexpected_state_(const char *operation, ScannerState
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
void ESP32BLETracker::update_coex_preference_(bool force_ble) {
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
if (force_ble && !this->coex_prefer_ble_) {
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
this->coex_prefer_ble_ = true;
@@ -854,6 +857,7 @@ void ESP32BLETracker::update_coex_preference_(bool force_ble) {
this->coex_prefer_ble_ = false;
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
}
#endif // CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
}
#endif

View File

@@ -92,9 +92,14 @@ async def to_code(config):
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.10.2")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11")
if framework_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.1.5")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.5.11")
else:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11")
esp32.add_extra_script(
"post",
"esp32_hosted.py",

View File

@@ -42,6 +42,11 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
symbols[i] = params->bit0;
}
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) {
*done = true;
}
#endif
return RMT_SYMBOLS_PER_BYTE;
}

View File

@@ -190,7 +190,7 @@ async def to_code(config):
cg.add_define("ESPHOME_VARIANT", "ESP8266")
cg.add_define(ThreadModel.SINGLE)
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"])
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("framework", "arduino")
@@ -230,6 +230,12 @@ async def to_code(config):
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
cg.add_build_flag("-DNEW_OOM_ABORT")
# In testing mode, fake a larger IRAM to allow linking grouped component tests
# Real ESP8266 hardware only has 32KB IRAM, but for CI testing we pretend it has 2MB
# This is done via a pre-build script that generates a custom linker script
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
@@ -265,3 +271,8 @@ def copy_files():
post_build_file,
CORE.relative_build_path("post_build.py"),
)
iram_fix_file = dir / "iram_fix.py.script"
copy_file_if_changed(
iram_fix_file,
CORE.relative_build_path("iram_fix.py"),
)

View File

@@ -0,0 +1,44 @@
import os
import re
# pylint: disable=E0602
Import("env") # noqa
def patch_linker_script_after_preprocess(source, target, env):
"""Patch the local linker script after PlatformIO preprocesses it."""
# Check if we're in testing mode by looking for the define
build_flags = env.get("BUILD_FLAGS", [])
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
if not testing_mode:
return
# Get the local linker script path
build_dir = env.subst("$BUILD_DIR")
local_ld = os.path.join(build_dir, "ld", "local.eagle.app.v6.common.ld")
if not os.path.exists(local_ld):
return
# Read the linker script
with open(local_ld, "r") as f:
content = f.read()
# Replace IRAM size from 0x8000 (32KB) to 0x200000 (2MB)
# The line looks like: iram1_0_seg : org = 0x40100000, len = 0x8000
updated = re.sub(
r"(iram1_0_seg\s*:\s*org\s*=\s*0x40100000\s*,\s*len\s*=\s*)0x8000",
r"\g<1>0x200000",
content,
)
if updated != content:
with open(local_ld, "w") as f:
f.write(updated)
print("ESPHome: Patched IRAM size to 2MB for testing mode")
# Hook into the build process right before linking
# This runs after PlatformIO has already preprocessed the linker scripts
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_linker_script_after_preprocess)

View File

@@ -19,6 +19,7 @@ from esphome.const import (
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -136,11 +137,12 @@ FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_port(config[CONF_PORT]))
if CONF_PASSWORD in config:
# Password could be set to an empty string and we can assume that means no password
if config.get(CONF_PASSWORD):
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
cg.add_define("USE_OTA_PASSWORD")
# Only include hash algorithms when password is configured

View File

@@ -90,13 +90,12 @@ void HomeassistantNumber::control(float value) {
api::HomeassistantActionRequest resp;
resp.set_service(SERVICE_NAME);
resp.data.emplace_back();
auto &entity_id = resp.data.back();
resp.data.init(2);
auto &entity_id = resp.data.emplace_back();
entity_id.set_key(ENTITY_ID_KEY);
entity_id.value = this->entity_id_;
resp.data.emplace_back();
auto &entity_value = resp.data.back();
auto &entity_value = resp.data.emplace_back();
entity_value.set_key(VALUE_KEY);
entity_value.value = to_string(value);

View File

@@ -51,8 +51,8 @@ void HomeassistantSwitch::write_state(bool state) {
resp.set_service(SERVICE_OFF);
}
resp.data.emplace_back();
auto &entity_id_kv = resp.data.back();
resp.data.init(1);
auto &entity_id_kv = resp.data.emplace_back();
entity_id_kv.set_key(ENTITY_ID_KEY);
entity_id_kv.value = this->entity_id_;

View File

@@ -9,8 +9,8 @@ static const char *const TAG = "htu21d";
static const uint8_t HTU21D_ADDRESS = 0x40;
static const uint8_t HTU21D_REGISTER_RESET = 0xFE;
static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xE3;
static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xE5;
static const uint8_t HTU21D_REGISTER_TEMPERATURE = 0xF3;
static const uint8_t HTU21D_REGISTER_HUMIDITY = 0xF5;
static const uint8_t HTU21D_WRITERHT_REG_CMD = 0xE6; /**< Write RH/T User Register 1 */
static const uint8_t HTU21D_REGISTER_STATUS = 0xE7;
static const uint8_t HTU21D_WRITEHEATER_REG_CMD = 0x51; /**< Write Heater Control Register */

View File

@@ -143,7 +143,18 @@ def validate_mclk_divisible_by_3(config):
return config
_use_legacy_driver = None
# Key for storing legacy driver setting in CORE.data
I2S_USE_LEGACY_DRIVER_KEY = "i2s_use_legacy_driver"
def _get_use_legacy_driver():
"""Get the legacy driver setting from CORE.data."""
return CORE.data.get(I2S_USE_LEGACY_DRIVER_KEY)
def _set_use_legacy_driver(value: bool) -> None:
"""Set the legacy driver setting in CORE.data."""
CORE.data[I2S_USE_LEGACY_DRIVER_KEY] = value
def i2s_audio_component_schema(
@@ -209,17 +220,15 @@ async def register_i2s_audio_component(var, config):
def validate_use_legacy(value):
global _use_legacy_driver # noqa: PLW0603
if CONF_USE_LEGACY in value:
if (_use_legacy_driver is not None) and (
_use_legacy_driver != value[CONF_USE_LEGACY]
):
existing_value = _get_use_legacy_driver()
if (existing_value is not None) and (existing_value != value[CONF_USE_LEGACY]):
raise cv.Invalid(
f"All i2s_audio components must set {CONF_USE_LEGACY} to the same value."
)
if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino):
raise cv.Invalid("Arduino supports only the legacy i2s driver")
_use_legacy_driver = value[CONF_USE_LEGACY]
_set_use_legacy_driver(value[CONF_USE_LEGACY])
return value
@@ -249,7 +258,8 @@ def _final_validate(_):
def use_legacy():
return not (CORE.using_esp_idf and not _use_legacy_driver)
legacy_driver = _get_use_legacy_driver()
return not (CORE.using_esp_idf and not legacy_driver)
FINAL_VALIDATE_SCHEMA = _final_validate

View File

@@ -218,7 +218,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
}
case improv::GET_WIFI_NETWORKS: {
std::vector<std::string> networks;
auto results = wifi::global_wifi_component->get_scan_result();
const auto &results = wifi::global_wifi_component->get_scan_result();
for (auto &scan : results) {
if (scan.get_is_hidden())
continue;

View File

@@ -35,6 +35,7 @@ CONF_CHARGE = "charge"
CONF_CHARGE_COULOMBS = "charge_coulombs"
CONF_ENERGY_JOULES = "energy_joules"
CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient"
CONF_RESET_ON_BOOT = "reset_on_boot"
UNIT_AMPERE_HOURS = "Ah"
UNIT_COULOMB = "C"
UNIT_JOULE = "J"
@@ -113,6 +114,7 @@ INA2XX_SCHEMA = cv.Schema(
cv.Optional(CONF_TEMPERATURE_COEFFICIENT, default=0): cv.int_range(
min=0, max=16383
),
cv.Optional(CONF_RESET_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_SHUNT_VOLTAGE): cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_MILLIVOLT,
@@ -206,6 +208,7 @@ async def setup_ina2xx(var, config):
cg.add(var.set_adc_range(config[CONF_ADC_RANGE]))
cg.add(var.set_adc_avg_samples(config[CONF_ADC_AVERAGING]))
cg.add(var.set_shunt_tempco(config[CONF_TEMPERATURE_COEFFICIENT]))
cg.add(var.set_reset_on_boot(config[CONF_RESET_ON_BOOT]))
adc_time_config = config[CONF_ADC_TIME]
if isinstance(adc_time_config, dict):

View File

@@ -257,7 +257,12 @@ bool INA2XX::reset_energy_counters() {
bool INA2XX::reset_config_() {
ESP_LOGV(TAG, "Reset");
ConfigurationRegister cfg{0};
cfg.RST = true;
if (!this->reset_on_boot_) {
ESP_LOGI(TAG, "Skipping on-boot device reset");
cfg.RST = false;
} else {
cfg.RST = true;
}
return this->write_unsigned_16_(RegisterMap::REG_CONFIG, cfg.raw_u16);
}

View File

@@ -127,6 +127,7 @@ class INA2XX : public PollingComponent {
void set_adc_time_die_temperature(AdcTime time) { this->adc_time_die_temperature_ = time; }
void set_adc_avg_samples(AdcAvgSamples samples) { this->adc_avg_samples_ = samples; }
void set_shunt_tempco(uint16_t coeff) { this->shunt_tempco_ppm_c_ = coeff; }
void set_reset_on_boot(bool reset) { this->reset_on_boot_ = reset; }
void set_shunt_voltage_sensor(sensor::Sensor *sensor) { this->shunt_voltage_sensor_ = sensor; }
void set_bus_voltage_sensor(sensor::Sensor *sensor) { this->bus_voltage_sensor_ = sensor; }
@@ -172,6 +173,7 @@ class INA2XX : public PollingComponent {
AdcTime adc_time_die_temperature_{AdcTime::ADC_TIME_4120US};
AdcAvgSamples adc_avg_samples_{AdcAvgSamples::ADC_AVG_SAMPLES_128};
uint16_t shunt_tempco_ppm_c_{0};
bool reset_on_boot_{true};
//
// Calculated coefficients

View File

@@ -177,9 +177,10 @@ void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ =
void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; }
bool LightState::supports_effects() { return !this->effects_.empty(); }
const std::vector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
const FixedVector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
void LightState::add_effects(const std::vector<LightEffect *> &effects) {
this->effects_.reserve(this->effects_.size() + effects.size());
// Called once from Python codegen during setup with all effects from YAML config
this->effects_.init(effects.size());
for (auto *effect : effects) {
this->effects_.push_back(effect);
}

View File

@@ -11,8 +11,9 @@
#include "light_traits.h"
#include "light_transformer.h"
#include <vector>
#include "esphome/core/helpers.h"
#include <strings.h>
#include <vector>
namespace esphome {
namespace light {
@@ -159,7 +160,7 @@ class LightState : public EntityBase, public Component {
bool supports_effects();
/// Get all effects for this light state.
const std::vector<LightEffect *> &get_effects() const;
const FixedVector<LightEffect *> &get_effects() const;
/// Add effects for this light state.
void add_effects(const std::vector<LightEffect *> &effects);
@@ -260,7 +261,7 @@ class LightState : public EntityBase, public Component {
/// The currently active transformer for this light (transition/flash).
std::unique_ptr<LightTransformer> transformer_{nullptr};
/// List of effects for this light.
std::vector<LightEffect *> effects_;
FixedVector<LightEffect *> effects_;
/// Object used to store the persisted values of the light.
ESPPreferenceObject rtc_;
/// Value for storing the index of the currently active effect. 0 if no effect is active

View File

@@ -486,7 +486,6 @@ CONF_RESUME_ON_INPUT = "resume_on_input"
CONF_RIGHT_BUTTON = "right_button"
CONF_ROLLOVER = "rollover"
CONF_ROOT_BACK_BTN = "root_back_btn"
CONF_ROWS = "rows"
CONF_SCALE_LINES = "scale_lines"
CONF_SCROLLBAR_MODE = "scrollbar_mode"
CONF_SELECTED_INDEX = "selected_index"

View File

@@ -2,7 +2,7 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components.key_provider import KeyProvider
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH
from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROWS, CONF_TEXT, CONF_WIDTH
from esphome.cpp_generator import MockObj
from ..automation import action_to_code
@@ -15,7 +15,6 @@ from ..defines import (
CONF_ONE_CHECKED,
CONF_PAD_COLUMN,
CONF_PAD_ROW,
CONF_ROWS,
CONF_SELECTED,
)
from ..helpers import lvgl_components_required

View File

@@ -2,7 +2,7 @@ from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import key_provider
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID
from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_ROWS, CONF_TRIGGER_ID
CODEOWNERS = ["@ssieb"]
@@ -19,7 +19,6 @@ MatrixKeyTrigger = matrix_keypad_ns.class_(
)
CONF_KEYPAD_ID = "keypad_id"
CONF_ROWS = "rows"
CONF_COLUMNS = "columns"
CONF_KEYS = "keys"
CONF_DEBOUNCE_TIME = "debounce_time"

View File

@@ -83,7 +83,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
#endif
auto &txt_records = service.txt_records;
txt_records.reserve(txt_count);
txt_records.init(txt_count);
if (!friendly_name_empty) {
txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())});
@@ -171,12 +171,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
fallback_service.proto = MDNS_STR(SERVICE_TCP);
fallback_service.port = USE_WEBSERVER_PORT;
fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
#endif
#ifdef USE_MDNS_STORE_SERVICES
// Copy to member variable if storage is enabled (verbose logging, OpenThread, or extra services)
this->services_ = services;
fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}};
#endif
}

View File

@@ -38,7 +38,7 @@ struct MDNSService {
// as defined in RFC6763 Section 7, like "_tcp" or "_udp"
const MDNSString *proto;
TemplatableValue<uint16_t> port;
std::vector<MDNSTXTRecord> txt_records;
FixedVector<MDNSTXTRecord> txt_records;
};
class MDNSComponent : public Component {

View File

@@ -12,8 +12,13 @@ namespace mdns {
static const char *const TAG = "mdns";
void MDNSComponent::setup() {
#ifdef USE_MDNS_STORE_SERVICES
this->compile_records_(this->services_);
const auto &services = this->services_;
#else
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
this->compile_records_(services);
#endif
esp_err_t err = mdns_init();
if (err != ESP_OK) {

View File

@@ -12,8 +12,13 @@ namespace esphome {
namespace mdns {
void MDNSComponent::setup() {
#ifdef USE_MDNS_STORE_SERVICES
this->compile_records_(this->services_);
const auto &services = this->services_;
#else
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
this->compile_records_(services);
#endif
MDNS.begin(this->hostname_.c_str());

View File

@@ -12,8 +12,13 @@ namespace esphome {
namespace mdns {
void MDNSComponent::setup() {
#ifdef USE_MDNS_STORE_SERVICES
this->compile_records_(this->services_);
const auto &services = this->services_;
#else
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
this->compile_records_(services);
#endif
MDNS.begin(this->hostname_.c_str());

View File

@@ -12,8 +12,13 @@ namespace esphome {
namespace mdns {
void MDNSComponent::setup() {
#ifdef USE_MDNS_STORE_SERVICES
this->compile_records_(this->services_);
const auto &services = this->services_;
#else
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
this->compile_records_(services);
#endif
MDNS.begin(this->hostname_.c_str());

View File

@@ -11,6 +11,7 @@ from esphome.const import (
CONF_BRIGHTNESS,
CONF_COLOR_ORDER,
CONF_DIMENSIONS,
CONF_DISABLED,
CONF_HEIGHT,
CONF_INIT_SEQUENCE,
CONF_INVERT_COLORS,
@@ -301,6 +302,8 @@ class DriverChip:
Check if a rotation can be implemented in hardware using the MADCTL register.
A rotation of 180 is always possible if x and y mirroring are supported, 90 and 270 are possible if the model supports swapping X and Y.
"""
if config.get(CONF_TRANSFORM) == CONF_DISABLED:
return False
transforms = self.transforms
rotation = config.get(CONF_ROTATION, 0)
if rotation == 0 or not transforms:
@@ -358,26 +361,26 @@ class DriverChip:
CONF_SWAP_XY: self.get_default(CONF_SWAP_XY),
},
)
# fill in defaults if not provided
mirror_x = transform.get(CONF_MIRROR_X, self.get_default(CONF_MIRROR_X))
mirror_y = transform.get(CONF_MIRROR_Y, self.get_default(CONF_MIRROR_Y))
swap_xy = transform.get(CONF_SWAP_XY, self.get_default(CONF_SWAP_XY))
transform[CONF_MIRROR_X] = mirror_x
transform[CONF_MIRROR_Y] = mirror_y
transform[CONF_SWAP_XY] = swap_xy
if not isinstance(transform, dict):
# Presumably disabled
return {
CONF_MIRROR_X: False,
CONF_MIRROR_Y: False,
CONF_SWAP_XY: False,
CONF_TRANSFORM: False,
}
# Can we use the MADCTL register to set the rotation?
if can_transform and CONF_TRANSFORM not in config:
rotation = config[CONF_ROTATION]
if rotation == 180:
transform[CONF_MIRROR_X] = not mirror_x
transform[CONF_MIRROR_Y] = not mirror_y
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
elif rotation == 90:
transform[CONF_SWAP_XY] = not swap_xy
transform[CONF_MIRROR_X] = not mirror_x
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
else:
transform[CONF_SWAP_XY] = not swap_xy
transform[CONF_MIRROR_Y] = not mirror_y
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
transform[CONF_TRANSFORM] = True
return transform

View File

@@ -37,6 +37,7 @@ from esphome.const import (
CONF_DATA_RATE,
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_DISABLED,
CONF_ENABLE_PIN,
CONF_ID,
CONF_INIT_SEQUENCE,
@@ -146,12 +147,15 @@ def swap_xy_schema(model):
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
bus_mode = config[CONF_BUS_MODE]
transform = cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
**swap_xy_schema(model),
}
transform = cv.Any(
cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
**swap_xy_schema(model),
}
),
cv.one_of(CONF_DISABLED, lower=True),
)
# CUSTOM model will need to provide a custom init sequence
iseqconf = (
@@ -160,7 +164,11 @@ def model_schema(config):
else cv.Optional(CONF_INIT_SEQUENCE)
)
# Dimensions are optional if the model has a default width and the x-y transform is not overridden
is_swapped = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True
transform_config = config.get(CONF_TRANSFORM, {})
is_swapped = (
isinstance(transform_config, dict)
and transform_config.get(CONF_SWAP_XY, False) is True
)
cv_dimensions = (
cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required
)
@@ -192,9 +200,7 @@ def model_schema(config):
.extend(
{
cv.GenerateID(): cv.declare_id(MipiSpi),
cv_dimensions(CONF_DIMENSIONS): dimension_schema(
model.get_default(CONF_DRAW_ROUNDING, 1)
),
cv_dimensions(CONF_DIMENSIONS): dimension_schema(1),
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
pins.gpio_output_pin_schema
),
@@ -400,6 +406,7 @@ def get_instance(config):
offset_height,
DISPLAY_ROTATIONS[rotation],
frac,
config[CONF_DRAW_ROUNDING],
]
)
return MipiSpiBuffer, templateargs
@@ -431,7 +438,6 @@ async def to_code(config):
else:
config[CONF_ROTATION] = 0
cg.add(var.set_model(config[CONF_MODEL]))
cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING]))
if enable_pin := config.get(CONF_ENABLE_PIN):
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
cg.add(var.set_enable_pins(enable))

View File

@@ -38,7 +38,7 @@ static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel ord
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
static const uint8_t DELAY_FLAG = 0xFF;
static constexpr uint8_t DELAY_FLAG = 0xFF;
// store a 16 bit value in a buffer, big endian.
static inline void put16_be(uint8_t *buf, uint16_t value) {
buf[0] = value >> 8;
@@ -79,7 +79,7 @@ class MipiSpi : public display::Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_1MHZ> {
public:
MipiSpi() {}
MipiSpi() = default;
void update() override { this->stop_poller(); }
void draw_pixel_at(int x, int y, Color color) override {}
void set_model(const char *model) { this->model_ = model; }
@@ -99,7 +99,6 @@ class MipiSpi : public display::Display,
int get_width_internal() override { return WIDTH; }
int get_height_internal() override { return HEIGHT; }
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
// reset the display, and write the init sequence
void setup() override {
@@ -326,6 +325,7 @@ class MipiSpi : public display::Display,
/**
* Writes a buffer to the display.
* @param ptr The pointer to the pixel data
* @param w Width of each line in bytes
* @param h Height of the buffer in rows
* @param pad Padding in bytes after each line
@@ -424,7 +424,6 @@ class MipiSpi : public display::Display,
// other properties set by configuration
bool invert_colors_{};
unsigned draw_rounding_{2};
optional<uint8_t> brightness_{};
const char *model_{"Unknown"};
std::vector<uint8_t> init_sequence_{};
@@ -444,12 +443,20 @@ class MipiSpi : public display::Display,
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
* @tparam FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer).
* @tparam ROUNDING The alignment requirement for drawing operations (e.g. 2 means that x coordinates must be even)
*/
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION, int FRACTION>
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION,
int FRACTION, unsigned ROUNDING>
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
OFFSET_WIDTH, OFFSET_HEIGHT> {
public:
// these values define the buffer size needed to write in accordance with the chip pixel alignment
// requirements. If the required rounding does not divide the width and height, we round up to the next multiple and
// ignore the extra columns and rows when drawing, but use them to write to the display.
static constexpr unsigned BUFFER_WIDTH = (WIDTH + ROUNDING - 1) / ROUNDING * ROUNDING;
static constexpr unsigned BUFFER_HEIGHT = (HEIGHT + ROUNDING - 1) / ROUNDING * ROUNDING;
MipiSpiBuffer() { this->rotation_ = ROTATION; }
void dump_config() override {
@@ -461,15 +468,15 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
" Buffer fraction: 1/%d\n"
" Buffer bytes: %zu\n"
" Draw rounding: %u",
this->rotation_, BUFFERPIXEL * 8, FRACTION, sizeof(BUFFERTYPE) * WIDTH * HEIGHT / FRACTION,
this->draw_rounding_);
this->rotation_, BUFFERPIXEL * 8, FRACTION,
sizeof(BUFFERTYPE) * BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION, ROUNDING);
}
void setup() override {
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
OFFSET_HEIGHT>::setup();
RAMAllocator<BUFFERTYPE> allocator{};
this->buffer_ = allocator.allocate(WIDTH * HEIGHT / FRACTION);
this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION);
if (this->buffer_ == nullptr) {
this->mark_failed("Buffer allocation failed");
}
@@ -508,15 +515,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
esph_log_v(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_,
this->y_high_);
// Some chips require that the drawing window be aligned on certain boundaries
auto dr = this->draw_rounding_;
this->x_low_ = this->x_low_ / dr * dr;
this->y_low_ = this->y_low_ / dr * dr;
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
this->x_low_ = this->x_low_ / ROUNDING * ROUNDING;
this->y_low_ = this->y_low_ / ROUNDING * ROUNDING;
this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
int w = this->x_high_ - this->x_low_ + 1;
int h = this->y_high_ - this->y_low_ + 1;
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
this->y_low_ - this->start_line_, WIDTH - w);
this->y_low_ - this->start_line_, BUFFER_WIDTH - w);
// invalidate watermarks
this->x_low_ = WIDTH;
this->y_low_ = HEIGHT;
@@ -536,10 +542,10 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
void draw_pixel_at(int x, int y, Color color) override {
if (!this->get_clipping().inside(x, y))
return;
rotate_coordinates_(x, y);
rotate_coordinates(x, y);
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
return;
this->buffer_[(y - this->start_line_) * WIDTH + x] = convert_color_(color);
this->buffer_[(y - this->start_line_) * BUFFER_WIDTH + x] = convert_color(color);
if (x < this->x_low_) {
this->x_low_ = x;
}
@@ -560,7 +566,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
this->y_low_ = this->start_line_;
this->x_high_ = WIDTH - 1;
this->y_high_ = this->end_line_ - 1;
std::fill_n(this->buffer_, HEIGHT * WIDTH / FRACTION, convert_color_(color));
std::fill_n(this->buffer_, HEIGHT * BUFFER_WIDTH / FRACTION, convert_color(color));
}
int get_width() override {
@@ -577,7 +583,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
protected:
// Rotate the coordinates to match the display orientation.
void rotate_coordinates_(int &x, int &y) const {
static void rotate_coordinates(int &x, int &y) {
if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) {
x = WIDTH - x - 1;
y = HEIGHT - y - 1;
@@ -593,7 +599,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
}
// Convert a color to the buffer pixel format.
BUFFERTYPE convert_color_(Color &color) const {
static BUFFERTYPE convert_color(const Color &color) {
if constexpr (BUFFERPIXEL == PIXEL_MODE_8) {
return (color.red & 0xE0) | (color.g & 0xE0) >> 3 | color.b >> 6;
} else if constexpr (BUFFERPIXEL == PIXEL_MODE_16) {

View File

@@ -3,6 +3,7 @@ import esphome.config_validation as cv
from .amoled import CO5300
from .ili import ILI9488_A
from .jc import AXS15231
DriverChip(
"WAVESHARE-4-TFT",
@@ -152,3 +153,12 @@ CO5300.extend(
cs_pin=12,
reset_pin=39,
)
AXS15231.extend(
"WAVESHARE-ESP32-S3-TOUCH-LCD-3.49",
width=172,
height=640,
data_rate="80MHz",
cs_pin=9,
reset_pin=21,
)

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import logging
from pathlib import Path
from esphome import pins
@@ -48,6 +49,7 @@ from .gpio import nrf52_pin_to_code # noqa
CODEOWNERS = ["@tomaszduda23"]
AUTO_LOAD = ["zephyr"]
IS_TARGET_PLATFORM = True
_LOGGER = logging.getLogger(__name__)
def set_core_data(config: ConfigType) -> ConfigType:
@@ -127,6 +129,10 @@ def _validate_mcumgr(config):
def _final_validate(config):
if CONF_DFU in config:
_validate_mcumgr(config)
if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT:
_LOGGER.warning(
"Selected generic Adafruit bootloader. The board might crash. Consider settings `bootloader:`"
)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -157,6 +163,13 @@ async def to_code(config: ConfigType) -> None:
if config[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT:
cg.add_define("USE_BOOTLOADER_MCUBOOT")
else:
if "_sd" in config[KEY_BOOTLOADER]:
bootloader = config[KEY_BOOTLOADER].split("_")
sd_id = bootloader[2][2:]
cg.add_define("USE_SOFTDEVICE_ID", int(sd_id))
if (len(bootloader)) > 3:
sd_version = bootloader[3][1:]
cg.add_define("USE_SOFTDEVICE_VERSION", int(sd_version))
# make sure that firmware.zip is created
# for Adafruit_nRF52_Bootloader
cg.add_platformio_option("board_upload.protocol", "nrfutil")

View File

@@ -11,10 +11,18 @@ from .const import (
BOARDS_ZEPHYR = {
"adafruit_itsybitsy_nrf52840": {
KEY_BOOTLOADER: [
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
]
},
"xiao_ble": {
KEY_BOOTLOADER: [
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
BOOTLOADER_ADAFRUIT,
BOOTLOADER_ADAFRUIT_NRF52_SD132,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
]
},
}

View File

@@ -66,6 +66,7 @@ from esphome.const import (
DEVICE_CLASS_SPEED,
DEVICE_CLASS_SULPHUR_DIOXIDE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TEMPERATURE_DELTA,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
DEVICE_CLASS_VOLTAGE,
@@ -130,6 +131,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_SPEED,
DEVICE_CLASS_SULPHUR_DIOXIDE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TEMPERATURE_DELTA,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
DEVICE_CLASS_VOLTAGE,

View File

@@ -89,6 +89,7 @@ from esphome.const import (
DEVICE_CLASS_SPEED,
DEVICE_CLASS_SULPHUR_DIOXIDE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TEMPERATURE_DELTA,
DEVICE_CLASS_TIMESTAMP,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
@@ -157,6 +158,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_SPEED,
DEVICE_CLASS_SULPHUR_DIOXIDE,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TEMPERATURE_DELTA,
DEVICE_CLASS_TIMESTAMP,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,

View File

@@ -145,7 +145,7 @@ class BSDSocketImpl : public Socket {
}
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override {
return ::sendto(fd_, buf, len, flags, to, tolen);
return ::sendto(fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument)
}
int setblocking(bool blocking) override {

View File

@@ -1,7 +1,7 @@
import logging
from esphome import core
from esphome.config_helpers import Extend, Remove, merge_config
from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered
import esphome.config_validation as cv
from esphome.const import CONF_SUBSTITUTIONS, VALID_SUBSTITUTIONS_CHARACTERS
from esphome.yaml_util import ESPHomeDataBase, ESPLiteralValue, make_data_base
@@ -170,10 +170,10 @@ def do_substitution_pass(config, command_line_substitutions, ignore_missing=Fals
return
# Merge substitutions in config, overriding with substitutions coming from command line:
substitutions = {
**config.get(CONF_SUBSTITUTIONS, {}),
**(command_line_substitutions or {}),
}
# Use merge_dicts_ordered to preserve OrderedDict type for move_to_end()
substitutions = merge_dicts_ordered(
config.get(CONF_SUBSTITUTIONS, {}), command_line_substitutions or {}
)
with cv.prepend_path("substitutions"):
if not isinstance(substitutions, dict):
raise cv.Invalid(

View File

@@ -241,9 +241,14 @@ void ThermostatClimate::control(const climate::ClimateCall &call) {
climate::ClimateTraits ThermostatClimate::traits() {
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
if (this->supports_two_points_)
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE);
if (this->humidity_sensor_ != nullptr)
traits.set_supports_current_humidity(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
if (this->supports_auto_)
traits.add_supported_mode(climate::CLIMATE_MODE_AUTO);
@@ -294,9 +299,6 @@ climate::ClimateTraits ThermostatClimate::traits() {
for (auto &it : this->custom_preset_config_) {
traits.add_supported_custom_preset(it.first);
}
traits.set_supports_two_point_target_temperature(this->supports_two_points_);
traits.set_supports_action(true);
return traits;
}

View File

@@ -14,6 +14,7 @@ MODELS = {
"GENERIC": Model.MODEL_GENERIC,
"RAC-PT1411HWRU-C": Model.MODEL_RAC_PT1411HWRU_C,
"RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F,
"RAS-2819T": Model.MODEL_RAS_2819T,
}
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).extend(

View File

@@ -1,4 +1,5 @@
#include "toshiba.h"
#include "esphome/components/remote_base/toshiba_ac_protocol.h"
#include <vector>
@@ -97,6 +98,282 @@ const std::vector<uint8_t> RAC_PT1411HWRU_TEMPERATURE_F{0x10, 0x30, 0x00, 0x20,
0x22, 0x06, 0x26, 0x07, 0x05, 0x25, 0x04, 0x24, 0x0C,
0x2C, 0x0D, 0x2D, 0x09, 0x08, 0x28, 0x0A, 0x2A, 0x0B};
// RAS-2819T protocol constants
const uint16_t RAS_2819T_HEADER1 = 0xC23D;
const uint8_t RAS_2819T_HEADER2 = 0xD5;
const uint8_t RAS_2819T_MESSAGE_LENGTH = 6;
// RAS-2819T fan speed codes for rc_code_1 (bytes 2-3)
const uint16_t RAS_2819T_FAN_AUTO = 0xBF40;
const uint16_t RAS_2819T_FAN_QUIET = 0xFF00;
const uint16_t RAS_2819T_FAN_LOW = 0x9F60;
const uint16_t RAS_2819T_FAN_MEDIUM = 0x5FA0;
const uint16_t RAS_2819T_FAN_HIGH = 0x3FC0;
// RAS-2819T fan speed codes for rc_code_2 (byte 1)
const uint8_t RAS_2819T_FAN2_AUTO = 0x66;
const uint8_t RAS_2819T_FAN2_QUIET = 0x01;
const uint8_t RAS_2819T_FAN2_LOW = 0x28;
const uint8_t RAS_2819T_FAN2_MEDIUM = 0x3C;
const uint8_t RAS_2819T_FAN2_HIGH = 0x50;
// RAS-2819T second packet suffix bytes for rc_code_2 (bytes 3-5)
// These are fixed patterns, not actual checksums
struct Ras2819tPacketSuffix {
uint8_t byte3;
uint8_t byte4;
uint8_t byte5;
};
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_AUTO{0x00, 0x02, 0x3D};
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_QUIET{0x00, 0x02, 0xD8};
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_LOW{0x00, 0x02, 0xFF};
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_MEDIUM{0x00, 0x02, 0x13};
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_HIGH{0x00, 0x02, 0x27};
// RAS-2819T swing toggle command
const uint64_t RAS_2819T_SWING_TOGGLE = 0xC23D6B94E01F;
// RAS-2819T single-packet commands
const uint64_t RAS_2819T_POWER_OFF_COMMAND = 0xC23D7B84E01F;
// RAS-2819T known valid command patterns for validation
const std::array<uint64_t, 2> RAS_2819T_VALID_SINGLE_COMMANDS = {
RAS_2819T_POWER_OFF_COMMAND, // Power off
RAS_2819T_SWING_TOGGLE, // Swing toggle
};
const uint16_t RAS_2819T_VALID_HEADER1 = 0xC23D;
const uint8_t RAS_2819T_VALID_HEADER2 = 0xD5;
const uint8_t RAS_2819T_DRY_BYTE2 = 0x1F;
const uint8_t RAS_2819T_DRY_BYTE3 = 0xE0;
const uint8_t RAS_2819T_DRY_TEMP_OFFSET = 0x24;
const uint8_t RAS_2819T_AUTO_BYTE2 = 0x1F;
const uint8_t RAS_2819T_AUTO_BYTE3 = 0xE0;
const uint8_t RAS_2819T_AUTO_TEMP_OFFSET = 0x08;
const uint8_t RAS_2819T_FAN_ONLY_TEMP = 0xE4;
const uint8_t RAS_2819T_FAN_ONLY_TEMP_INV = 0x1B;
const uint8_t RAS_2819T_HEAT_TEMP_OFFSET = 0x0C;
// RAS-2819T second packet fixed values
const uint8_t RAS_2819T_AUTO_DRY_FAN_BYTE = 0x65;
const uint8_t RAS_2819T_AUTO_DRY_SUFFIX = 0x3A;
const uint8_t RAS_2819T_HEAT_SUFFIX = 0x3B;
// RAS-2819T temperature codes for 18-30°C
static const uint8_t RAS_2819T_TEMP_CODES[] = {
0x10, // 18°C
0x30, // 19°C
0x20, // 20°C
0x60, // 21°C
0x70, // 22°C
0x50, // 23°C
0x40, // 24°C
0xC0, // 25°C
0xD0, // 26°C
0x90, // 27°C
0x80, // 28°C
0xA0, // 29°C
0xB0 // 30°C
};
// Helper functions for RAS-2819T protocol
//
// ===== RAS-2819T PROTOCOL DOCUMENTATION =====
//
// The RAS-2819T uses a two-packet IR protocol with some exceptions for simple commands.
//
// PACKET STRUCTURE:
// All packets are 6 bytes (48 bits) transmitted with standard Toshiba timing.
//
// TWO-PACKET COMMANDS (Mode/Temperature/Fan changes):
//
// First Packet (rc_code_1): [C2 3D] [FAN_HI FAN_LO] [TEMP] [~TEMP]
// Byte 0-1: Header (always 0xC23D)
// Byte 2-3: Fan speed encoding (varies by mode, see fan tables below)
// Byte 4: Temperature + mode encoding
// Byte 5: Bitwise complement of temperature byte
//
// Second Packet (rc_code_2): [D5] [FAN2] [00] [SUF1] [SUF2] [SUF3]
// Byte 0: Header (always 0xD5)
// Byte 1: Fan speed secondary encoding
// Byte 2: Always 0x00
// Byte 3-5: Fixed suffix pattern (depends on fan speed and mode)
//
// TEMPERATURE ENCODING:
// Base temp codes: 18°C=0x10, 19°C=0x30, 20°C=0x20, 21°C=0x60, 22°C=0x70,
// 23°C=0x50, 24°C=0x40, 25°C=0xC0, 26°C=0xD0, 27°C=0x90,
// 28°C=0x80, 29°C=0xA0, 30°C=0xB0
// Mode offsets added to base temp:
// COOL: No offset
// HEAT: +0x0C (e.g., 24°C heat = 0x40 | 0x0C = 0x4C)
// AUTO: +0x08 (e.g., 24°C auto = 0x40 | 0x08 = 0x48)
// DRY: +0x24 (e.g., 24°C dry = 0x40 | 0x24 = 0x64)
//
// FAN SPEED ENCODING (First packet bytes 2-3):
// AUTO: 0xBF40, QUIET: 0xFF00, LOW: 0x9F60, MEDIUM: 0x5FA0, HIGH: 0x3FC0
// Special cases: AUTO/DRY modes use 0x1FE0 instead
//
// SINGLE-PACKET COMMANDS:
// Power Off: 0xC23D7B84E01F (6 bytes, no second packet)
// Swing Toggle: 0xC23D6B94E01F (6 bytes, no second packet)
//
// MODE DETECTION (from first packet):
// - Check bytes 2-3: if 0x7B84 → OFF mode
// - Check bytes 2-3: if 0x1FE0 → AUTO/DRY/low-temp-COOL (distinguish by temp code)
// - Otherwise: COOL/HEAT/FAN_ONLY (distinguish by temp code and byte 5)
/**
* Get fan speed encoding for RAS-2819T first packet (rc_code_1, bytes 2-3)
*/
static uint16_t get_ras_2819t_fan_code(climate::ClimateFanMode fan_mode) {
switch (fan_mode) {
case climate::CLIMATE_FAN_QUIET:
return RAS_2819T_FAN_QUIET;
case climate::CLIMATE_FAN_LOW:
return RAS_2819T_FAN_LOW;
case climate::CLIMATE_FAN_MEDIUM:
return RAS_2819T_FAN_MEDIUM;
case climate::CLIMATE_FAN_HIGH:
return RAS_2819T_FAN_HIGH;
case climate::CLIMATE_FAN_AUTO:
default:
return RAS_2819T_FAN_AUTO;
}
}
/**
* Get fan speed encoding for RAS-2819T rc_code_2 packet (second packet)
*/
struct Ras2819tSecondPacketCodes {
uint8_t fan_byte;
Ras2819tPacketSuffix suffix;
};
static Ras2819tSecondPacketCodes get_ras_2819t_second_packet_codes(climate::ClimateFanMode fan_mode) {
switch (fan_mode) {
case climate::CLIMATE_FAN_QUIET:
return {RAS_2819T_FAN2_QUIET, RAS_2819T_SUFFIX_QUIET};
case climate::CLIMATE_FAN_LOW:
return {RAS_2819T_FAN2_LOW, RAS_2819T_SUFFIX_LOW};
case climate::CLIMATE_FAN_MEDIUM:
return {RAS_2819T_FAN2_MEDIUM, RAS_2819T_SUFFIX_MEDIUM};
case climate::CLIMATE_FAN_HIGH:
return {RAS_2819T_FAN2_HIGH, RAS_2819T_SUFFIX_HIGH};
case climate::CLIMATE_FAN_AUTO:
default:
return {RAS_2819T_FAN2_AUTO, RAS_2819T_SUFFIX_AUTO};
}
}
/**
* Get temperature code for RAS-2819T protocol
*/
static uint8_t get_ras_2819t_temp_code(float temperature) {
int temp_index = static_cast<int>(temperature) - 18;
if (temp_index < 0 || temp_index >= static_cast<int>(sizeof(RAS_2819T_TEMP_CODES))) {
ESP_LOGW(TAG, "Temperature %.1f°C out of range [18-30°C], defaulting to 24°C", temperature);
return 0x40; // Default to 24°C
}
return RAS_2819T_TEMP_CODES[temp_index];
}
/**
* Decode temperature from RAS-2819T temp code
*/
static float decode_ras_2819t_temperature(uint8_t temp_code) {
uint8_t base_temp_code = temp_code & 0xF0;
// Find the code in the temperature array
for (size_t temp_index = 0; temp_index < sizeof(RAS_2819T_TEMP_CODES); temp_index++) {
if (RAS_2819T_TEMP_CODES[temp_index] == base_temp_code) {
return static_cast<float>(temp_index + 18); // 18°C is the minimum
}
}
ESP_LOGW(TAG, "Unknown temp code: 0x%02X, defaulting to 24°C", base_temp_code);
return 24.0f; // Default to 24°C
}
/**
* Decode fan speed from RAS-2819T IR codes
*/
static climate::ClimateFanMode decode_ras_2819t_fan_mode(uint16_t fan_code) {
switch (fan_code) {
case RAS_2819T_FAN_QUIET:
return climate::CLIMATE_FAN_QUIET;
case RAS_2819T_FAN_LOW:
return climate::CLIMATE_FAN_LOW;
case RAS_2819T_FAN_MEDIUM:
return climate::CLIMATE_FAN_MEDIUM;
case RAS_2819T_FAN_HIGH:
return climate::CLIMATE_FAN_HIGH;
case RAS_2819T_FAN_AUTO:
default:
return climate::CLIMATE_FAN_AUTO;
}
}
/**
* Validate RAS-2819T IR command structure and content
*/
static bool is_valid_ras_2819t_command(uint64_t rc_code_1, uint64_t rc_code_2 = 0) {
// Check header of first packet
uint16_t header1 = (rc_code_1 >> 32) & 0xFFFF;
if (header1 != RAS_2819T_VALID_HEADER1) {
return false;
}
// Single packet commands
if (rc_code_2 == 0) {
for (uint64_t valid_cmd : RAS_2819T_VALID_SINGLE_COMMANDS) {
if (rc_code_1 == valid_cmd) {
return true;
}
}
// Additional validation for unknown single packets
return false;
}
// Two-packet commands - validate second packet header
uint8_t header2 = (rc_code_2 >> 40) & 0xFF;
if (header2 != RAS_2819T_VALID_HEADER2) {
return false;
}
// Validate temperature complement in first packet (byte 4 should be ~byte 5)
uint8_t temp_byte = (rc_code_1 >> 8) & 0xFF;
uint8_t temp_complement = rc_code_1 & 0xFF;
if (temp_byte != static_cast<uint8_t>(~temp_complement)) {
return false;
}
// Validate fan speed combinations make sense
uint16_t fan_code = (rc_code_1 >> 16) & 0xFFFF;
uint8_t fan2_byte = (rc_code_2 >> 32) & 0xFF;
// Check if fan codes are from known valid patterns
bool valid_fan_combo = false;
if (fan_code == RAS_2819T_FAN_AUTO && fan2_byte == RAS_2819T_FAN2_AUTO)
valid_fan_combo = true;
if (fan_code == RAS_2819T_FAN_QUIET && fan2_byte == RAS_2819T_FAN2_QUIET)
valid_fan_combo = true;
if (fan_code == RAS_2819T_FAN_LOW && fan2_byte == RAS_2819T_FAN2_LOW)
valid_fan_combo = true;
if (fan_code == RAS_2819T_FAN_MEDIUM && fan2_byte == RAS_2819T_FAN2_MEDIUM)
valid_fan_combo = true;
if (fan_code == RAS_2819T_FAN_HIGH && fan2_byte == RAS_2819T_FAN2_HIGH)
valid_fan_combo = true;
if (fan_code == 0x1FE0 && fan2_byte == RAS_2819T_AUTO_DRY_FAN_BYTE)
valid_fan_combo = true; // AUTO/DRY
return valid_fan_combo;
}
void ToshibaClimate::setup() {
if (this->sensor_) {
this->sensor_->add_on_state_callback([this](float state) {
@@ -126,16 +403,43 @@ void ToshibaClimate::setup() {
this->minimum_temperature_ = this->temperature_min_();
this->maximum_temperature_ = this->temperature_max_();
this->swing_modes_ = this->toshiba_swing_modes_();
// Ensure swing mode is always initialized to a valid value
if (this->swing_modes_.empty() || this->swing_modes_.find(this->swing_mode) == this->swing_modes_.end()) {
// No swing support for this model or current swing mode not supported, reset to OFF
this->swing_mode = climate::CLIMATE_SWING_OFF;
}
// Ensure mode is valid - ESPHome should only use standard climate modes
if (this->mode != climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_HEAT &&
this->mode != climate::CLIMATE_MODE_COOL && this->mode != climate::CLIMATE_MODE_HEAT_COOL &&
this->mode != climate::CLIMATE_MODE_DRY && this->mode != climate::CLIMATE_MODE_FAN_ONLY) {
ESP_LOGW(TAG, "Invalid mode detected during setup, resetting to OFF");
this->mode = climate::CLIMATE_MODE_OFF;
}
// Ensure fan mode is valid
if (!this->fan_mode.has_value()) {
ESP_LOGW(TAG, "Fan mode not set during setup, defaulting to AUTO");
this->fan_mode = climate::CLIMATE_FAN_AUTO;
}
// Never send nan to HA
if (std::isnan(this->target_temperature))
this->target_temperature = 24;
// Log final state for debugging HA errors
ESP_LOGV(TAG, "Setup complete - Mode: %d, Fan: %s, Swing: %d, Temp: %.1f", static_cast<int>(this->mode),
this->fan_mode.has_value() ? std::to_string(static_cast<int>(this->fan_mode.value())).c_str() : "NONE",
static_cast<int>(this->swing_mode), this->target_temperature);
}
void ToshibaClimate::transmit_state() {
if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F) {
transmit_rac_pt1411hwru_();
this->transmit_rac_pt1411hwru_();
} else if (this->model_ == MODEL_RAS_2819T) {
this->transmit_ras_2819t_();
} else {
transmit_generic_();
this->transmit_generic_();
}
}
@@ -230,7 +534,7 @@ void ToshibaClimate::transmit_generic_() {
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
encode_(data, message, message_length, 1);
this->encode_(data, message, message_length, 1);
transmit.perform();
}
@@ -348,15 +652,12 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() {
message[11] += message[index];
}
}
ESP_LOGV(TAG, "*** Generated codes: 0x%.2X%.2X%.2X%.2X%.2X%.2X 0x%.2X%.2X%.2X%.2X%.2X%.2X", message[0], message[1],
message[2], message[3], message[4], message[5], message[6], message[7], message[8], message[9], message[10],
message[11]);
// load first block of IR code and repeat it once
encode_(data, &message[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
this->encode_(data, &message[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
// load second block of IR code, if present
if (message[6] != 0) {
encode_(data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH, 0);
this->encode_(data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH, 0);
}
transmit.perform();
@@ -366,19 +667,19 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() {
data->space(TOSHIBA_PACKET_SPACE);
switch (this->swing_mode) {
case climate::CLIMATE_SWING_VERTICAL:
encode_(data, &RAC_PT1411HWRU_SWING_VERTICAL[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
this->encode_(data, &RAC_PT1411HWRU_SWING_VERTICAL[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
break;
case climate::CLIMATE_SWING_OFF:
default:
encode_(data, &RAC_PT1411HWRU_SWING_OFF[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
this->encode_(data, &RAC_PT1411HWRU_SWING_OFF[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
}
data->space(TOSHIBA_PACKET_SPACE);
transmit.perform();
if (this->sensor_) {
transmit_rac_pt1411hwru_temp_(true, false);
this->transmit_rac_pt1411hwru_temp_(true, false);
}
}
@@ -430,15 +731,217 @@ void ToshibaClimate::transmit_rac_pt1411hwru_temp_(const bool cs_state, const bo
// Byte 5: Footer lower/bitwise complement of byte 4
message[5] = ~message[4];
ESP_LOGV(TAG, "*** Generated code: 0x%.2X%.2X%.2X%.2X%.2X%.2X", message[0], message[1], message[2], message[3],
message[4], message[5]);
// load IR code and repeat it once
encode_(data, message, RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
this->encode_(data, message, RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
transmit.perform();
}
}
void ToshibaClimate::transmit_ras_2819t_() {
// Handle swing mode transmission for RAS-2819T
// Note: RAS-2819T uses a toggle command, so we need to track state changes
// Check if ONLY swing mode changed (and no other climate parameters)
bool swing_changed = (this->swing_mode != this->last_swing_mode_);
bool mode_changed = (this->mode != this->last_mode_);
bool fan_changed = (this->fan_mode != this->last_fan_mode_);
bool temp_changed = (abs(this->target_temperature - this->last_target_temperature_) > 0.1f);
bool only_swing_changed = swing_changed && !mode_changed && !fan_changed && !temp_changed;
if (only_swing_changed) {
// Send ONLY swing toggle command (like the physical remote does)
auto swing_transmit = this->transmitter_->transmit();
auto *swing_data = swing_transmit.get_data();
// Convert toggle command to bytes for transmission
uint8_t swing_message[RAS_2819T_MESSAGE_LENGTH];
swing_message[0] = (RAS_2819T_SWING_TOGGLE >> 40) & 0xFF;
swing_message[1] = (RAS_2819T_SWING_TOGGLE >> 32) & 0xFF;
swing_message[2] = (RAS_2819T_SWING_TOGGLE >> 24) & 0xFF;
swing_message[3] = (RAS_2819T_SWING_TOGGLE >> 16) & 0xFF;
swing_message[4] = (RAS_2819T_SWING_TOGGLE >> 8) & 0xFF;
swing_message[5] = RAS_2819T_SWING_TOGGLE & 0xFF;
// Use single packet transmission WITH repeat (like regular commands)
this->encode_(swing_data, swing_message, RAS_2819T_MESSAGE_LENGTH, 1);
swing_transmit.perform();
// Update all state tracking
this->last_swing_mode_ = this->swing_mode;
this->last_mode_ = this->mode;
this->last_fan_mode_ = this->fan_mode;
this->last_target_temperature_ = this->target_temperature;
// Immediately publish the state change to Home Assistant
this->publish_state();
return; // Exit early - don't send climate command
}
// If we get here, send the regular climate command (temperature/mode/fan)
uint8_t message1[RAS_2819T_MESSAGE_LENGTH] = {0};
uint8_t message2[RAS_2819T_MESSAGE_LENGTH] = {0};
float temperature =
clamp<float>(this->target_temperature, TOSHIBA_RAS_2819T_TEMP_C_MIN, TOSHIBA_RAS_2819T_TEMP_C_MAX);
// Build first packet (RAS_2819T_HEADER1 + 4 bytes)
message1[0] = (RAS_2819T_HEADER1 >> 8) & 0xFF;
message1[1] = RAS_2819T_HEADER1 & 0xFF;
// Handle OFF mode
if (this->mode == climate::CLIMATE_MODE_OFF) {
// Extract bytes from power off command constant
message1[2] = (RAS_2819T_POWER_OFF_COMMAND >> 24) & 0xFF;
message1[3] = (RAS_2819T_POWER_OFF_COMMAND >> 16) & 0xFF;
message1[4] = (RAS_2819T_POWER_OFF_COMMAND >> 8) & 0xFF;
message1[5] = RAS_2819T_POWER_OFF_COMMAND & 0xFF;
// No second packet for OFF
} else {
// Get temperature and fan encoding
uint8_t temp_code = get_ras_2819t_temp_code(temperature);
// Get fan speed encoding for rc_code_1
climate::ClimateFanMode effective_fan_mode = this->fan_mode.value();
// Dry mode only supports AUTO fan speed
if (this->mode == climate::CLIMATE_MODE_DRY) {
effective_fan_mode = climate::CLIMATE_FAN_AUTO;
if (this->fan_mode.value() != climate::CLIMATE_FAN_AUTO) {
ESP_LOGW(TAG, "Dry mode only supports AUTO fan speed, forcing AUTO");
}
}
uint16_t fan_code = get_ras_2819t_fan_code(effective_fan_mode);
// Mode and temperature encoding
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
// All cooling temperatures support fan speed control
message1[2] = (fan_code >> 8) & 0xFF;
message1[3] = fan_code & 0xFF;
message1[4] = temp_code;
message1[5] = ~temp_code;
break;
case climate::CLIMATE_MODE_HEAT:
// Heating supports fan speed control
message1[2] = (fan_code >> 8) & 0xFF;
message1[3] = fan_code & 0xFF;
// Heat mode adds offset to temperature code
message1[4] = temp_code | RAS_2819T_HEAT_TEMP_OFFSET;
message1[5] = ~(temp_code | RAS_2819T_HEAT_TEMP_OFFSET);
break;
case climate::CLIMATE_MODE_HEAT_COOL:
// Auto mode uses fixed encoding
message1[2] = RAS_2819T_AUTO_BYTE2;
message1[3] = RAS_2819T_AUTO_BYTE3;
message1[4] = temp_code | RAS_2819T_AUTO_TEMP_OFFSET;
message1[5] = ~(temp_code | RAS_2819T_AUTO_TEMP_OFFSET);
break;
case climate::CLIMATE_MODE_DRY:
// Dry mode uses fixed encoding and forces AUTO fan
message1[2] = RAS_2819T_DRY_BYTE2;
message1[3] = RAS_2819T_DRY_BYTE3;
message1[4] = temp_code | RAS_2819T_DRY_TEMP_OFFSET;
message1[5] = ~message1[4];
break;
case climate::CLIMATE_MODE_FAN_ONLY:
// Fan only mode supports fan speed control
message1[2] = (fan_code >> 8) & 0xFF;
message1[3] = fan_code & 0xFF;
message1[4] = RAS_2819T_FAN_ONLY_TEMP;
message1[5] = RAS_2819T_FAN_ONLY_TEMP_INV;
break;
default:
// Default case supports fan speed control
message1[2] = (fan_code >> 8) & 0xFF;
message1[3] = fan_code & 0xFF;
message1[4] = temp_code;
message1[5] = ~temp_code;
break;
}
// Build second packet (RAS_2819T_HEADER2 + 4 bytes)
message2[0] = RAS_2819T_HEADER2;
// Get fan speed encoding for rc_code_2
Ras2819tSecondPacketCodes second_packet_codes = get_ras_2819t_second_packet_codes(effective_fan_mode);
// Determine header byte 2 and fan encoding based on mode
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
message2[1] = second_packet_codes.fan_byte;
message2[2] = 0x00;
message2[3] = second_packet_codes.suffix.byte3;
message2[4] = second_packet_codes.suffix.byte4;
message2[5] = second_packet_codes.suffix.byte5;
break;
case climate::CLIMATE_MODE_HEAT:
message2[1] = second_packet_codes.fan_byte;
message2[2] = 0x00;
message2[3] = second_packet_codes.suffix.byte3;
message2[4] = 0x00;
message2[5] = RAS_2819T_HEAT_SUFFIX;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
case climate::CLIMATE_MODE_DRY:
// Auto/Dry modes use fixed values regardless of fan setting
message2[1] = RAS_2819T_AUTO_DRY_FAN_BYTE;
message2[2] = 0x00;
message2[3] = 0x00;
message2[4] = 0x00;
message2[5] = RAS_2819T_AUTO_DRY_SUFFIX;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
message2[1] = second_packet_codes.fan_byte;
message2[2] = 0x00;
message2[3] = second_packet_codes.suffix.byte3;
message2[4] = 0x00;
message2[5] = RAS_2819T_HEAT_SUFFIX;
break;
default:
message2[1] = second_packet_codes.fan_byte;
message2[2] = 0x00;
message2[3] = second_packet_codes.suffix.byte3;
message2[4] = second_packet_codes.suffix.byte4;
message2[5] = second_packet_codes.suffix.byte5;
break;
}
}
// Log final messages being transmitted
// Transmit using proper Toshiba protocol timing
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
// Use existing Toshiba encode function for proper timing
this->encode_(data, message1, RAS_2819T_MESSAGE_LENGTH, 1);
if (this->mode != climate::CLIMATE_MODE_OFF) {
// Send second packet with gap
this->encode_(data, message2, RAS_2819T_MESSAGE_LENGTH, 0);
}
transmit.perform();
// Update all state tracking after successful transmission
this->last_swing_mode_ = this->swing_mode;
this->last_mode_ = this->mode;
this->last_fan_mode_ = this->fan_mode;
this->last_target_temperature_ = this->target_temperature;
}
uint8_t ToshibaClimate::is_valid_rac_pt1411hwru_header_(const uint8_t *message) {
const std::vector<uint8_t> header{RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER,
RAC_PT1411HWRU_SWING_HEADER};
@@ -464,11 +967,11 @@ bool ToshibaClimate::compare_rac_pt1411hwru_packets_(const uint8_t *message1, co
bool ToshibaClimate::is_valid_rac_pt1411hwru_message_(const uint8_t *message) {
uint8_t checksum = 0;
switch (is_valid_rac_pt1411hwru_header_(message)) {
switch (this->is_valid_rac_pt1411hwru_header_(message)) {
case RAC_PT1411HWRU_MESSAGE_HEADER0:
case RAC_PT1411HWRU_CS_HEADER:
case RAC_PT1411HWRU_SWING_HEADER:
if (is_valid_rac_pt1411hwru_header_(message) && (message[2] == static_cast<uint8_t>(~message[3])) &&
if (this->is_valid_rac_pt1411hwru_header_(message) && (message[2] == static_cast<uint8_t>(~message[3])) &&
(message[4] == static_cast<uint8_t>(~message[5]))) {
return true;
}
@@ -490,7 +993,103 @@ bool ToshibaClimate::is_valid_rac_pt1411hwru_message_(const uint8_t *message) {
return false;
}
bool ToshibaClimate::process_ras_2819t_command_(const remote_base::ToshibaAcData &toshiba_data) {
// Check for power-off command (single packet)
if (toshiba_data.rc_code_2 == 0 && toshiba_data.rc_code_1 == RAS_2819T_POWER_OFF_COMMAND) {
this->mode = climate::CLIMATE_MODE_OFF;
ESP_LOGI(TAG, "Mode: OFF");
this->publish_state();
return true;
}
// Check for swing toggle command (single packet)
if (toshiba_data.rc_code_2 == 0 && toshiba_data.rc_code_1 == RAS_2819T_SWING_TOGGLE) {
// Toggle swing mode
if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL) {
this->swing_mode = climate::CLIMATE_SWING_OFF;
ESP_LOGI(TAG, "Swing: OFF");
} else {
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
ESP_LOGI(TAG, "Swing: VERTICAL");
}
this->publish_state();
return true;
}
// Handle regular two-packet commands (mode/temperature/fan changes)
if (toshiba_data.rc_code_2 != 0) {
// Convert to byte array for easier processing
uint8_t message1[6], message2[6];
for (uint8_t i = 0; i < 6; i++) {
message1[i] = (toshiba_data.rc_code_1 >> (40 - i * 8)) & 0xFF;
message2[i] = (toshiba_data.rc_code_2 >> (40 - i * 8)) & 0xFF;
}
// Decode the protocol using message1 (rc_code_1)
uint8_t temp_code = message1[4];
// Decode mode - check bytes 2-3 pattern and temperature code
if ((message1[2] == 0x7B) && (message1[3] == 0x84)) {
// OFF mode has specific pattern
this->mode = climate::CLIMATE_MODE_OFF;
ESP_LOGI(TAG, "Mode: OFF");
} else if ((message1[2] == 0x1F) && (message1[3] == 0xE0)) {
// 0x1FE0 pattern is used for AUTO, DRY, and low-temp COOL
if ((temp_code & 0x0F) == 0x08) {
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
ESP_LOGI(TAG, "Mode: AUTO");
} else if ((temp_code & 0x0F) == 0x04) {
this->mode = climate::CLIMATE_MODE_DRY;
ESP_LOGI(TAG, "Mode: DRY");
} else {
this->mode = climate::CLIMATE_MODE_COOL;
ESP_LOGI(TAG, "Mode: COOL (low temp)");
}
} else {
// Variable fan speed patterns - decode by temperature code
if ((temp_code & 0x0F) == 0x0C) {
this->mode = climate::CLIMATE_MODE_HEAT;
ESP_LOGI(TAG, "Mode: HEAT");
} else if (message1[5] == 0x1B) {
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
ESP_LOGI(TAG, "Mode: FAN_ONLY");
} else {
this->mode = climate::CLIMATE_MODE_COOL;
ESP_LOGI(TAG, "Mode: COOL");
}
}
// Decode fan speed from rc_code_1
uint16_t fan_code = (message1[2] << 8) | message1[3];
this->fan_mode = decode_ras_2819t_fan_mode(fan_code);
// Decode temperature
if (this->mode != climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_FAN_ONLY) {
this->target_temperature = decode_ras_2819t_temperature(temp_code);
}
this->publish_state();
return true;
} else {
ESP_LOGD(TAG, "Unknown single-packet RAS-2819T command: 0x%" PRIX64, toshiba_data.rc_code_1);
return false;
}
}
bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
// Try modern ToshibaAcProtocol decoder first (handles RAS-2819T and potentially others)
remote_base::ToshibaAcProtocol toshiba_protocol;
auto decode_result = toshiba_protocol.decode(data);
if (decode_result.has_value()) {
auto toshiba_data = decode_result.value();
// Validate and process RAS-2819T commands
if (is_valid_ras_2819t_command(toshiba_data.rc_code_1, toshiba_data.rc_code_2)) {
return this->process_ras_2819t_command_(toshiba_data);
}
}
// Fall back to generic processing for older protocols
uint8_t message[18] = {0};
uint8_t message_length = TOSHIBA_HEADER_LENGTH, temperature_code = 0;
@@ -499,11 +1098,11 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
return false;
}
// Read incoming bits into buffer
if (!decode_(&data, message, message_length)) {
if (!this->decode_(&data, message, message_length)) {
return false;
}
// Determine incoming message protocol version and/or length
if (is_valid_rac_pt1411hwru_header_(message)) {
if (this->is_valid_rac_pt1411hwru_header_(message)) {
// We already received four bytes
message_length = RAC_PT1411HWRU_MESSAGE_LENGTH - 4;
} else if ((message[0] ^ message[1] ^ message[2]) != message[3]) {
@@ -514,11 +1113,11 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
message_length = message[2] + 2;
}
// Decode the remaining bytes
if (!decode_(&data, &message[4], message_length)) {
if (!this->decode_(&data, &message[4], message_length)) {
return false;
}
// If this is a RAC-PT1411HWRU message, we expect the first packet a second time and also possibly a third packet
if (is_valid_rac_pt1411hwru_header_(message)) {
if (this->is_valid_rac_pt1411hwru_header_(message)) {
// There is always a space between packets
if (!data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) {
return false;
@@ -527,7 +1126,7 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) {
return false;
}
if (!decode_(&data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
if (!this->decode_(&data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
return false;
}
// If this is a RAC-PT1411HWRU message, there may also be a third packet.
@@ -535,25 +1134,25 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) {
// Validate header 3
data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE);
if (decode_(&data, &message[12], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
if (!is_valid_rac_pt1411hwru_message_(&message[12])) {
if (this->decode_(&data, &message[12], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
if (!this->is_valid_rac_pt1411hwru_message_(&message[12])) {
// If a third packet was received but the checksum is not valid, fail
return false;
}
}
}
if (!compare_rac_pt1411hwru_packets_(&message[0], &message[6])) {
if (!this->compare_rac_pt1411hwru_packets_(&message[0], &message[6])) {
// If the first two packets don't match each other, fail
return false;
}
if (!is_valid_rac_pt1411hwru_message_(&message[0])) {
if (!this->is_valid_rac_pt1411hwru_message_(&message[0])) {
// If the first packet isn't valid, fail
return false;
}
}
// Header has been verified, now determine protocol version and set the climate component properties
switch (is_valid_rac_pt1411hwru_header_(message)) {
switch (this->is_valid_rac_pt1411hwru_header_(message)) {
// Power, temperature, mode, fan speed
case RAC_PT1411HWRU_MESSAGE_HEADER0:
// Get the mode
@@ -608,7 +1207,7 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
break;
}
// Get the target temperature
if (is_valid_rac_pt1411hwru_message_(&message[12])) {
if (this->is_valid_rac_pt1411hwru_message_(&message[12])) {
temperature_code =
(message[4] >> 4) | (message[14] & RAC_PT1411HWRU_FLAG_FRAC) | (message[15] & RAC_PT1411HWRU_FLAG_NEG);
if (message[15] & RAC_PT1411HWRU_FLAG_FAH) {

View File

@@ -1,6 +1,7 @@
#pragma once
#include "esphome/components/climate_ir/climate_ir.h"
#include "esphome/components/remote_base/toshiba_ac_protocol.h"
namespace esphome {
namespace toshiba {
@@ -10,6 +11,7 @@ enum Model {
MODEL_GENERIC = 0, // Temperature range is from 17 to 30
MODEL_RAC_PT1411HWRU_C = 1, // Temperature range is from 16 to 30
MODEL_RAC_PT1411HWRU_F = 2, // Temperature range is from 16 to 30
MODEL_RAS_2819T = 3, // RAS-2819T protocol variant, temperature range 18 to 30
};
// Supported temperature ranges
@@ -19,6 +21,8 @@ const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN = 16.0;
const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX = 30.0;
const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN = 60.0;
const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MAX = 86.0;
const float TOSHIBA_RAS_2819T_TEMP_C_MIN = 18.0;
const float TOSHIBA_RAS_2819T_TEMP_C_MAX = 30.0;
class ToshibaClimate : public climate_ir::ClimateIR {
public:
@@ -35,6 +39,9 @@ class ToshibaClimate : public climate_ir::ClimateIR {
void transmit_generic_();
void transmit_rac_pt1411hwru_();
void transmit_rac_pt1411hwru_temp_(bool cs_state = true, bool cs_send_update = true);
void transmit_ras_2819t_();
// Process RAS-2819T IR command data
bool process_ras_2819t_command_(const remote_base::ToshibaAcData &toshiba_data);
// Returns the header if valid, else returns zero
uint8_t is_valid_rac_pt1411hwru_header_(const uint8_t *message);
// Returns true if message is a valid RAC-PT1411HWRU IR message, regardless if first or second packet
@@ -43,11 +50,26 @@ class ToshibaClimate : public climate_ir::ClimateIR {
bool compare_rac_pt1411hwru_packets_(const uint8_t *message1, const uint8_t *message2);
bool on_receive(remote_base::RemoteReceiveData data) override;
private:
// RAS-2819T state tracking for swing mode optimization
climate::ClimateSwingMode last_swing_mode_{climate::CLIMATE_SWING_OFF};
climate::ClimateMode last_mode_{climate::CLIMATE_MODE_OFF};
optional<climate::ClimateFanMode> last_fan_mode_{};
float last_target_temperature_{24.0f};
float temperature_min_() {
return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MIN : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN;
if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F)
return TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN;
if (this->model_ == MODEL_RAS_2819T)
return TOSHIBA_RAS_2819T_TEMP_C_MIN;
return TOSHIBA_GENERIC_TEMP_C_MIN; // Default to GENERIC for unknown models
}
float temperature_max_() {
return (this->model_ == MODEL_GENERIC) ? TOSHIBA_GENERIC_TEMP_C_MAX : TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX;
if (this->model_ == MODEL_RAC_PT1411HWRU_C || this->model_ == MODEL_RAC_PT1411HWRU_F)
return TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX;
if (this->model_ == MODEL_RAS_2819T)
return TOSHIBA_RAS_2819T_TEMP_C_MAX;
return TOSHIBA_GENERIC_TEMP_C_MAX; // Default to GENERIC for unknown models
}
std::set<climate::ClimateSwingMode> toshiba_swing_modes_() {
return (this->model_ == MODEL_GENERIC)

View File

@@ -380,24 +380,25 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
if (this->on_connect_) {
this->on_connect_(rsp);
}
this->sessions_.insert(rsp);
this->sessions_.push_back(rsp);
}
void AsyncEventSource::loop() {
// Clean up dead sessions safely
// This follows the ESP-IDF pattern where free_ctx marks resources as dead
// and the main loop handles the actual cleanup to avoid race conditions
auto it = this->sessions_.begin();
while (it != this->sessions_.end()) {
auto *ses = *it;
for (size_t i = 0; i < this->sessions_.size();) {
auto *ses = this->sessions_[i];
// If the session has a dead socket (marked by destroy callback)
if (ses->fd_.load() == 0) {
ESP_LOGD(TAG, "Removing dead event source session");
it = this->sessions_.erase(it);
delete ses; // NOLINT(cppcoreguidelines-owning-memory)
// Remove by swapping with last element (O(1) removal, order doesn't matter for sessions)
this->sessions_[i] = this->sessions_.back();
this->sessions_.pop_back();
} else {
ses->loop();
++it;
++i;
}
}
}

View File

@@ -8,7 +8,6 @@
#include <functional>
#include <list>
#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>
@@ -315,7 +314,10 @@ class AsyncEventSource : public AsyncWebHandler {
protected:
std::string url_;
std::set<AsyncEventSourceResponse *> sessions_;
// Use vector instead of set: SSE sessions are typically 1-5 connections (browsers, dashboards).
// Linear search is faster than red-black tree overhead for this small dataset.
// Only operations needed: add session, remove session, iterate sessions - no need for sorted order.
std::vector<AsyncEventSourceResponse *> sessions_;
connect_handler_t on_connect_{};
esphome::web_server::WebServer *web_server_;
};

View File

@@ -402,8 +402,8 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
# Disable Enterprise WiFi support if no EAP is configured
if CORE.is_esp32 and not has_eap:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", False)
if CORE.is_esp32:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap)
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
@@ -447,6 +447,8 @@ async def to_code(config):
var.get_disconnect_trigger(), [], on_disconnect_config
)
CORE.add_job(final_step)
@automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({}))
async def wifi_connected_to_code(config, condition_id, template_arg, args):
@@ -468,6 +470,28 @@ async def wifi_disable_to_code(config, action_id, template_arg, args):
return cg.new_Pvariable(action_id, template_arg)
KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results"
def request_wifi_scan_results():
"""Request that WiFi scan results be kept in memory after connection.
Components that need access to scan results after WiFi is connected should
call this function during their code generation. This prevents the WiFi component from
freeing scan result memory after successful connection.
"""
CORE.data[KEEP_SCAN_RESULTS_KEY] = True
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step():
"""Final code generation step to configure scan result retention."""
if CORE.data.get(KEEP_SCAN_RESULTS_KEY, False):
cg.add(
cg.RawExpression("wifi::global_wifi_component->set_keep_scan_results(true)")
)
@automation.register_action(
"wifi.configure",
WiFiConfigureAction,

View File

@@ -549,7 +549,7 @@ void WiFiComponent::start_scanning() {
// Using insertion sort instead of std::stable_sort saves flash memory
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
// IMPORTANT: This sort is stable (preserves relative order of equal elements)
static void insertion_sort_scan_results(std::vector<WiFiScanResult> &results) {
template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
const size_t size = results.size();
for (size_t i = 1; i < size; i++) {
// Make a copy to avoid issues with move semantics during comparison
@@ -713,6 +713,12 @@ void WiFiComponent::check_connecting_finished() {
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
this->num_retried_ = 0;
// Free scan results memory unless a component needs them
if (!this->keep_scan_results_) {
this->scan_result_.clear();
this->scan_result_.shrink_to_fit();
}
if (this->fast_connect_) {
this->save_fast_connect_settings_();
}

View File

@@ -121,6 +121,14 @@ struct EAPAuth {
using bssid_t = std::array<uint8_t, 6>;
// Use std::vector for RP2040 since scan count is unknown (callback-based)
// Use FixedVector for other platforms where count is queried first
#ifdef USE_RP2040
template<typename T> using wifi_scan_vector_t = std::vector<T>;
#else
template<typename T> using wifi_scan_vector_t = FixedVector<T>;
#endif
class WiFiAP {
public:
void set_ssid(const std::string &ssid);
@@ -278,7 +286,7 @@ class WiFiComponent : public Component {
const std::string &get_use_address() const;
void set_use_address(const std::string &use_address);
const std::vector<WiFiScanResult> &get_scan_result() const { return scan_result_; }
const wifi_scan_vector_t<WiFiScanResult> &get_scan_result() const { return scan_result_; }
network::IPAddress wifi_soft_ap_ip();
@@ -316,6 +324,7 @@ class WiFiComponent : public Component {
int8_t wifi_rssi();
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; }
Trigger<> *get_connect_trigger() const { return this->connect_trigger_; };
Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; };
@@ -385,7 +394,7 @@ class WiFiComponent : public Component {
std::string use_address_;
std::vector<WiFiAP> sta_;
std::vector<WiFiSTAPriority> sta_priorities_;
std::vector<WiFiScanResult> scan_result_;
wifi_scan_vector_t<WiFiScanResult> scan_result_;
WiFiAP selected_ap_;
WiFiAP ap_;
optional<float> output_power_;
@@ -424,6 +433,7 @@ class WiFiComponent : public Component {
#endif
bool enable_on_boot_;
bool got_ipv4_address_{false};
bool keep_scan_results_{false};
// Pointers at the end (naturally aligned)
Trigger<> *connect_trigger_{new Trigger<>()};

View File

@@ -696,7 +696,15 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
this->retry_connect();
return;
}
// Count the number of results first
auto *head = reinterpret_cast<bss_info *>(arg);
size_t count = 0;
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
count++;
}
this->scan_result_.init(count);
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi,

View File

@@ -784,7 +784,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
}
records.resize(number);
scan_result_.reserve(number);
scan_result_.init(number);
for (int i = 0; i < number; i++) {
auto &record = records[i];
bssid_t bssid;

View File

@@ -411,7 +411,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
if (num < 0)
return;
this->scan_result_.reserve(static_cast<unsigned int>(num));
this->scan_result_.init(static_cast<unsigned int>(num));
for (int i = 0; i < num; i++) {
String ssid = WiFi.SSID(i);
wifi_auth_mode_t authmode = WiFi.encryptionType(i);

View File

@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import text_sensor
from esphome.components import text_sensor, wifi
import esphome.config_validation as cv
from esphome.const import (
CONF_BSSID,
@@ -77,7 +77,9 @@ async def to_code(config):
await setup_conf(config, CONF_SSID)
await setup_conf(config, CONF_BSSID)
await setup_conf(config, CONF_MAC_ADDRESS)
await setup_conf(config, CONF_SCAN_RESULTS)
if CONF_SCAN_RESULTS in config:
await setup_conf(config, CONF_SCAN_RESULTS)
wifi.request_wifi_scan_results()
await setup_conf(config, CONF_DNS_ADDRESS)
if conf := config.get(CONF_IP_ADDRESS):
wifi_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])

View File

@@ -222,18 +222,25 @@ def copy_files():
] in ["xiao_ble"]:
fake_board_manifest = """
{
"frameworks": [
"zephyr"
],
"name": "esphome nrf52",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104
},
"url": "https://esphome.io/",
"vendor": "esphome"
"frameworks": [
"zephyr"
],
"name": "esphome nrf52",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200
},
"url": "https://esphome.io/",
"vendor": "esphome",
"build": {
"softdevice": {
"sd_fwid": "0x00B6"
}
}
}
"""
write_file_if_changed(
CORE.relative_build_path(f"boards/{zephyr_data()[KEY_BOARD]}.json"),
fake_board_manifest,

View File

@@ -12,7 +12,7 @@ from typing import Any
import voluptuous as vol
from esphome import core, loader, pins, yaml_util
from esphome.config_helpers import Extend, Remove
from esphome.config_helpers import Extend, Remove, merge_dicts_ordered
import esphome.config_validation as cv
from esphome.const import (
CONF_ESPHOME,
@@ -922,10 +922,9 @@ def validate_config(
if CONF_SUBSTITUTIONS in config or command_line_substitutions:
from esphome.components import substitutions
result[CONF_SUBSTITUTIONS] = {
**(config.get(CONF_SUBSTITUTIONS) or {}),
**command_line_substitutions,
}
result[CONF_SUBSTITUTIONS] = merge_dicts_ordered(
config.get(CONF_SUBSTITUTIONS) or {}, command_line_substitutions
)
result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS)
try:
substitutions.do_substitution_pass(config, command_line_substitutions)

View File

@@ -10,6 +10,7 @@ from esphome.const import (
PlatformFramework,
)
from esphome.core import CORE
from esphome.util import OrderedDict
# Pre-build lookup map from (platform, framework) tuples to PlatformFramework enum
_PLATFORM_FRAMEWORK_LOOKUP = {
@@ -17,6 +18,25 @@ _PLATFORM_FRAMEWORK_LOOKUP = {
}
def merge_dicts_ordered(*dicts: dict) -> OrderedDict:
"""Merge multiple dicts into an OrderedDict, preserving key order.
This is a helper to ensure that dictionary merging preserves OrderedDict type,
which is important for operations like move_to_end().
Args:
*dicts: Variable number of dictionaries to merge (later dicts override earlier ones)
Returns:
OrderedDict with merged contents
"""
result = OrderedDict()
for d in dicts:
if d:
result.update(d)
return result
class Extend:
def __init__(self, value):
self.value = value
@@ -60,7 +80,11 @@ def merge_config(full_old, full_new):
if isinstance(new, dict):
if not isinstance(old, dict):
return new
res = old.copy()
# Preserve OrderedDict type by copying to OrderedDict if either input is OrderedDict
if isinstance(old, OrderedDict) or isinstance(new, OrderedDict):
res = OrderedDict(old)
else:
res = old.copy()
for k, v in new.items():
if isinstance(v, Remove) and k in old:
del res[k]

View File

@@ -244,6 +244,20 @@ RESERVED_IDS = [
"uart0",
"uart1",
"uart2",
# ESP32 ROM functions
"crc16_be",
"crc16_le",
"crc32_be",
"crc32_le",
"crc8_be",
"crc8_le",
"dbg_state",
"debug_timer",
"one_bits",
"recv_packet",
"send_packet",
"check_pos",
"software_reset",
]

View File

@@ -696,6 +696,7 @@ CONF_OPEN_DRAIN = "open_drain"
CONF_OPEN_DRAIN_INTERRUPT = "open_drain_interrupt"
CONF_OPEN_DURATION = "open_duration"
CONF_OPEN_ENDSTOP = "open_endstop"
CONF_OPENTHREAD = "openthread"
CONF_OPERATION = "operation"
CONF_OPTIMISTIC = "optimistic"
CONF_OPTION = "option"
@@ -836,6 +837,7 @@ CONF_RMT_CHANNEL = "rmt_channel"
CONF_RMT_SYMBOLS = "rmt_symbols"
CONF_ROTATION = "rotation"
CONF_ROW = "row"
CONF_ROWS = "rows"
CONF_RS_PIN = "rs_pin"
CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance"
CONF_RTD_WIRES = "rtd_wires"
@@ -1298,6 +1300,7 @@ DEVICE_CLASS_SULPHUR_DIOXIDE = "sulphur_dioxide"
DEVICE_CLASS_SWITCH = "switch"
DEVICE_CLASS_TAMPER = "tamper"
DEVICE_CLASS_TEMPERATURE = "temperature"
DEVICE_CLASS_TEMPERATURE_DELTA = "temperature_delta"
DEVICE_CLASS_TIMESTAMP = "timestamp"
DEVICE_CLASS_UPDATE = "update"
DEVICE_CLASS_VIBRATION = "vibration"

View File

@@ -7,6 +7,7 @@
#include "esphome/core/preferences.h"
#include "esphome/core/scheduler.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include <vector>
@@ -14,7 +15,7 @@ namespace esphome {
template<typename... Ts> class AndCondition : public Condition<Ts...> {
public:
explicit AndCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
explicit AndCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
bool check(Ts... x) override {
for (auto *condition : this->conditions_) {
if (!condition->check(x...))
@@ -25,12 +26,12 @@ template<typename... Ts> class AndCondition : public Condition<Ts...> {
}
protected:
std::vector<Condition<Ts...> *> conditions_;
FixedVector<Condition<Ts...> *> conditions_;
};
template<typename... Ts> class OrCondition : public Condition<Ts...> {
public:
explicit OrCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
explicit OrCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
bool check(Ts... x) override {
for (auto *condition : this->conditions_) {
if (condition->check(x...))
@@ -41,7 +42,7 @@ template<typename... Ts> class OrCondition : public Condition<Ts...> {
}
protected:
std::vector<Condition<Ts...> *> conditions_;
FixedVector<Condition<Ts...> *> conditions_;
};
template<typename... Ts> class NotCondition : public Condition<Ts...> {
@@ -55,7 +56,7 @@ template<typename... Ts> class NotCondition : public Condition<Ts...> {
template<typename... Ts> class XorCondition : public Condition<Ts...> {
public:
explicit XorCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
explicit XorCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
bool check(Ts... x) override {
size_t result = 0;
for (auto *condition : this->conditions_) {
@@ -66,7 +67,7 @@ template<typename... Ts> class XorCondition : public Condition<Ts...> {
}
protected:
std::vector<Condition<Ts...> *> conditions_;
FixedVector<Condition<Ts...> *> conditions_;
};
template<typename... Ts> class LambdaCondition : public Condition<Ts...> {

View File

@@ -202,7 +202,7 @@
#define USB_HOST_MAX_REQUESTS 16
#ifdef USE_ARDUINO
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1)
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2)
#define USE_ETHERNET
#define USE_ETHERNET_KSZ8081
#endif

View File

@@ -197,6 +197,18 @@ template<typename T> class FixedVector {
public:
FixedVector() = default;
/// Constructor from initializer list - allocates exact size needed
/// This enables brace initialization: FixedVector<int> v = {1, 2, 3};
FixedVector(std::initializer_list<T> init_list) {
init(init_list.size());
size_t idx = 0;
for (const auto &item : init_list) {
new (data_ + idx) T(item);
++idx;
}
size_ = init_list.size();
}
~FixedVector() { cleanup_(); }
// Disable copy operations (avoid accidental expensive copies)

View File

@@ -10,6 +10,10 @@ from esphome.helpers import get_bool_env
from .util.password import password_hash
# Sentinel file name used for CORE.config_path when dashboard initializes.
# This ensures .parent returns the config directory instead of root.
_DASHBOARD_SENTINEL_FILE = "___DASHBOARD_SENTINEL___.yaml"
class DashboardSettings:
"""Settings for the dashboard."""
@@ -48,7 +52,12 @@ class DashboardSettings:
self.config_dir = Path(args.configuration)
self.absolute_config_dir = self.config_dir.resolve()
self.verbose = args.verbose
CORE.config_path = self.config_dir / "."
# Set to a sentinel file so .parent gives us the config directory.
# Previously this was `os.path.join(self.config_dir, ".")` which worked because
# os.path.dirname("/config/.") returns "/config", but Path("/config/.").parent
# normalizes to Path("/config") first, then .parent returns Path("/"), breaking
# secret resolution. Using a sentinel file ensures .parent gives the correct directory.
CORE.config_path = self.config_dir / _DASHBOARD_SENTINEL_FILE
@property
def relative_url(self) -> str:

View File

@@ -242,7 +242,7 @@ def send_check(
def perform_ota(
sock: socket.socket, password: str, file_handle: io.IOBase, filename: Path
sock: socket.socket, password: str | None, file_handle: io.IOBase, filename: Path
) -> None:
file_contents = file_handle.read()
file_size = len(file_contents)
@@ -278,13 +278,13 @@ def perform_ota(
def perform_auth(
sock: socket.socket,
password: str,
password: str | None,
hash_func: Callable[..., Any],
nonce_size: int,
hash_name: str,
) -> None:
"""Perform challenge-response authentication using specified hash algorithm."""
if not password:
if password is None:
raise OTAError("ESP requests password, but no password given!")
nonce_bytes = receive_exactly(
@@ -385,7 +385,7 @@ def perform_ota(
def run_ota_impl_(
remote_host: str | list[str], remote_port: int, password: str, filename: Path
remote_host: str | list[str], remote_port: int, password: str | None, filename: Path
) -> tuple[int, str | None]:
from esphome.core import CORE
@@ -436,7 +436,7 @@ def run_ota_impl_(
def run_ota(
remote_host: str | list[str], remote_port: int, password: str, filename: Path
remote_host: str | list[str], remote_port: int, password: str | None, filename: Path
) -> tuple[int, str | None]:
try:
return run_ota_impl_(remote_host, remote_port, password, filename)

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