mirror of
https://github.com/esphome/esphome.git
synced 2025-11-02 08:01:50 +00:00
Compare commits
57 Commits
ota_handle
...
fan_fixed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
410afd196f | ||
|
|
4e6d74c981 | ||
|
|
fd64585f99 | ||
|
|
077cce9848 | ||
|
|
bd87e56bc7 | ||
|
|
58235049e3 | ||
|
|
29ed3c20af | ||
|
|
08aae39ea4 | ||
|
|
03fd114371 | ||
|
|
918650f15a | ||
|
|
287f65cbaf | ||
|
|
f18c70a256 | ||
|
|
6fb490f49b | ||
|
|
66cf7c3a3b | ||
|
|
f29021b5ef | ||
|
|
7549ca4d39 | ||
|
|
33e7a2101b | ||
|
|
59a216bfcb | ||
|
|
09d89000ad | ||
|
|
b6c9ece0e6 | ||
|
|
0bb6a6872d | ||
|
|
7169556722 | ||
|
|
372c162e6b | ||
|
|
b635689c29 | ||
|
|
e4aec7f413 | ||
|
|
bb99f68d33 | ||
|
|
47cbe74453 | ||
|
|
cc815fd683 | ||
|
|
4cc41606d1 | ||
|
|
6cf0a38b86 | ||
|
|
5e6ce6ee48 | ||
|
|
091c12cb48 | ||
|
|
39b93079e5 | ||
|
|
93c555ae87 | ||
|
|
977dd9dd34 | ||
|
|
fe6f877185 | ||
|
|
c7aef0016a | ||
|
|
c69e7f4e78 | ||
|
|
6d1ee10742 | ||
|
|
516889f35e | ||
|
|
26e4754673 | ||
|
|
a3b3032319 | ||
|
|
b0f764a37e | ||
|
|
5c7029623e | ||
|
|
fdb23a2c13 | ||
|
|
43bcd98649 | ||
|
|
274c0505f7 | ||
|
|
eaf0a367b4 | ||
|
|
657e6f0bce | ||
|
|
935acc7d5e | ||
|
|
acd24402dd | ||
|
|
ac36b97262 | ||
|
|
828f2addcd | ||
|
|
f11e8e36b5 | ||
|
|
788c402cfe | ||
|
|
04d127015c | ||
|
|
f559fad4fc |
2
.github/workflows/ci-api-proto.yml
vendored
2
.github/workflows/ci-api-proto.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
|
||||
56
.github/workflows/ci.yml
vendored
56
.github/workflows/ci.yml
vendored
@@ -180,6 +180,7 @@ jobs:
|
||||
memory_impact: ${{ steps.determine.outputs.memory-impact }}
|
||||
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
|
||||
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
|
||||
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
@@ -214,6 +215,7 @@ jobs:
|
||||
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
|
||||
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
|
||||
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
|
||||
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
|
||||
|
||||
integration-tests:
|
||||
name: Run integration tests
|
||||
@@ -458,7 +460,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
max-parallel: 2
|
||||
matrix:
|
||||
include:
|
||||
- id: clang-tidy
|
||||
@@ -536,59 +538,18 @@ jobs:
|
||||
run: script/ci-suggest-changes
|
||||
if: always()
|
||||
|
||||
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) > 0
|
||||
outputs:
|
||||
matrix: ${{ steps.split.outputs.components }}
|
||||
steps:
|
||||
- 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: Split components intelligently based on bus configurations
|
||||
id: split
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
||||
# 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" --directly-changed "$directly_changed" --batch-size 40 --output github)
|
||||
|
||||
echo "$output" >> $GITHUB_OUTPUT
|
||||
|
||||
test-build-components-split:
|
||||
name: Test components batch (${{ matrix.components }})
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
- test-build-components-splitter
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
|
||||
matrix:
|
||||
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
|
||||
components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
|
||||
steps:
|
||||
- name: Show disk space
|
||||
run: |
|
||||
@@ -849,7 +810,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -913,7 +874,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
@@ -943,13 +904,13 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
@@ -980,7 +941,6 @@ jobs:
|
||||
- clang-tidy-nosplit
|
||||
- clang-tidy-split
|
||||
- determine-jobs
|
||||
- test-build-components-splitter
|
||||
- test-build-components-split
|
||||
- pre-commit-ci-lite
|
||||
- memory-impact-target-branch
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
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@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
||||
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -138,7 +138,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.1
|
||||
rev: v0.14.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -207,14 +207,14 @@ def choose_upload_log_host(
|
||||
if has_mqtt_logging():
|
||||
resolved.append("MQTT")
|
||||
|
||||
if has_api() and has_non_ip_address():
|
||||
if has_api() and has_non_ip_address() and has_resolvable_address():
|
||||
resolved.extend(_resolve_with_cache(CORE.address, purpose))
|
||||
|
||||
elif purpose == Purpose.UPLOADING:
|
||||
if has_ota() and has_mqtt_ip_lookup():
|
||||
resolved.append("MQTTIP")
|
||||
|
||||
if has_ota() and has_non_ip_address():
|
||||
if has_ota() and has_non_ip_address() and has_resolvable_address():
|
||||
resolved.extend(_resolve_with_cache(CORE.address, purpose))
|
||||
else:
|
||||
resolved.append(device)
|
||||
@@ -318,7 +318,17 @@ def has_resolvable_address() -> bool:
|
||||
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
|
||||
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
|
||||
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
|
||||
return CORE.address is not None
|
||||
if CORE.address is None:
|
||||
return False
|
||||
|
||||
if has_ip_address():
|
||||
return True
|
||||
|
||||
if has_mdns():
|
||||
return True
|
||||
|
||||
# .local mDNS hostnames are only resolvable if mDNS is enabled
|
||||
return not CORE.address.endswith(".local")
|
||||
|
||||
|
||||
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
|
||||
|
||||
@@ -172,12 +172,6 @@ def alarm_control_panel_schema(
|
||||
return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel)
|
||||
ALARM_CONTROL_PANEL_SCHEMA.add_extra(
|
||||
cv.deprecated_schema_constant("alarm_control_panel")
|
||||
)
|
||||
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(AlarmControlPanel),
|
||||
|
||||
@@ -425,7 +425,7 @@ message ListEntitiesFanResponse {
|
||||
bool disabled_by_default = 9;
|
||||
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
EntityCategory entity_category = 11;
|
||||
repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"];
|
||||
repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"];
|
||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
// Deprecated in API version 1.6 - only used in deprecated fields
|
||||
|
||||
@@ -423,7 +423,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
|
||||
msg.supports_speed = traits.supports_speed();
|
||||
msg.supports_direction = traits.supports_direction();
|
||||
msg.supported_speed_count = traits.supported_speed_count();
|
||||
msg.supported_preset_modes = &traits.supported_preset_modes_for_api_();
|
||||
msg.supported_preset_modes = &traits.supported_preset_modes();
|
||||
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
void APIConnection::fan_command(const FanCommandRequest &msg) {
|
||||
|
||||
@@ -355,8 +355,8 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(10, this->icon_ref_);
|
||||
#endif
|
||||
buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category));
|
||||
for (const auto &it : *this->supported_preset_modes) {
|
||||
buffer.encode_string(12, it, true);
|
||||
for (const char *it : *this->supported_preset_modes) {
|
||||
buffer.encode_string(12, it, strlen(it), true);
|
||||
}
|
||||
#ifdef USE_DEVICES
|
||||
buffer.encode_uint32(13, this->device_id);
|
||||
@@ -376,8 +376,8 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const {
|
||||
#endif
|
||||
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
|
||||
if (!this->supported_preset_modes->empty()) {
|
||||
for (const auto &it : *this->supported_preset_modes) {
|
||||
size.add_length_force(1, it.size());
|
||||
for (const char *it : *this->supported_preset_modes) {
|
||||
size.add_length_force(1, strlen(it));
|
||||
}
|
||||
}
|
||||
#ifdef USE_DEVICES
|
||||
|
||||
@@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
|
||||
bool supports_speed{false};
|
||||
bool supports_direction{false};
|
||||
int32_t supported_speed_count{0};
|
||||
const std::set<std::string> *supported_preset_modes{};
|
||||
const std::vector<const char *> *supported_preset_modes{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
|
||||
@@ -548,11 +548,6 @@ def binary_sensor_schema(
|
||||
return _BINARY_SENSOR_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
BINARY_SENSOR_SCHEMA = binary_sensor_schema()
|
||||
BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
|
||||
|
||||
|
||||
async def setup_binary_sensor_core_(var, config):
|
||||
await setup_entity(var, config, "binary_sensor")
|
||||
|
||||
|
||||
@@ -84,11 +84,6 @@ def button_schema(
|
||||
return _BUTTON_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
BUTTON_SCHEMA = button_schema(Button)
|
||||
BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
|
||||
|
||||
|
||||
async def setup_button_core_(var, config):
|
||||
await setup_entity(var, config, "button")
|
||||
|
||||
|
||||
@@ -270,11 +270,6 @@ def climate_schema(
|
||||
return _CLIMATE_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
CLIMATE_SCHEMA = climate_schema(Climate)
|
||||
CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
|
||||
|
||||
|
||||
async def setup_climate_core_(var, config):
|
||||
await setup_entity(var, config, "climate")
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import logging
|
||||
|
||||
from esphome import core
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate, remote_base, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
|
||||
from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -52,26 +51,6 @@ def climate_ir_with_receiver_schema(
|
||||
)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
def deprecated_schema_constant(config):
|
||||
type: str = "unknown"
|
||||
if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
|
||||
type = str(id.type).split("::", maxsplit=1)[0]
|
||||
_LOGGER.warning(
|
||||
"Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
|
||||
"Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
|
||||
"If you are seeing this, report an issue to the external_component author and ask them to update it. "
|
||||
"https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
|
||||
"Component using this schema: %s",
|
||||
type,
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
|
||||
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)
|
||||
|
||||
|
||||
async def register_climate_ir(var, config):
|
||||
await cg.register_component(var, config)
|
||||
await remote_base.register_transmittable(var, config)
|
||||
|
||||
@@ -151,11 +151,6 @@ def cover_schema(
|
||||
return _COVER_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
COVER_SCHEMA = cover_schema(Cover)
|
||||
COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
|
||||
|
||||
|
||||
async def setup_cover_core_(var, config):
|
||||
await setup_entity(var, config, "cover")
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "e131_addressable_light_effect.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome {
|
||||
namespace e131 {
|
||||
|
||||
@@ -76,14 +78,14 @@ void E131Component::loop() {
|
||||
}
|
||||
|
||||
void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
|
||||
if (light_effects_.count(light_effect)) {
|
||||
if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
|
||||
light_effect->get_last_universe());
|
||||
|
||||
light_effects_.insert(light_effect);
|
||||
light_effects_.push_back(light_effect);
|
||||
|
||||
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
|
||||
join_(universe);
|
||||
@@ -91,14 +93,17 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
|
||||
}
|
||||
|
||||
void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
|
||||
if (!light_effects_.count(light_effect)) {
|
||||
auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect);
|
||||
if (it == light_effects_.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
|
||||
light_effect->get_last_universe());
|
||||
|
||||
light_effects_.erase(light_effect);
|
||||
// Swap with last element and pop for O(1) removal (order doesn't matter)
|
||||
*it = light_effects_.back();
|
||||
light_effects_.pop_back();
|
||||
|
||||
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
|
||||
leave_(universe);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <cinttypes>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
@@ -47,9 +46,8 @@ class E131Component : public esphome::Component {
|
||||
|
||||
E131ListenMethod listen_method_{E131_MULTICAST};
|
||||
std::unique_ptr<socket::Socket> socket_;
|
||||
std::set<E131AddressableLightEffect *> light_effects_;
|
||||
std::vector<E131AddressableLightEffect *> light_effects_;
|
||||
std::map<int, int> universe_consumers_;
|
||||
std::map<int, E131Packet> universe_packets_;
|
||||
};
|
||||
|
||||
} // namespace e131
|
||||
|
||||
@@ -85,11 +85,6 @@ def event_schema(
|
||||
return _EVENT_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
EVENT_SCHEMA = event_schema()
|
||||
EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event"))
|
||||
|
||||
|
||||
async def setup_event_core_(var, config, *, event_types: list[str]):
|
||||
await setup_entity(var, config, "event")
|
||||
|
||||
|
||||
@@ -189,10 +189,6 @@ def fan_schema(
|
||||
return _FAN_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
FAN_SCHEMA = fan_schema(Fan)
|
||||
FAN_SCHEMA.add_extra(cv.deprecated_schema_constant("fan"))
|
||||
|
||||
_PRESET_MODES_SCHEMA = cv.All(
|
||||
cv.ensure_list(cv.string_strict),
|
||||
cv.Length(min=1),
|
||||
|
||||
@@ -51,7 +51,14 @@ void FanCall::validate_() {
|
||||
|
||||
if (!this->preset_mode_.empty()) {
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
if (preset_modes.find(this->preset_mode_) == preset_modes.end()) {
|
||||
bool found = false;
|
||||
for (const auto &mode : preset_modes) {
|
||||
if (strcmp(mode, this->preset_mode_.c_str()) == 0) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str());
|
||||
this->preset_mode_.clear();
|
||||
}
|
||||
@@ -92,11 +99,12 @@ FanCall FanRestoreState::to_call(Fan &fan) {
|
||||
call.set_speed(this->speed);
|
||||
call.set_direction(this->direction);
|
||||
|
||||
if (fan.get_traits().supports_preset_modes()) {
|
||||
auto traits = fan.get_traits();
|
||||
if (traits.supports_preset_modes()) {
|
||||
// Use stored preset index to get preset name
|
||||
const auto &preset_modes = fan.get_traits().supported_preset_modes();
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
if (this->preset_mode < preset_modes.size()) {
|
||||
call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode));
|
||||
call.set_preset_mode(preset_modes[this->preset_mode]);
|
||||
}
|
||||
}
|
||||
return call;
|
||||
@@ -107,11 +115,12 @@ void FanRestoreState::apply(Fan &fan) {
|
||||
fan.speed = this->speed;
|
||||
fan.direction = this->direction;
|
||||
|
||||
if (fan.get_traits().supports_preset_modes()) {
|
||||
auto traits = fan.get_traits();
|
||||
if (traits.supports_preset_modes()) {
|
||||
// Use stored preset index to get preset name
|
||||
const auto &preset_modes = fan.get_traits().supported_preset_modes();
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
if (this->preset_mode < preset_modes.size()) {
|
||||
fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode);
|
||||
fan.preset_mode = preset_modes[this->preset_mode];
|
||||
}
|
||||
}
|
||||
fan.publish_state();
|
||||
@@ -182,18 +191,25 @@ void Fan::save_state_() {
|
||||
return;
|
||||
}
|
||||
|
||||
auto traits = this->get_traits();
|
||||
|
||||
FanRestoreState state{};
|
||||
state.state = this->state;
|
||||
state.oscillating = this->oscillating;
|
||||
state.speed = this->speed;
|
||||
state.direction = this->direction;
|
||||
|
||||
if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) {
|
||||
const auto &preset_modes = this->get_traits().supported_preset_modes();
|
||||
if (traits.supports_preset_modes() && !this->preset_mode.empty()) {
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
// Store index of current preset mode
|
||||
auto preset_iterator = preset_modes.find(this->preset_mode);
|
||||
if (preset_iterator != preset_modes.end())
|
||||
state.preset_mode = std::distance(preset_modes.begin(), preset_iterator);
|
||||
size_t i = 0;
|
||||
for (const auto &mode : preset_modes) {
|
||||
if (strcmp(mode, this->preset_mode.c_str()) == 0) {
|
||||
state.preset_mode = i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
this->rtc_.save(&state);
|
||||
@@ -216,8 +232,8 @@ void Fan::dump_traits_(const char *tag, const char *prefix) {
|
||||
}
|
||||
if (traits.supports_preset_modes()) {
|
||||
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
|
||||
for (const std::string &s : traits.supported_preset_modes())
|
||||
ESP_LOGCONFIG(tag, "%s - %s", prefix, s.c_str());
|
||||
for (const char *s : traits.supported_preset_modes())
|
||||
ESP_LOGCONFIG(tag, "%s - %s", prefix, s);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace esphome {
|
||||
#include <vector>
|
||||
#include <initializer_list>
|
||||
|
||||
#ifdef USE_API
|
||||
namespace api {
|
||||
class APIConnection;
|
||||
} // namespace api
|
||||
#endif
|
||||
namespace esphome {
|
||||
|
||||
namespace fan {
|
||||
|
||||
@@ -36,27 +30,27 @@ class FanTraits {
|
||||
/// Set whether this fan supports changing direction
|
||||
void set_direction(bool direction) { this->direction_ = direction; }
|
||||
/// Return the preset modes supported by the fan.
|
||||
std::set<std::string> supported_preset_modes() const { return this->preset_modes_; }
|
||||
/// Set the preset modes supported by the fan.
|
||||
void set_supported_preset_modes(const std::set<std::string> &preset_modes) { this->preset_modes_ = preset_modes; }
|
||||
const std::vector<const char *> &supported_preset_modes() const { return this->preset_modes_; }
|
||||
/// Set the preset modes supported by the fan (from initializer list).
|
||||
void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) {
|
||||
this->preset_modes_ = preset_modes;
|
||||
}
|
||||
/// Set the preset modes supported by the fan (from vector).
|
||||
void set_supported_preset_modes(const std::vector<const char *> &preset_modes) { this->preset_modes_ = preset_modes; }
|
||||
|
||||
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
|
||||
void set_supported_preset_modes(const std::vector<std::string> &preset_modes) = delete;
|
||||
void set_supported_preset_modes(std::initializer_list<std::string> preset_modes) = delete;
|
||||
|
||||
/// Return if preset modes are supported
|
||||
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
|
||||
|
||||
protected:
|
||||
#ifdef USE_API
|
||||
// The API connection is a friend class to access internal methods
|
||||
friend class api::APIConnection;
|
||||
// This method returns a reference to the internal preset modes set.
|
||||
// It is used by the API to avoid copying data when encoding messages.
|
||||
// Warning: Do not use this method outside of the API connection code.
|
||||
// It returns a reference to internal data that can be invalidated.
|
||||
const std::set<std::string> &supported_preset_modes_for_api_() const { return this->preset_modes_; }
|
||||
#endif
|
||||
bool oscillation_{false};
|
||||
bool speed_{false};
|
||||
bool direction_{false};
|
||||
int speed_count_{};
|
||||
std::set<std::string> preset_modes_{};
|
||||
std::vector<const char *> preset_modes_{};
|
||||
};
|
||||
|
||||
} // namespace fan
|
||||
|
||||
@@ -94,6 +94,8 @@ async def to_code(config):
|
||||
)
|
||||
use_interrupt = False
|
||||
|
||||
cg.add(var.set_use_interrupt(use_interrupt))
|
||||
if use_interrupt:
|
||||
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
||||
else:
|
||||
# Only generate call when disabling interrupts (default is true)
|
||||
cg.add(var.set_use_interrupt(use_interrupt))
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/output/binary_output.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
@@ -22,7 +20,7 @@ class HBridgeFan : public Component, public fan::Fan {
|
||||
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
|
||||
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
|
||||
void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
|
||||
void set_preset_modes(const std::set<std::string> &presets) { preset_modes_ = presets; }
|
||||
void set_preset_modes(std::initializer_list<const char *> presets) { preset_modes_ = presets; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -38,7 +36,7 @@ class HBridgeFan : public Component, public fan::Fan {
|
||||
int speed_count_{};
|
||||
DecayMode decay_mode_{DECAY_MODE_SLOW};
|
||||
fan::FanTraits traits_;
|
||||
std::set<std::string> preset_modes_{};
|
||||
std::vector<const char *> preset_modes_{};
|
||||
|
||||
void control(const fan::FanCall &call) override;
|
||||
void write_state_();
|
||||
|
||||
@@ -125,7 +125,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
|
||||
|
||||
case IMAGE_TYPE_RGB:
|
||||
#if LV_COLOR_DEPTH == 32
|
||||
switch (this->transparent_) {
|
||||
switch (this->transparency_) {
|
||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
|
||||
break;
|
||||
@@ -156,7 +156,8 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
|
||||
break;
|
||||
}
|
||||
#else
|
||||
this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
|
||||
this->dsc_.header.cf =
|
||||
this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -91,11 +91,6 @@ def lock_schema(
|
||||
return _LOCK_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
LOCK_SCHEMA = lock_schema()
|
||||
LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock"))
|
||||
|
||||
|
||||
async def _setup_lock_core(var, config):
|
||||
await setup_entity(var, config, "lock")
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from esphome import codegen as cg, config_validation as cv
|
||||
from esphome.const import CONF_ITEMS
|
||||
@@ -12,6 +13,7 @@ from esphome.core import ID, Lambda
|
||||
from esphome.cpp_generator import LambdaExpression, MockObj
|
||||
from esphome.cpp_types import uint32
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import Expression, SafeExpType
|
||||
|
||||
from .helpers import requires_component
|
||||
|
||||
@@ -42,7 +44,13 @@ def static_cast(type, value):
|
||||
def call_lambda(lamb: LambdaExpression):
|
||||
expr = lamb.content.strip()
|
||||
if expr.startswith("return") and expr.endswith(";"):
|
||||
return expr[6:][:-1].strip()
|
||||
return expr[6:-1].strip()
|
||||
# If lambda has parameters, call it with those parameter names
|
||||
# Parameter names come from hardcoded component code (like "x", "it", "event")
|
||||
# not from user input, so they're safe to use directly
|
||||
if lamb.parameters and lamb.parameters.parameters:
|
||||
param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters)
|
||||
return f"{lamb}({param_names})"
|
||||
return f"{lamb}()"
|
||||
|
||||
|
||||
@@ -65,10 +73,20 @@ class LValidator:
|
||||
return cv.returning_lambda(value)
|
||||
return self.validator(value)
|
||||
|
||||
async def process(self, value, args=()):
|
||||
async def process(
|
||||
self, value: Any, args: list[tuple[SafeExpType, str]] | None = None
|
||||
) -> Expression:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, Lambda):
|
||||
# Local import to avoid circular import
|
||||
from .lvcode import CodeContext, LambdaContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# CodeContext does not have get_automation_parameters
|
||||
# so we need to assert the type here
|
||||
assert isinstance(CodeContext.code_context, LambdaContext)
|
||||
args = args or CodeContext.code_context.get_automation_parameters()
|
||||
return cg.RawExpression(
|
||||
call_lambda(
|
||||
await cg.process_lambda(value, args, return_type=self.rtype)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import image
|
||||
from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw
|
||||
@@ -17,6 +19,7 @@ from esphome.cpp_generator import MockObj
|
||||
from esphome.cpp_types import ESPTime, int32, uint32
|
||||
from esphome.helpers import cpp_string_escape
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import Expression, SafeExpType
|
||||
|
||||
from . import types as ty
|
||||
from .defines import (
|
||||
@@ -388,11 +391,23 @@ class TextValidator(LValidator):
|
||||
return value
|
||||
return super().__call__(value)
|
||||
|
||||
async def process(self, value, args=()):
|
||||
async def process(
|
||||
self, value: Any, args: list[tuple[SafeExpType, str]] | None = None
|
||||
) -> Expression:
|
||||
# Local import to avoid circular import at module level
|
||||
|
||||
from .lvcode import CodeContext, LambdaContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# CodeContext does not have get_automation_parameters
|
||||
# so we need to assert the type here
|
||||
assert isinstance(CodeContext.code_context, LambdaContext)
|
||||
args = args or CodeContext.code_context.get_automation_parameters()
|
||||
|
||||
if isinstance(value, dict):
|
||||
if format_str := value.get(CONF_FORMAT):
|
||||
args = [str(x) for x in value[CONF_ARGS]]
|
||||
arg_expr = cg.RawExpression(",".join(args))
|
||||
str_args = [str(x) for x in value[CONF_ARGS]]
|
||||
arg_expr = cg.RawExpression(",".join(str_args))
|
||||
format_str = cpp_string_escape(format_str)
|
||||
return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()")
|
||||
if time_format := value.get(CONF_TIME_FORMAT):
|
||||
|
||||
@@ -164,6 +164,9 @@ class LambdaContext(CodeContext):
|
||||
code_text.append(text)
|
||||
return code_text
|
||||
|
||||
def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]:
|
||||
return self.parameters
|
||||
|
||||
async def __aenter__(self):
|
||||
await super().__aenter__()
|
||||
add_line_marks(self.where)
|
||||
@@ -178,9 +181,8 @@ class LvContext(LambdaContext):
|
||||
|
||||
added_lambda_count = 0
|
||||
|
||||
def __init__(self, args=None):
|
||||
self.args = args or LVGL_COMP_ARG
|
||||
super().__init__(parameters=self.args)
|
||||
def __init__(self):
|
||||
super().__init__(parameters=LVGL_COMP_ARG)
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await super().__aexit__(exc_type, exc_val, exc_tb)
|
||||
@@ -189,6 +191,11 @@ class LvContext(LambdaContext):
|
||||
cg.add(expression)
|
||||
return expression
|
||||
|
||||
def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]:
|
||||
# When generating automations, we don't want the `lv_component` parameter to be passed
|
||||
# to the lambda.
|
||||
return []
|
||||
|
||||
def __call__(self, *args):
|
||||
return self.add(*args)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from ..defines import CONF_WIDGET
|
||||
from ..lvcode import (
|
||||
API_EVENT,
|
||||
EVENT_ARG,
|
||||
LVGL_COMP_ARG,
|
||||
UPDATE_EVENT,
|
||||
LambdaContext,
|
||||
LvContext,
|
||||
@@ -30,7 +29,7 @@ async def to_code(config):
|
||||
await wait_for_widgets()
|
||||
async with LambdaContext(EVENT_ARG) as lamb:
|
||||
lv_add(sensor.publish_state(widget.get_value()))
|
||||
async with LvContext(LVGL_COMP_ARG):
|
||||
async with LvContext():
|
||||
lv_add(
|
||||
lvgl_static.add_event_cb(
|
||||
widget.obj,
|
||||
|
||||
@@ -33,7 +33,7 @@ from ..lv_validation import (
|
||||
pixels,
|
||||
size,
|
||||
)
|
||||
from ..lvcode import LocalVariable, lv, lv_assign
|
||||
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr
|
||||
from ..schemas import STYLE_PROPS, STYLE_REMAP, TEXT_SCHEMA, point_schema
|
||||
from ..types import LvType, ObjUpdateAction, WidgetType
|
||||
from . import Widget, get_widgets
|
||||
@@ -70,15 +70,18 @@ class CanvasType(WidgetType):
|
||||
width = config[CONF_WIDTH]
|
||||
height = config[CONF_HEIGHT]
|
||||
use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else ""
|
||||
lv.canvas_set_buffer(
|
||||
w.obj,
|
||||
lv.custom_mem_alloc(
|
||||
literal(f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})")
|
||||
),
|
||||
width,
|
||||
height,
|
||||
literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"),
|
||||
buf_size = literal(
|
||||
f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})"
|
||||
)
|
||||
with LocalVariable("buf", cg.void, lv_expr.custom_mem_alloc(buf_size)) as buf:
|
||||
cg.add(cg.RawExpression(f"memset({buf}, 0, {buf_size});"))
|
||||
lv.canvas_set_buffer(
|
||||
w.obj,
|
||||
buf,
|
||||
width,
|
||||
height,
|
||||
literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"),
|
||||
)
|
||||
|
||||
|
||||
canvas_spec = CanvasType()
|
||||
|
||||
@@ -192,10 +192,6 @@ def media_player_schema(
|
||||
return _MEDIA_PLAYER_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer)
|
||||
MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player"))
|
||||
|
||||
MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
cv.Schema(
|
||||
{
|
||||
|
||||
@@ -238,11 +238,6 @@ def number_schema(
|
||||
return _NUMBER_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
NUMBER_SCHEMA = number_schema(Number)
|
||||
NUMBER_SCHEMA.add_extra(cv.deprecated_schema_constant("number"))
|
||||
|
||||
|
||||
async def setup_number_core_(
|
||||
var, config, *, min_value: float, max_value: float, step: float
|
||||
):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, esp32_rmt, remote_base
|
||||
@@ -18,9 +20,12 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AUTO_LOAD = ["remote_base"]
|
||||
|
||||
CONF_EOT_LEVEL = "eot_level"
|
||||
CONF_NON_BLOCKING = "non_blocking"
|
||||
CONF_ON_TRANSMIT = "on_transmit"
|
||||
CONF_ON_COMPLETE = "on_complete"
|
||||
CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID
|
||||
@@ -65,11 +70,25 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
esp32_c6=48,
|
||||
esp32_h2=48,
|
||||
): cv.All(cv.only_on_esp32, cv.int_range(min=2)),
|
||||
cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean),
|
||||
cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
def _validate_non_blocking(config):
|
||||
if CORE.is_esp32 and CONF_NON_BLOCKING not in config:
|
||||
_LOGGER.warning(
|
||||
"'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n"
|
||||
"The default behavior changed in 2025.11.0; previously blocking mode was used.\n"
|
||||
"To silence this warning, explicitly set 'non_blocking: true' (or 'false')."
|
||||
)
|
||||
config[CONF_NON_BLOCKING] = True
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _validate_non_blocking
|
||||
|
||||
DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterComponent),
|
||||
@@ -95,6 +114,7 @@ async def to_code(config):
|
||||
if CORE.is_esp32:
|
||||
var = cg.new_Pvariable(config[CONF_ID], pin)
|
||||
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
|
||||
cg.add(var.set_non_blocking(config[CONF_NON_BLOCKING]))
|
||||
if CONF_CLOCK_RESOLUTION in config:
|
||||
cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION]))
|
||||
if CONF_USE_DMA in config:
|
||||
|
||||
@@ -54,6 +54,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
|
||||
#if defined(USE_ESP32)
|
||||
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
|
||||
void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; }
|
||||
void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; }
|
||||
#endif
|
||||
|
||||
Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; };
|
||||
@@ -74,6 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
|
||||
|
||||
#ifdef USE_ESP32
|
||||
void configure_rmt_();
|
||||
void wait_for_rmt_();
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
|
||||
RemoteTransmitterComponentStore store_{};
|
||||
@@ -90,6 +92,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
|
||||
esp_err_t error_code_{ESP_OK};
|
||||
std::string error_string_{""};
|
||||
bool inverted_{false};
|
||||
bool non_blocking_{false};
|
||||
#endif
|
||||
uint8_t carrier_duty_percent_;
|
||||
|
||||
|
||||
@@ -196,12 +196,29 @@ void RemoteTransmitterComponent::configure_rmt_() {
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteTransmitterComponent::wait_for_rmt_() {
|
||||
esp_err_t error = rmt_tx_wait_all_done(this->channel_, -1);
|
||||
if (error != ESP_OK) {
|
||||
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
|
||||
this->status_set_warning();
|
||||
}
|
||||
|
||||
this->complete_trigger_->trigger();
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
|
||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
|
||||
uint64_t total_duration = 0;
|
||||
|
||||
if (this->is_failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the timeout was cancelled, block until the tx is complete
|
||||
if (this->non_blocking_ && this->cancel_timeout("complete")) {
|
||||
this->wait_for_rmt_();
|
||||
}
|
||||
|
||||
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
|
||||
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
|
||||
this->configure_rmt_();
|
||||
@@ -212,6 +229,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
|
||||
// encode any delay at the start of the buffer to simplify the encoder callback
|
||||
// this will be skipped the first time around
|
||||
total_duration += send_wait * (send_times - 1);
|
||||
send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait));
|
||||
while (send_wait > 0) {
|
||||
int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX));
|
||||
@@ -229,6 +247,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
if (!level) {
|
||||
value = -value;
|
||||
}
|
||||
total_duration += value * send_times;
|
||||
value = this->from_microseconds_(static_cast<uint32_t>(value));
|
||||
while (value > 0) {
|
||||
int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX));
|
||||
@@ -260,13 +279,12 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
} else {
|
||||
this->status_clear_warning();
|
||||
}
|
||||
error = rmt_tx_wait_all_done(this->channel_, -1);
|
||||
if (error != ESP_OK) {
|
||||
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
|
||||
this->status_set_warning();
|
||||
}
|
||||
|
||||
this->complete_trigger_->trigger();
|
||||
if (this->non_blocking_) {
|
||||
this->set_timeout("complete", total_duration / 1000, [this]() { this->wait_for_rmt_(); });
|
||||
} else {
|
||||
this->wait_for_rmt_();
|
||||
}
|
||||
}
|
||||
#else
|
||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
|
||||
|
||||
@@ -86,11 +86,6 @@ def select_schema(
|
||||
return _SELECT_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
SELECT_SCHEMA = select_schema(Select)
|
||||
SELECT_SCHEMA.add_extra(cv.deprecated_schema_constant("select"))
|
||||
|
||||
|
||||
async def setup_select_core_(var, config, *, options: list[str]):
|
||||
await setup_entity(var, config, "select")
|
||||
|
||||
|
||||
@@ -369,11 +369,6 @@ def sensor_schema(
|
||||
return _SENSOR_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
SENSOR_SCHEMA = sensor_schema()
|
||||
SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("sensor"))
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_))
|
||||
async def offset_filter_to_code(config, filter_id):
|
||||
template_ = await cg.templatable(config, [], float)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/output/binary_output.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
@@ -18,7 +16,7 @@ class SpeedFan : public Component, public fan::Fan {
|
||||
void set_output(output::FloatOutput *output) { this->output_ = output; }
|
||||
void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; }
|
||||
void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; }
|
||||
void set_preset_modes(const std::set<std::string> &presets) { this->preset_modes_ = presets; }
|
||||
void set_preset_modes(std::initializer_list<const char *> presets) { this->preset_modes_ = presets; }
|
||||
fan::FanTraits get_traits() override { return this->traits_; }
|
||||
|
||||
protected:
|
||||
@@ -30,7 +28,7 @@ class SpeedFan : public Component, public fan::Fan {
|
||||
output::BinaryOutput *direction_{nullptr};
|
||||
int speed_count_{};
|
||||
fan::FanTraits traits_;
|
||||
std::set<std::string> preset_modes_{};
|
||||
std::vector<const char *> preset_modes_{};
|
||||
};
|
||||
|
||||
} // namespace speed
|
||||
|
||||
@@ -139,11 +139,6 @@ def switch_schema(
|
||||
return _SWITCH_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
SWITCH_SCHEMA = switch_schema(Switch)
|
||||
SWITCH_SCHEMA.add_extra(cv.deprecated_schema_constant("switch"))
|
||||
|
||||
|
||||
async def setup_switch_core_(var, config):
|
||||
await setup_entity(var, config, "switch")
|
||||
|
||||
|
||||
@@ -25,6 +25,20 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor,
|
||||
this->sensor_data_.push_back(sd);
|
||||
this->sensor_map_[sensor].store_index = this->next_store_index_++;
|
||||
};
|
||||
|
||||
static const LogString *sensor_type_to_string(AlarmSensorType type) {
|
||||
switch (type) {
|
||||
case ALARM_SENSOR_TYPE_INSTANT:
|
||||
return LOG_STR("instant");
|
||||
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
|
||||
return LOG_STR("delayed_follower");
|
||||
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
|
||||
return LOG_STR("instant_always");
|
||||
case ALARM_SENSOR_TYPE_DELAYED:
|
||||
default:
|
||||
return LOG_STR("delayed");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void TemplateAlarmControlPanel::dump_config() {
|
||||
@@ -46,35 +60,20 @@ void TemplateAlarmControlPanel::dump_config() {
|
||||
" Supported Features: %" PRIu32,
|
||||
(this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features());
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
for (auto sensor_info : this->sensor_map_) {
|
||||
ESP_LOGCONFIG(TAG, " Binary Sensor:");
|
||||
for (auto const &[sensor, info] : this->sensor_map_) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Binary Sensor:\n"
|
||||
" Name: %s\n"
|
||||
" Type: %s\n"
|
||||
" Armed home bypass: %s\n"
|
||||
" Armed night bypass: %s\n"
|
||||
" Auto bypass: %s\n"
|
||||
" Chime mode: %s",
|
||||
sensor_info.first->get_name().c_str(),
|
||||
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
|
||||
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
|
||||
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO),
|
||||
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME));
|
||||
const char *sensor_type;
|
||||
switch (sensor_info.second.type) {
|
||||
case ALARM_SENSOR_TYPE_INSTANT:
|
||||
sensor_type = "instant";
|
||||
break;
|
||||
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
|
||||
sensor_type = "delayed_follower";
|
||||
break;
|
||||
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
|
||||
sensor_type = "instant_always";
|
||||
break;
|
||||
case ALARM_SENSOR_TYPE_DELAYED:
|
||||
default:
|
||||
sensor_type = "delayed";
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Sensor type: %s", sensor_type);
|
||||
sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(info.type)),
|
||||
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
|
||||
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
|
||||
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO),
|
||||
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_CHIME));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -123,39 +122,37 @@ void TemplateAlarmControlPanel::loop() {
|
||||
bool instant_sensor_faulted = false;
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// Test all of the sensors in the list regardless of the alarm panel state
|
||||
for (auto sensor_info : this->sensor_map_) {
|
||||
// Test all of the sensors regardless of the alarm panel state
|
||||
for (auto const &[sensor, info] : this->sensor_map_) {
|
||||
// Check for chime zones
|
||||
if ((sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)) {
|
||||
if (info.flags & BINARY_SENSOR_MODE_CHIME) {
|
||||
// Look for the transition from closed to open
|
||||
if ((!this->sensor_data_[sensor_info.second.store_index].last_chime_state) && (sensor_info.first->state)) {
|
||||
if ((!this->sensor_data_[info.store_index].last_chime_state) && (sensor->state)) {
|
||||
// Must be disarmed to chime
|
||||
if (this->current_state_ == ACP_STATE_DISARMED) {
|
||||
this->chime_callback_.call();
|
||||
}
|
||||
}
|
||||
// Record the sensor state change
|
||||
this->sensor_data_[sensor_info.second.store_index].last_chime_state = sensor_info.first->state;
|
||||
this->sensor_data_[info.store_index].last_chime_state = sensor->state;
|
||||
}
|
||||
// Check for faulted sensors
|
||||
if (sensor_info.first->state) { // Sensor triggered?
|
||||
if (sensor->state) {
|
||||
// Skip if auto bypassed
|
||||
if (std::count(this->bypassed_sensor_indicies_.begin(), this->bypassed_sensor_indicies_.end(),
|
||||
sensor_info.second.store_index) == 1) {
|
||||
info.store_index) == 1) {
|
||||
continue;
|
||||
}
|
||||
// Skip if bypass armed home
|
||||
if (this->current_state_ == ACP_STATE_ARMED_HOME &&
|
||||
(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) {
|
||||
if ((this->current_state_ == ACP_STATE_ARMED_HOME) && (info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) {
|
||||
continue;
|
||||
}
|
||||
// Skip if bypass armed night
|
||||
if (this->current_state_ == ACP_STATE_ARMED_NIGHT &&
|
||||
(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) {
|
||||
if ((this->current_state_ == ACP_STATE_ARMED_NIGHT) && (info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (sensor_info.second.type) {
|
||||
switch (info.type) {
|
||||
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
|
||||
next_state = ACP_STATE_TRIGGERED;
|
||||
[[fallthrough]];
|
||||
@@ -247,11 +244,11 @@ void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_p
|
||||
|
||||
void TemplateAlarmControlPanel::bypass_before_arming() {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
for (auto sensor_info : this->sensor_map_) {
|
||||
for (auto const &[sensor, info] : this->sensor_map_) {
|
||||
// Check for faulted bypass_auto sensors and remove them from monitoring
|
||||
if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) {
|
||||
ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor_info.first->get_name().c_str());
|
||||
this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index);
|
||||
if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) {
|
||||
ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str());
|
||||
this->bypassed_sensor_indicies_.push_back(info.store_index);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -6,17 +6,21 @@ namespace template_ {
|
||||
|
||||
static const char *const TAG = "template.binary_sensor";
|
||||
|
||||
void TemplateBinarySensor::setup() { this->loop(); }
|
||||
void TemplateBinarySensor::setup() {
|
||||
if (!this->f_.has_value()) {
|
||||
this->disable_loop();
|
||||
} else {
|
||||
this->loop();
|
||||
}
|
||||
}
|
||||
|
||||
void TemplateBinarySensor::loop() {
|
||||
if (!this->f_.has_value())
|
||||
return;
|
||||
|
||||
auto s = (*this->f_)();
|
||||
auto s = this->f_();
|
||||
if (s.has_value()) {
|
||||
this->publish_state(*s);
|
||||
}
|
||||
}
|
||||
|
||||
void TemplateBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Template Binary Sensor", this); }
|
||||
|
||||
} // namespace template_
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -8,7 +9,7 @@ namespace template_ {
|
||||
|
||||
class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor {
|
||||
public:
|
||||
void set_template(optional<bool> (*f)()) { this->f_ = f; }
|
||||
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
@@ -17,7 +18,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
protected:
|
||||
optional<optional<bool> (*)()> f_;
|
||||
TemplateLambda<bool> f_;
|
||||
};
|
||||
|
||||
} // namespace template_
|
||||
|
||||
@@ -33,28 +33,27 @@ void TemplateCover::setup() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this->state_f_.has_value() && !this->tilt_f_.has_value())
|
||||
this->disable_loop();
|
||||
}
|
||||
void TemplateCover::loop() {
|
||||
bool changed = false;
|
||||
|
||||
if (this->state_f_.has_value()) {
|
||||
auto s = (*this->state_f_)();
|
||||
if (s.has_value()) {
|
||||
auto pos = clamp(*s, 0.0f, 1.0f);
|
||||
if (pos != this->position) {
|
||||
this->position = pos;
|
||||
changed = true;
|
||||
}
|
||||
auto s = this->state_f_();
|
||||
if (s.has_value()) {
|
||||
auto pos = clamp(*s, 0.0f, 1.0f);
|
||||
if (pos != this->position) {
|
||||
this->position = pos;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (this->tilt_f_.has_value()) {
|
||||
auto s = (*this->tilt_f_)();
|
||||
if (s.has_value()) {
|
||||
auto tilt = clamp(*s, 0.0f, 1.0f);
|
||||
if (tilt != this->tilt) {
|
||||
this->tilt = tilt;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
auto tilt = this->tilt_f_();
|
||||
if (tilt.has_value()) {
|
||||
auto tilt_val = clamp(*tilt, 0.0f, 1.0f);
|
||||
if (tilt_val != this->tilt) {
|
||||
this->tilt = tilt_val;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +62,6 @@ void TemplateCover::loop() {
|
||||
}
|
||||
void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
|
||||
void TemplateCover::set_state_lambda(optional<float> (*f)()) { this->state_f_ = f; }
|
||||
float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; }
|
||||
Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; }
|
||||
@@ -124,7 +122,6 @@ CoverTraits TemplateCover::get_traits() {
|
||||
}
|
||||
Trigger<float> *TemplateCover::get_position_trigger() const { return this->position_trigger_; }
|
||||
Trigger<float> *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; }
|
||||
void TemplateCover::set_tilt_lambda(optional<float> (*tilt_f)()) { this->tilt_f_ = tilt_f; }
|
||||
void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; }
|
||||
void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; }
|
||||
void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
#include "esphome/components/cover/cover.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -17,7 +18,8 @@ class TemplateCover : public cover::Cover, public Component {
|
||||
public:
|
||||
TemplateCover();
|
||||
|
||||
void set_state_lambda(optional<float> (*f)());
|
||||
template<typename F> void set_state_lambda(F &&f) { this->state_f_.set(std::forward<F>(f)); }
|
||||
template<typename F> void set_tilt_lambda(F &&f) { this->tilt_f_.set(std::forward<F>(f)); }
|
||||
Trigger<> *get_open_trigger() const;
|
||||
Trigger<> *get_close_trigger() const;
|
||||
Trigger<> *get_stop_trigger() const;
|
||||
@@ -26,7 +28,6 @@ class TemplateCover : public cover::Cover, public Component {
|
||||
Trigger<float> *get_tilt_trigger() const;
|
||||
void set_optimistic(bool optimistic);
|
||||
void set_assumed_state(bool assumed_state);
|
||||
void set_tilt_lambda(optional<float> (*tilt_f)());
|
||||
void set_has_stop(bool has_stop);
|
||||
void set_has_position(bool has_position);
|
||||
void set_has_tilt(bool has_tilt);
|
||||
@@ -45,8 +46,8 @@ class TemplateCover : public cover::Cover, public Component {
|
||||
void stop_prev_trigger_();
|
||||
|
||||
TemplateCoverRestoreMode restore_mode_{COVER_RESTORE};
|
||||
optional<optional<float> (*)()> state_f_;
|
||||
optional<optional<float> (*)()> tilt_f_;
|
||||
TemplateLambda<float> state_f_;
|
||||
TemplateLambda<float> tilt_f_;
|
||||
bool assumed_state_{false};
|
||||
bool optimistic_{false};
|
||||
Trigger<> *open_trigger_;
|
||||
|
||||
@@ -40,14 +40,13 @@ void TemplateDate::update() {
|
||||
if (!this->f_.has_value())
|
||||
return;
|
||||
|
||||
auto val = (*this->f_)();
|
||||
if (!val.has_value())
|
||||
return;
|
||||
|
||||
this->year_ = val->year;
|
||||
this->month_ = val->month;
|
||||
this->day_ = val->day_of_month;
|
||||
this->publish_state();
|
||||
auto val = this->f_();
|
||||
if (val.has_value()) {
|
||||
this->year_ = val->year;
|
||||
this->month_ = val->month;
|
||||
this->day_ = val->day_of_month;
|
||||
this->publish_state();
|
||||
}
|
||||
}
|
||||
|
||||
void TemplateDate::control(const datetime::DateCall &call) {
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/time.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace template_ {
|
||||
|
||||
class TemplateDate : public datetime::DateEntity, public PollingComponent {
|
||||
public:
|
||||
void set_template(optional<ESPTime> (*f)()) { this->f_ = f; }
|
||||
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
@@ -35,7 +36,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent {
|
||||
ESPTime initial_value_{};
|
||||
bool restore_value_{false};
|
||||
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
|
||||
optional<optional<ESPTime> (*)()> f_;
|
||||
TemplateLambda<ESPTime> f_;
|
||||
|
||||
ESPPreferenceObject pref_;
|
||||
};
|
||||
|
||||
@@ -43,17 +43,16 @@ void TemplateDateTime::update() {
|
||||
if (!this->f_.has_value())
|
||||
return;
|
||||
|
||||
auto val = (*this->f_)();
|
||||
if (!val.has_value())
|
||||
return;
|
||||
|
||||
this->year_ = val->year;
|
||||
this->month_ = val->month;
|
||||
this->day_ = val->day_of_month;
|
||||
this->hour_ = val->hour;
|
||||
this->minute_ = val->minute;
|
||||
this->second_ = val->second;
|
||||
this->publish_state();
|
||||
auto val = this->f_();
|
||||
if (val.has_value()) {
|
||||
this->year_ = val->year;
|
||||
this->month_ = val->month;
|
||||
this->day_ = val->day_of_month;
|
||||
this->hour_ = val->hour;
|
||||
this->minute_ = val->minute;
|
||||
this->second_ = val->second;
|
||||
this->publish_state();
|
||||
}
|
||||
}
|
||||
|
||||
void TemplateDateTime::control(const datetime::DateTimeCall &call) {
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/time.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace template_ {
|
||||
|
||||
class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent {
|
||||
public:
|
||||
void set_template(optional<ESPTime> (*f)()) { this->f_ = f; }
|
||||
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
@@ -35,7 +36,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen
|
||||
ESPTime initial_value_{};
|
||||
bool restore_value_{false};
|
||||
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
|
||||
optional<optional<ESPTime> (*)()> f_;
|
||||
TemplateLambda<ESPTime> f_;
|
||||
|
||||
ESPPreferenceObject pref_;
|
||||
};
|
||||
|
||||
@@ -40,14 +40,13 @@ void TemplateTime::update() {
|
||||
if (!this->f_.has_value())
|
||||
return;
|
||||
|
||||
auto val = (*this->f_)();
|
||||
if (!val.has_value())
|
||||
return;
|
||||
|
||||
this->hour_ = val->hour;
|
||||
this->minute_ = val->minute;
|
||||
this->second_ = val->second;
|
||||
this->publish_state();
|
||||
auto val = this->f_();
|
||||
if (val.has_value()) {
|
||||
this->hour_ = val->hour;
|
||||
this->minute_ = val->minute;
|
||||
this->second_ = val->second;
|
||||
this->publish_state();
|
||||
}
|
||||
}
|
||||
|
||||
void TemplateTime::control(const datetime::TimeCall &call) {
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/time.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace template_ {
|
||||
|
||||
class TemplateTime : public datetime::TimeEntity, public PollingComponent {
|
||||
public:
|
||||
void set_template(optional<ESPTime> (*f)()) { this->f_ = f; }
|
||||
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
@@ -35,7 +36,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent {
|
||||
ESPTime initial_value_{};
|
||||
bool restore_value_{false};
|
||||
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
|
||||
optional<optional<ESPTime> (*)()> f_;
|
||||
TemplateLambda<ESPTime> f_;
|
||||
|
||||
ESPPreferenceObject pref_;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/fan/fan.h"
|
||||
|
||||
@@ -16,7 +14,7 @@ class TemplateFan : public Component, public fan::Fan {
|
||||
void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; }
|
||||
void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; }
|
||||
void set_speed_count(int count) { this->speed_count_ = count; }
|
||||
void set_preset_modes(const std::set<std::string> &presets) { this->preset_modes_ = presets; }
|
||||
void set_preset_modes(std::initializer_list<const char *> presets) { this->preset_modes_ = presets; }
|
||||
fan::FanTraits get_traits() override { return this->traits_; }
|
||||
|
||||
protected:
|
||||
@@ -26,7 +24,7 @@ class TemplateFan : public Component, public fan::Fan {
|
||||
bool has_direction_{false};
|
||||
int speed_count_{0};
|
||||
fan::FanTraits traits_;
|
||||
std::set<std::string> preset_modes_{};
|
||||
std::vector<const char *> preset_modes_{};
|
||||
};
|
||||
|
||||
} // namespace template_
|
||||
|
||||
@@ -11,14 +11,16 @@ static const char *const TAG = "template.lock";
|
||||
TemplateLock::TemplateLock()
|
||||
: lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {}
|
||||
|
||||
void TemplateLock::loop() {
|
||||
void TemplateLock::setup() {
|
||||
if (!this->f_.has_value())
|
||||
return;
|
||||
auto val = (*this->f_)();
|
||||
if (!val.has_value())
|
||||
return;
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
this->publish_state(*val);
|
||||
void TemplateLock::loop() {
|
||||
auto val = this->f_();
|
||||
if (val.has_value()) {
|
||||
this->publish_state(*val);
|
||||
}
|
||||
}
|
||||
void TemplateLock::control(const lock::LockCall &call) {
|
||||
if (this->prev_trigger_ != nullptr) {
|
||||
@@ -45,7 +47,6 @@ void TemplateLock::open_latch() {
|
||||
this->open_trigger_->trigger();
|
||||
}
|
||||
void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
void TemplateLock::set_state_lambda(optional<lock::LockState> (*f)()) { this->f_ = f; }
|
||||
float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; }
|
||||
Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
#include "esphome/components/lock/lock.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -11,9 +12,10 @@ class TemplateLock : public lock::Lock, public Component {
|
||||
public:
|
||||
TemplateLock();
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_state_lambda(optional<lock::LockState> (*f)());
|
||||
template<typename F> void set_state_lambda(F &&f) { this->f_.set(std::forward<F>(f)); }
|
||||
Trigger<> *get_lock_trigger() const;
|
||||
Trigger<> *get_unlock_trigger() const;
|
||||
Trigger<> *get_open_trigger() const;
|
||||
@@ -26,7 +28,7 @@ class TemplateLock : public lock::Lock, public Component {
|
||||
void control(const lock::LockCall &call) override;
|
||||
void open_latch() override;
|
||||
|
||||
optional<optional<lock::LockState> (*)()> f_;
|
||||
TemplateLambda<lock::LockState> f_;
|
||||
bool optimistic_{false};
|
||||
Trigger<> *lock_trigger_;
|
||||
Trigger<> *unlock_trigger_;
|
||||
|
||||
@@ -30,11 +30,10 @@ void TemplateNumber::update() {
|
||||
if (!this->f_.has_value())
|
||||
return;
|
||||
|
||||
auto val = (*this->f_)();
|
||||
if (!val.has_value())
|
||||
return;
|
||||
|
||||
this->publish_state(*val);
|
||||
auto val = this->f_();
|
||||
if (val.has_value()) {
|
||||
this->publish_state(*val);
|
||||
}
|
||||
}
|
||||
|
||||
void TemplateNumber::control(float value) {
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace template_ {
|
||||
|
||||
class TemplateNumber : public number::Number, public PollingComponent {
|
||||
public:
|
||||
void set_template(optional<float> (*f)()) { this->f_ = f; }
|
||||
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
@@ -28,7 +29,7 @@ class TemplateNumber : public number::Number, public PollingComponent {
|
||||
float initial_value_{NAN};
|
||||
bool restore_value_{false};
|
||||
Trigger<float> *set_trigger_ = new Trigger<float>();
|
||||
optional<optional<float> (*)()> f_;
|
||||
TemplateLambda<float> f_;
|
||||
|
||||
ESPPreferenceObject pref_;
|
||||
};
|
||||
|
||||
@@ -31,16 +31,14 @@ void TemplateSelect::update() {
|
||||
if (!this->f_.has_value())
|
||||
return;
|
||||
|
||||
auto val = (*this->f_)();
|
||||
if (!val.has_value())
|
||||
return;
|
||||
|
||||
if (!this->has_option(*val)) {
|
||||
ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str());
|
||||
return;
|
||||
auto val = this->f_();
|
||||
if (val.has_value()) {
|
||||
if (!this->has_option(*val)) {
|
||||
ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str());
|
||||
return;
|
||||
}
|
||||
this->publish_state(*val);
|
||||
}
|
||||
|
||||
this->publish_state(*val);
|
||||
}
|
||||
|
||||
void TemplateSelect::control(const std::string &value) {
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace template_ {
|
||||
|
||||
class TemplateSelect : public select::Select, public PollingComponent {
|
||||
public:
|
||||
void set_template(optional<std::string> (*f)()) { this->f_ = f; }
|
||||
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
@@ -28,7 +29,7 @@ class TemplateSelect : public select::Select, public PollingComponent {
|
||||
size_t initial_option_index_{0};
|
||||
bool restore_value_ = false;
|
||||
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
|
||||
optional<optional<std::string> (*)()> f_;
|
||||
TemplateLambda<std::string> f_;
|
||||
|
||||
ESPPreferenceObject pref_;
|
||||
};
|
||||
|
||||
@@ -11,13 +11,14 @@ void TemplateSensor::update() {
|
||||
if (!this->f_.has_value())
|
||||
return;
|
||||
|
||||
auto val = (*this->f_)();
|
||||
auto val = this->f_();
|
||||
if (val.has_value()) {
|
||||
this->publish_state(*val);
|
||||
}
|
||||
}
|
||||
|
||||
float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
void TemplateSensor::set_template(optional<float> (*f)()) { this->f_ = f; }
|
||||
|
||||
void TemplateSensor::dump_config() {
|
||||
LOG_SENSOR("", "Template Sensor", this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -8,7 +9,7 @@ namespace template_ {
|
||||
|
||||
class TemplateSensor : public sensor::Sensor, public PollingComponent {
|
||||
public:
|
||||
void set_template(optional<float> (*f)());
|
||||
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
|
||||
|
||||
void update() override;
|
||||
|
||||
@@ -17,7 +18,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent {
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
optional<optional<float> (*)()> f_;
|
||||
TemplateLambda<float> f_;
|
||||
};
|
||||
|
||||
} // namespace template_
|
||||
|
||||
@@ -9,13 +9,10 @@ static const char *const TAG = "template.switch";
|
||||
TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {}
|
||||
|
||||
void TemplateSwitch::loop() {
|
||||
if (!this->f_.has_value())
|
||||
return;
|
||||
auto s = (*this->f_)();
|
||||
if (!s.has_value())
|
||||
return;
|
||||
|
||||
this->publish_state(*s);
|
||||
auto s = this->f_();
|
||||
if (s.has_value()) {
|
||||
this->publish_state(*s);
|
||||
}
|
||||
}
|
||||
void TemplateSwitch::write_state(bool state) {
|
||||
if (this->prev_trigger_ != nullptr) {
|
||||
@@ -35,11 +32,13 @@ void TemplateSwitch::write_state(bool state) {
|
||||
}
|
||||
void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
bool TemplateSwitch::assumed_state() { return this->assumed_state_; }
|
||||
void TemplateSwitch::set_state_lambda(optional<bool> (*f)()) { this->f_ = f; }
|
||||
float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; }
|
||||
Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; }
|
||||
Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; }
|
||||
void TemplateSwitch::setup() {
|
||||
if (!this->f_.has_value())
|
||||
this->disable_loop();
|
||||
|
||||
optional<bool> initial_state = this->get_initial_state_with_restore_mode();
|
||||
|
||||
if (initial_state.has_value()) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -14,7 +15,7 @@ class TemplateSwitch : public switch_::Switch, public Component {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_state_lambda(optional<bool> (*f)());
|
||||
template<typename F> void set_state_lambda(F &&f) { this->f_.set(std::forward<F>(f)); }
|
||||
Trigger<> *get_turn_on_trigger() const;
|
||||
Trigger<> *get_turn_off_trigger() const;
|
||||
void set_optimistic(bool optimistic);
|
||||
@@ -28,7 +29,7 @@ class TemplateSwitch : public switch_::Switch, public Component {
|
||||
|
||||
void write_state(bool state) override;
|
||||
|
||||
optional<optional<bool> (*)()> f_;
|
||||
TemplateLambda<bool> f_;
|
||||
bool optimistic_{false};
|
||||
bool assumed_state_{false};
|
||||
Trigger<> *turn_on_trigger_;
|
||||
|
||||
@@ -7,10 +7,8 @@ namespace template_ {
|
||||
static const char *const TAG = "template.text";
|
||||
|
||||
void TemplateText::setup() {
|
||||
if (!(this->f_ == nullptr)) {
|
||||
if (this->f_.has_value())
|
||||
return;
|
||||
}
|
||||
if (this->f_.has_value())
|
||||
return;
|
||||
std::string value = this->initial_value_;
|
||||
if (!this->pref_) {
|
||||
ESP_LOGD(TAG, "State from initial: %s", value.c_str());
|
||||
@@ -26,17 +24,13 @@ void TemplateText::setup() {
|
||||
}
|
||||
|
||||
void TemplateText::update() {
|
||||
if (this->f_ == nullptr)
|
||||
return;
|
||||
|
||||
if (!this->f_.has_value())
|
||||
return;
|
||||
|
||||
auto val = (*this->f_)();
|
||||
if (!val.has_value())
|
||||
return;
|
||||
|
||||
this->publish_state(*val);
|
||||
auto val = this->f_();
|
||||
if (val.has_value()) {
|
||||
this->publish_state(*val);
|
||||
}
|
||||
}
|
||||
|
||||
void TemplateText::control(const std::string &value) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace template_ {
|
||||
@@ -61,7 +62,7 @@ template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase {
|
||||
|
||||
class TemplateText : public text::Text, public PollingComponent {
|
||||
public:
|
||||
void set_template(optional<std::string> (*f)()) { this->f_ = f; }
|
||||
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
@@ -78,7 +79,7 @@ class TemplateText : public text::Text, public PollingComponent {
|
||||
bool optimistic_ = false;
|
||||
std::string initial_value_;
|
||||
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
|
||||
optional<optional<std::string> (*)()> f_{nullptr};
|
||||
TemplateLambda<std::string> f_{};
|
||||
|
||||
TemplateTextSaverBase *pref_ = nullptr;
|
||||
};
|
||||
|
||||
@@ -10,13 +10,14 @@ void TemplateTextSensor::update() {
|
||||
if (!this->f_.has_value())
|
||||
return;
|
||||
|
||||
auto val = (*this->f_)();
|
||||
auto val = this->f_();
|
||||
if (val.has_value()) {
|
||||
this->publish_state(*val);
|
||||
}
|
||||
}
|
||||
|
||||
float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
void TemplateTextSensor::set_template(optional<std::string> (*f)()) { this->f_ = f; }
|
||||
|
||||
void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); }
|
||||
|
||||
} // namespace template_
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -9,7 +10,7 @@ namespace template_ {
|
||||
|
||||
class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent {
|
||||
public:
|
||||
void set_template(optional<std::string> (*f)());
|
||||
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
|
||||
|
||||
void update() override;
|
||||
|
||||
@@ -18,7 +19,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
optional<optional<std::string> (*)()> f_{};
|
||||
TemplateLambda<std::string> f_{};
|
||||
};
|
||||
|
||||
} // namespace template_
|
||||
|
||||
@@ -33,19 +33,19 @@ void TemplateValve::setup() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this->state_f_.has_value())
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void TemplateValve::loop() {
|
||||
bool changed = false;
|
||||
|
||||
if (this->state_f_.has_value()) {
|
||||
auto s = (*this->state_f_)();
|
||||
if (s.has_value()) {
|
||||
auto pos = clamp(*s, 0.0f, 1.0f);
|
||||
if (pos != this->position) {
|
||||
this->position = pos;
|
||||
changed = true;
|
||||
}
|
||||
auto s = this->state_f_();
|
||||
if (s.has_value()) {
|
||||
auto pos = clamp(*s, 0.0f, 1.0f);
|
||||
if (pos != this->position) {
|
||||
this->position = pos;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ void TemplateValve::loop() {
|
||||
|
||||
void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
|
||||
void TemplateValve::set_state_lambda(optional<float> (*f)()) { this->state_f_ = f; }
|
||||
float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/template_lambda.h"
|
||||
#include "esphome/components/valve/valve.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -17,7 +18,7 @@ class TemplateValve : public valve::Valve, public Component {
|
||||
public:
|
||||
TemplateValve();
|
||||
|
||||
void set_state_lambda(optional<float> (*f)());
|
||||
template<typename F> void set_state_lambda(F &&f) { this->state_f_.set(std::forward<F>(f)); }
|
||||
Trigger<> *get_open_trigger() const;
|
||||
Trigger<> *get_close_trigger() const;
|
||||
Trigger<> *get_stop_trigger() const;
|
||||
@@ -42,7 +43,7 @@ class TemplateValve : public valve::Valve, public Component {
|
||||
void stop_prev_trigger_();
|
||||
|
||||
TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE};
|
||||
optional<optional<float> (*)()> state_f_;
|
||||
TemplateLambda<float> state_f_;
|
||||
bool assumed_state_{false};
|
||||
bool optimistic_{false};
|
||||
Trigger<> *open_trigger_;
|
||||
|
||||
@@ -84,11 +84,6 @@ def text_schema(
|
||||
return _TEXT_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
TEXT_SCHEMA = text_schema()
|
||||
TEXT_SCHEMA.add_extra(cv.deprecated_schema_constant("text"))
|
||||
|
||||
|
||||
async def setup_text_core_(
|
||||
var,
|
||||
config,
|
||||
|
||||
@@ -193,11 +193,6 @@ def text_sensor_schema(
|
||||
return _TEXT_SENSOR_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
TEXT_SENSOR_SCHEMA = text_sensor_schema()
|
||||
TEXT_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("text_sensor"))
|
||||
|
||||
|
||||
async def build_filters(config):
|
||||
return await cg.build_registry_list(FILTER_REGISTRY, config)
|
||||
|
||||
|
||||
@@ -84,11 +84,6 @@ def update_schema(
|
||||
return _UPDATE_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
UPDATE_SCHEMA = update_schema()
|
||||
UPDATE_SCHEMA.add_extra(cv.deprecated_schema_constant("update"))
|
||||
|
||||
|
||||
async def setup_update_core_(var, config):
|
||||
await setup_entity(var, config, "update")
|
||||
|
||||
|
||||
@@ -129,11 +129,6 @@ def valve_schema(
|
||||
return _VALVE_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
VALVE_SCHEMA = valve_schema()
|
||||
VALVE_SCHEMA.add_extra(cv.deprecated_schema_constant("valve"))
|
||||
|
||||
|
||||
async def _setup_valve_core(var, config):
|
||||
await setup_entity(var, config, "valve")
|
||||
|
||||
|
||||
@@ -319,6 +319,9 @@ def iter_ids(config, path=None):
|
||||
yield from iter_ids(item, path + [i])
|
||||
elif isinstance(config, dict):
|
||||
for key, value in config.items():
|
||||
if len(path) == 0 and key == CONF_SUBSTITUTIONS:
|
||||
# Ignore IDs in substitution definitions.
|
||||
continue
|
||||
if isinstance(key, core.ID):
|
||||
yield key, path
|
||||
yield from iter_ids(value, path + [key])
|
||||
|
||||
@@ -2195,26 +2195,3 @@ def rename_key(old_key, new_key):
|
||||
return config
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
# Remove before 2025.11.0
|
||||
def deprecated_schema_constant(entity_type: str):
|
||||
def validator(config):
|
||||
type: str = "unknown"
|
||||
if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
|
||||
type = str(id.type).split("::", maxsplit=1)[0]
|
||||
_LOGGER.warning(
|
||||
"Using `%s.%s_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
|
||||
"Please use `%s.%s_schema(...)` instead. "
|
||||
"If you are seeing this, report an issue to the external_component author and ask them to update it. "
|
||||
"https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
|
||||
"Component using this schema: %s",
|
||||
entity_type,
|
||||
entity_type.upper(),
|
||||
entity_type,
|
||||
entity_type,
|
||||
type,
|
||||
)
|
||||
return config
|
||||
|
||||
return validator
|
||||
|
||||
51
esphome/core/template_lambda.h
Normal file
51
esphome/core/template_lambda.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/optional.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/** Lightweight wrapper for template platform lambdas (stateless function pointers only).
|
||||
*
|
||||
* This optimizes template platforms by storing only a function pointer (4 bytes on ESP32)
|
||||
* instead of std::function (16-32 bytes).
|
||||
*
|
||||
* IMPORTANT: This only supports stateless lambdas (no captures). The set_template() method
|
||||
* is an internal API used by YAML codegen, not intended for external use.
|
||||
*
|
||||
* Lambdas must return optional<T> to support the pattern:
|
||||
* return {}; // Don't publish a value
|
||||
* return 42.0; // Publish this value
|
||||
*
|
||||
* operator() returns optional<T>, returning nullopt when no lambda is set (nullptr check).
|
||||
*
|
||||
* @tparam T The return type (e.g., float for sensor values)
|
||||
* @tparam Args Optional arguments for the lambda
|
||||
*/
|
||||
template<typename T, typename... Args> class TemplateLambda {
|
||||
public:
|
||||
TemplateLambda() : f_(nullptr) {}
|
||||
|
||||
/** Set the lambda function pointer.
|
||||
* INTERNAL API: Only for use by YAML codegen.
|
||||
* Only stateless lambdas (no captures) are supported.
|
||||
*/
|
||||
void set(optional<T> (*f)(Args...)) { this->f_ = f; }
|
||||
|
||||
/** Check if a lambda is set */
|
||||
bool has_value() const { return this->f_ != nullptr; }
|
||||
|
||||
/** Call the lambda, returning nullopt if no lambda is set */
|
||||
optional<T> operator()(Args &&...args) {
|
||||
if (this->f_ == nullptr)
|
||||
return nullopt;
|
||||
return this->f_(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
/** Alias for operator() for compatibility */
|
||||
optional<T> call(Args &&...args) { return (*this)(std::forward<Args>(args)...); }
|
||||
|
||||
protected:
|
||||
optional<T> (*f_)(Args...); // Function pointer (4 bytes on ESP32)
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==5.1.0
|
||||
click==8.1.7
|
||||
esphome-dashboard==20251013.0
|
||||
aioesphomeapi==42.4.0
|
||||
aioesphomeapi==42.5.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.16 # dashboard_import
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pylint==4.0.2
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.14.1 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.14.2 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@ def fix_remote_receiver():
|
||||
remote_receiver_schema["CONFIG_SCHEMA"] = {
|
||||
"type": "schema",
|
||||
"schema": {
|
||||
"extends": ["binary_sensor.BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"],
|
||||
"extends": ["binary_sensor._BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"],
|
||||
"config_vars": output["remote_base"].pop("binary"),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -43,12 +43,14 @@ from enum import StrEnum
|
||||
from functools import cache
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from helpers import (
|
||||
CPP_FILE_EXTENSIONS,
|
||||
ESPHOME_TESTS_COMPONENTS_PATH,
|
||||
PYTHON_FILE_EXTENSIONS,
|
||||
changed_files,
|
||||
core_changed,
|
||||
@@ -65,12 +67,17 @@ from helpers import (
|
||||
parse_test_filename,
|
||||
root_path,
|
||||
)
|
||||
from split_components_for_ci import create_intelligent_batches
|
||||
|
||||
# Threshold for splitting clang-tidy jobs
|
||||
# For small PRs (< 65 files), use nosplit for faster CI
|
||||
# For large PRs (>= 65 files), use split for better parallelization
|
||||
CLANG_TIDY_SPLIT_THRESHOLD = 65
|
||||
|
||||
# Component test batch size (weighted)
|
||||
# Isolated components count as 10x, groupable components count as 1x
|
||||
COMPONENT_TEST_BATCH_SIZE = 40
|
||||
|
||||
|
||||
class Platform(StrEnum):
|
||||
"""Platform identifiers for memory impact analysis."""
|
||||
@@ -686,6 +693,22 @@ def main() -> None:
|
||||
# Determine which C++ unit tests to run
|
||||
cpp_run_all, cpp_components = determine_cpp_unit_tests(args.branch)
|
||||
|
||||
# Split components into batches for CI testing
|
||||
# This intelligently groups components with similar bus configurations
|
||||
component_test_batches: list[str]
|
||||
if changed_components_with_tests:
|
||||
tests_dir = Path(root_path) / ESPHOME_TESTS_COMPONENTS_PATH
|
||||
batches, _ = create_intelligent_batches(
|
||||
components=changed_components_with_tests,
|
||||
tests_dir=tests_dir,
|
||||
batch_size=COMPONENT_TEST_BATCH_SIZE,
|
||||
directly_changed=directly_changed_with_tests,
|
||||
)
|
||||
# Convert batches to space-separated strings for CI matrix
|
||||
component_test_batches = [" ".join(batch) for batch in batches]
|
||||
else:
|
||||
component_test_batches = []
|
||||
|
||||
output: dict[str, Any] = {
|
||||
"integration_tests": run_integration,
|
||||
"clang_tidy": run_clang_tidy,
|
||||
@@ -703,6 +726,7 @@ def main() -> None:
|
||||
"memory_impact": memory_impact,
|
||||
"cpp_unit_tests_run_all": cpp_run_all,
|
||||
"cpp_unit_tests_components": cpp_components,
|
||||
"component_test_batches": component_test_batches,
|
||||
}
|
||||
|
||||
# Output as JSON
|
||||
|
||||
@@ -62,6 +62,10 @@ def create_intelligent_batches(
|
||||
) -> tuple[list[list[str]], dict[tuple[str, str], list[str]]]:
|
||||
"""Create batches optimized for component grouping.
|
||||
|
||||
IMPORTANT: This function is called from both split_components_for_ci.py (standalone script)
|
||||
and determine-jobs.py (integrated into job determination). Be careful when refactoring
|
||||
to ensure changes work in both contexts.
|
||||
|
||||
Args:
|
||||
components: List of component names to batch
|
||||
tests_dir: Path to tests/components directory
|
||||
|
||||
@@ -18,7 +18,8 @@ def test_gpio_binary_sensor_basic_setup(
|
||||
|
||||
assert "new gpio::GPIOBinarySensor();" in main_cpp
|
||||
assert "App.register_binary_sensor" in main_cpp
|
||||
assert "bs_gpio->set_use_interrupt(true);" in main_cpp
|
||||
# set_use_interrupt(true) should NOT be generated (uses C++ default)
|
||||
assert "bs_gpio->set_use_interrupt(true);" not in main_cpp
|
||||
assert "bs_gpio->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp
|
||||
|
||||
|
||||
@@ -51,8 +52,8 @@ def test_gpio_binary_sensor_esp8266_other_pins_use_interrupt(
|
||||
"tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml"
|
||||
)
|
||||
|
||||
# GPIO5 should still use interrupts
|
||||
assert "bs_gpio5->set_use_interrupt(true);" in main_cpp
|
||||
# GPIO5 should still use interrupts (default, so no setter call)
|
||||
assert "bs_gpio5->set_use_interrupt(true);" not in main_cpp
|
||||
assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,19 @@ number:
|
||||
widget: spinbox_id
|
||||
id: lvgl_spinbox_number
|
||||
name: LVGL Spinbox Number
|
||||
- platform: template
|
||||
id: test_brightness
|
||||
name: "Test Brightness"
|
||||
min_value: 0
|
||||
max_value: 255
|
||||
step: 1
|
||||
optimistic: true
|
||||
# Test lambda in automation accessing x parameter directly
|
||||
# This is a real-world pattern from user configs
|
||||
on_value:
|
||||
- lambda: !lambda |-
|
||||
// Direct use of x parameter in automation
|
||||
ESP_LOGD("test", "Brightness: %.0f", x);
|
||||
|
||||
light:
|
||||
- platform: lvgl
|
||||
@@ -110,3 +123,21 @@ text:
|
||||
platform: lvgl
|
||||
widget: hello_label
|
||||
mode: text
|
||||
|
||||
text_sensor:
|
||||
- platform: template
|
||||
id: test_text_sensor
|
||||
name: "Test Text Sensor"
|
||||
# Test nested lambdas in LVGL actions can access automation parameters
|
||||
on_value:
|
||||
- lvgl.label.update:
|
||||
id: hello_label
|
||||
text: !lambda return x.c_str();
|
||||
- lvgl.label.update:
|
||||
id: hello_label
|
||||
text: !lambda |-
|
||||
// Test complex lambda with conditionals accessing x parameter
|
||||
if (x == "*") {
|
||||
return "WILDCARD";
|
||||
}
|
||||
return x.c_str();
|
||||
|
||||
@@ -257,7 +257,30 @@ lvgl:
|
||||
text: "Hello shiny day"
|
||||
text_color: 0xFFFFFF
|
||||
align: bottom_mid
|
||||
- label:
|
||||
id: setup_lambda_label
|
||||
# Test lambda in widget property during setup (LvContext)
|
||||
# Should NOT receive lv_component parameter
|
||||
text: !lambda |-
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "Setup: %d", 42);
|
||||
return std::string(buf);
|
||||
align: top_mid
|
||||
text_font: space16
|
||||
- label:
|
||||
id: chip_info_label
|
||||
# Test complex setup lambda (real-world pattern)
|
||||
# Should NOT receive lv_component parameter
|
||||
text: !lambda |-
|
||||
// Test conditional compilation and string formatting
|
||||
char buf[64];
|
||||
#ifdef USE_ESP_IDF
|
||||
snprintf(buf, sizeof(buf), "IDF: v%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR);
|
||||
#else
|
||||
snprintf(buf, sizeof(buf), "Arduino");
|
||||
#endif
|
||||
return std::string(buf);
|
||||
align: top_left
|
||||
- obj:
|
||||
align: center
|
||||
arc_opa: COVER
|
||||
|
||||
@@ -2,6 +2,7 @@ remote_transmitter:
|
||||
- id: xmitr
|
||||
pin: ${pin}
|
||||
carrier_duty_percent: 50%
|
||||
non_blocking: true
|
||||
clock_resolution: ${clock_resolution}
|
||||
rmt_symbols: ${rmt_symbols}
|
||||
|
||||
|
||||
@@ -9,6 +9,13 @@ esphome:
|
||||
id: template_sens
|
||||
state: !lambda "return 42.0;"
|
||||
|
||||
# Test C++ API: set_template() with stateless lambda (no captures)
|
||||
# NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break.
|
||||
- lambda: |-
|
||||
id(template_sens).set_template([]() -> esphome::optional<float> {
|
||||
return 123.0f;
|
||||
});
|
||||
|
||||
- datetime.date.set:
|
||||
id: test_date
|
||||
date:
|
||||
@@ -215,6 +222,7 @@ cover:
|
||||
|
||||
number:
|
||||
- platform: template
|
||||
id: template_number
|
||||
name: "Template number"
|
||||
optimistic: true
|
||||
min_value: 0
|
||||
|
||||
@@ -32,6 +32,7 @@ switch:
|
||||
name: "Test Switch"
|
||||
id: test_switch
|
||||
optimistic: true
|
||||
lambda: return false;
|
||||
|
||||
interval:
|
||||
- interval: 0.5s
|
||||
|
||||
@@ -152,6 +152,14 @@ def test_main_all_tests_should_run(
|
||||
assert output["memory_impact"]["should_run"] == "false"
|
||||
assert output["cpp_unit_tests_run_all"] is False
|
||||
assert output["cpp_unit_tests_components"] == ["wifi", "api", "sensor"]
|
||||
# component_test_batches should be present and be a list of space-separated strings
|
||||
assert "component_test_batches" in output
|
||||
assert isinstance(output["component_test_batches"], list)
|
||||
# Each batch should be a space-separated string of component names
|
||||
for batch in output["component_test_batches"]:
|
||||
assert isinstance(batch, str)
|
||||
# Should contain at least one component (no empty batches)
|
||||
assert len(batch) > 0
|
||||
|
||||
|
||||
def test_main_no_tests_should_run(
|
||||
@@ -209,6 +217,9 @@ def test_main_no_tests_should_run(
|
||||
assert output["memory_impact"]["should_run"] == "false"
|
||||
assert output["cpp_unit_tests_run_all"] is False
|
||||
assert output["cpp_unit_tests_components"] == []
|
||||
# component_test_batches should be empty list
|
||||
assert "component_test_batches" in output
|
||||
assert output["component_test_batches"] == []
|
||||
|
||||
|
||||
def test_main_with_branch_argument(
|
||||
|
||||
@@ -261,6 +261,17 @@ def test_device_duplicate_id(
|
||||
assert "ID duplicate_device redefined!" in captured.out
|
||||
|
||||
|
||||
def test_substitution_with_id(
|
||||
yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""Test that a ids coming from substitutions do not cause false positive ID redefinition."""
|
||||
load_config_from_fixture(
|
||||
yaml_file, "id_collision_with_substitution.yaml", FIXTURES_DIR
|
||||
)
|
||||
captured = capsys.readouterr()
|
||||
assert "ID some_switch_id redefined!" not in captured.out
|
||||
|
||||
|
||||
def test_add_platform_defines_priority() -> None:
|
||||
"""Test that _add_platform_defines runs after globals.
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
host:
|
||||
|
||||
substitutions:
|
||||
support_switches:
|
||||
- platform: gpio
|
||||
id: some_switch_id
|
||||
pin: 12
|
||||
|
||||
switch: $support_switches
|
||||
@@ -744,7 +744,7 @@ def test_choose_upload_log_host_ota_local_all_options() -> None:
|
||||
check_default=None,
|
||||
purpose=Purpose.UPLOADING,
|
||||
)
|
||||
assert result == ["MQTTIP", "test.local"]
|
||||
assert result == ["MQTTIP"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_serial_ports")
|
||||
@@ -794,7 +794,7 @@ def test_choose_upload_log_host_ota_local_all_options_logging() -> None:
|
||||
check_default=None,
|
||||
purpose=Purpose.LOGGING,
|
||||
)
|
||||
assert result == ["MQTTIP", "MQTT", "test.local"]
|
||||
assert result == ["MQTTIP", "MQTT"]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_no_mqtt_logging")
|
||||
@@ -1564,7 +1564,7 @@ def test_has_resolvable_address() -> None:
|
||||
setup_core(
|
||||
config={CONF_MDNS: {CONF_DISABLED: True}}, address="esphome-device.local"
|
||||
)
|
||||
assert has_resolvable_address() is True
|
||||
assert has_resolvable_address() is False
|
||||
|
||||
# Test with mDNS disabled and regular DNS hostname (resolvable)
|
||||
setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="device.example.com")
|
||||
|
||||
Reference in New Issue
Block a user