1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-02 16:11:53 +00:00

Compare commits

...

47 Commits

Author SHA1 Message Date
J. Nick Koston
410afd196f preen 2025-10-31 11:13:57 -05:00
J. Nick Koston
4e6d74c981 Merge branch 'dev' into fan_fixed 2025-10-31 10:40:02 -05:00
dependabot[bot]
fd64585f99 Bump github/codeql-action from 4.31.0 to 4.31.2 (#11626)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-30 16:50:06 -05:00
Markus
077cce9848 [core] .local addresses are only resolvable if mDNS is enabled (#11508) 2025-10-30 10:08:08 -05:00
J. Nick Koston
bd87e56bc7 [e131] Replace std::set with std::vector to reduce flash usage (#11598) 2025-10-30 15:14:03 +13:00
J. Nick Koston
58235049e3 [template] Eliminate optional wrapper to save 4 bytes RAM per instance (#11610) 2025-10-30 15:06:21 +13:00
J. Nick Koston
29ed3c20af [gpio] Skip set_use_interrupt call when using default value (#11612) 2025-10-30 14:28:38 +13:00
J. Nick Koston
08aae39ea4 [ci] Consolidate component splitting into determine-jobs (#11614) 2025-10-30 14:27:28 +13:00
J. Nick Koston
03fd114371 [ci] Restore parallel execution for clang-tidy split mode (#11613) 2025-10-30 14:26:37 +13:00
Stuart Parmenter
918650f15a [lvgl] memset canvas buffer to prevent display of random garbage (#11582)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-10-29 21:06:45 +00:00
Stuart Parmenter
287f65cbaf [lvgl] fix typo from previous refactor (#11596) 2025-10-30 07:27:31 +11:00
J. Nick Koston
0bb6a6872d Merge branch 'dev' into fan_fixed 2025-10-28 23:47:02 -05:00
J. Nick Koston
372c162e6b make sure no dangling 2025-10-28 23:02:14 -05:00
J. Nick Koston
b635689c29 make sure no dangling 2025-10-28 23:01:28 -05:00
J. Nick Koston
e4aec7f413 make sure no dangling 2025-10-28 22:57:50 -05:00
J. Nick Koston
bb99f68d33 cleanup 2025-10-28 22:47:36 -05:00
J. Nick Koston
47cbe74453 cleanup 2025-10-28 22:41:13 -05:00
J. Nick Koston
cc815fd683 cleanup 2025-10-28 22:40:56 -05:00
J. Nick Koston
4cc41606d1 cleanup 2025-10-28 22:40:45 -05:00
J. Nick Koston
6cf0a38b86 preen 2025-10-28 22:26:27 -05:00
J. Nick Koston
5e6ce6ee48 Merge branch 'dev' into fan_fixed 2025-10-28 22:15:50 -05:00
J. Nick Koston
091c12cb48 preen 2025-10-22 13:29:14 -10:00
J. Nick Koston
39b93079e5 simp 2025-10-22 13:26:53 -10:00
J. Nick Koston
93c555ae87 reset 2025-10-22 13:18:14 -10:00
J. Nick Koston
977dd9dd34 manual copy 2025-10-22 12:29:23 -10:00
J. Nick Koston
fe6f877185 manual copy 2025-10-22 12:28:51 -10:00
J. Nick Koston
c7aef0016a manual copy 2025-10-22 12:27:29 -10:00
J. Nick Koston
c69e7f4e78 init 2025-10-22 12:25:35 -10:00
J. Nick Koston
6d1ee10742 manual copy 2025-10-22 12:24:47 -10:00
J. Nick Koston
516889f35e Merge remote-tracking branch 'origin/fan_fixed' into fan_fixed 2025-10-22 12:02:31 -10:00
J. Nick Koston
26e4754673 fixed 2025-10-22 12:02:20 -10:00
J. Nick Koston
a3b3032319 Merge branch 'dev' into fan_fixed 2025-10-22 11:56:27 -10:00
J. Nick Koston
b0f764a37e fixed 2025-10-22 11:52:15 -10:00
J. Nick Koston
5c7029623e fixed 2025-10-22 11:44:42 -10:00
J. Nick Koston
fdb23a2c13 fixed 2025-10-22 11:42:31 -10:00
J. Nick Koston
43bcd98649 fixed 2025-10-22 11:41:15 -10:00
J. Nick Koston
274c0505f7 fixed 2025-10-22 11:38:52 -10:00
J. Nick Koston
eaf0a367b4 fixed 2025-10-22 11:37:19 -10:00
J. Nick Koston
657e6f0bce fixed 2025-10-22 11:28:53 -10:00
J. Nick Koston
935acc7d5e fixed 2025-10-22 11:24:12 -10:00
J. Nick Koston
acd24402dd reduce scope 2025-10-22 11:16:28 -10:00
J. Nick Koston
ac36b97262 reduce scope 2025-10-22 11:16:13 -10:00
J. Nick Koston
828f2addcd Merge remote-tracking branch 'origin/fan_fixed' into fan_fixed 2025-10-22 11:09:23 -10:00
J. Nick Koston
f11e8e36b5 missed 2025-10-22 11:09:10 -10:00
J. Nick Koston
788c402cfe Merge branch 'fan_base_tests' into fan_fixed 2025-10-22 11:05:09 -10:00
J. Nick Koston
04d127015c Add basic fan compile tests
baseline for https://github.com/esphome/esphome/pull/11483
2025-10-22 11:04:38 -10:00
J. Nick Koston
f559fad4fc [fan] Use FixedVector for preset modes, preserve config order (breaking) 2025-10-22 11:03:32 -10:00
51 changed files with 348 additions and 261 deletions

View File

@@ -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: |
@@ -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

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
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@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
category: "/language:${{matrix.language}}"

View File

@@ -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):

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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))

View File

@@ -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_();

View File

@@ -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;
}

View File

@@ -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,11 +70,14 @@ class CanvasType(WidgetType):
width = config[CONF_WIDTH]
height = config[CONF_HEIGHT]
use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else ""
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,
lv.custom_mem_alloc(
literal(f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})")
),
buf,
width,
height,
literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"),

View File

@@ -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

View File

@@ -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_

View File

@@ -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_

View File

@@ -33,12 +33,13 @@ 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_)();
auto s = this->state_f_();
if (s.has_value()) {
auto pos = clamp(*s, 0.0f, 1.0f);
if (pos != this->position) {
@@ -46,24 +47,21 @@ void TemplateCover::loop() {
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;
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;
}
}
}
if (changed)
this->publish_state();
}
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; }

View File

@@ -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_;

View File

@@ -40,14 +40,13 @@ void TemplateDate::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
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) {

View File

@@ -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_;
};

View File

@@ -43,10 +43,8 @@ void TemplateDateTime::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
auto val = this->f_();
if (val.has_value()) {
this->year_ = val->year;
this->month_ = val->month;
this->day_ = val->day_of_month;
@@ -54,6 +52,7 @@ void TemplateDateTime::update() {
this->minute_ = val->minute;
this->second_ = val->second;
this->publish_state();
}
}
void TemplateDateTime::control(const datetime::DateTimeCall &call) {

View File

@@ -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_;
};

View File

@@ -40,14 +40,13 @@ void TemplateTime::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
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) {

View File

@@ -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_;
};

View File

@@ -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_

View File

@@ -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();
}
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_; }

View File

@@ -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_;

View File

@@ -30,11 +30,10 @@ void TemplateNumber::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
auto val = this->f_();
if (val.has_value()) {
this->publish_state(*val);
}
}
void TemplateNumber::control(float value) {

View File

@@ -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_;
};

View File

@@ -31,16 +31,14 @@ void TemplateSelect::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
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);
}
}
void TemplateSelect::control(const std::string &value) {

View File

@@ -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_;
};

View File

@@ -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);

View File

@@ -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_

View File

@@ -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;
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()) {

View File

@@ -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_;

View File

@@ -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;
}
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;
auto val = this->f_();
if (val.has_value()) {
this->publish_state(*val);
}
}
void TemplateText::control(const std::string &value) {

View File

@@ -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;
};

View File

@@ -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_

View File

@@ -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_

View File

@@ -33,13 +33,14 @@ 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_)();
auto s = this->state_f_();
if (s.has_value()) {
auto pos = clamp(*s, 0.0f, 1.0f);
if (pos != this->position) {
@@ -47,7 +48,6 @@ void TemplateValve::loop() {
changed = true;
}
}
}
if (changed)
this->publish_state();
@@ -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_; }

View File

@@ -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_;

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -32,6 +32,7 @@ switch:
name: "Test Switch"
id: test_switch
optimistic: true
lambda: return false;
interval:
- interval: 0.5s

View File

@@ -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(

View File

@@ -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")