1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-13 21:35:48 +00:00

Compare commits

..

14 Commits

Author SHA1 Message Date
J. Nick Koston
94186517e0 DNM: Test CI 2025-11-01 03:10:49 -05:00
J. Nick Koston
2ff3280e8c DNM: Test CI 2025-11-01 03:09:13 -05:00
J. Nick Koston
fe0d9828a8 preen 2025-11-01 03:04:55 -05:00
J. Nick Koston
b6a31a24bc preen 2025-11-01 02:55:52 -05:00
J. Nick Koston
bb971e7b17 preen 2025-11-01 02:52:15 -05:00
J. Nick Koston
5d868cd64c Merge remote-tracking branch 'origin/cache_components_graph_ci' into cache_components_graph_ci 2025-11-01 02:49:32 -05:00
J. Nick Koston
9af24944af preen 2025-11-01 02:49:06 -05:00
J. Nick Koston
09113e2e02 preen 2025-11-01 02:46:45 -05:00
J. Nick Koston
b18a2542ee preen 2025-11-01 02:44:00 -05:00
J. Nick Koston
c4c72ede2c preen 2025-11-01 02:43:34 -05:00
J. Nick Koston
c6e3261a6a preen 2025-11-01 02:39:35 -05:00
J. Nick Koston
9f03c40656 cache 2025-11-01 02:34:40 -05:00
J. Nick Koston
ce4ed4b5a6 cache 2025-11-01 02:32:05 -05:00
J. Nick Koston
d1be68d808 cache 2025-11-01 02:30:54 -05:00
1056 changed files with 5439 additions and 11502 deletions

View File

@@ -51,79 +51,7 @@ This document provides essential context for AI models interacting with this pro
* **Naming Conventions:** * **Naming Conventions:**
* **Python:** Follows PEP 8. Use clear, descriptive names following snake_case. * **Python:** Follows PEP 8. Use clear, descriptive names following snake_case.
* **C++:** Follows the Google C++ Style Guide with these specifics (following clang-tidy conventions): * **C++:** Follows the Google C++ Style Guide.
- Function, method, and variable names: `lower_snake_case`
- Class/struct/enum names: `UpperCamelCase`
- Top-level constants (global/namespace scope): `UPPER_SNAKE_CASE`
- Function-local constants: `lower_snake_case`
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
- Favor descriptive names over abbreviations
* **C++ Field Visibility:**
* **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`.
* **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants:
1. **Pointer lifetime issues:** When setters validate and store pointers from known lists to prevent dangling references.
```cpp
// Helper to find matching string in vector and return its pointer
inline const char *vector_find(const std::vector<const char *> &vec, const char *value) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
return item;
}
return nullptr;
}
class ClimateDevice {
public:
void set_custom_fan_modes(std::initializer_list<const char *> modes) {
this->custom_fan_modes_ = modes;
this->active_custom_fan_mode_ = nullptr; // Reset when modes change
}
bool set_custom_fan_mode(const char *mode) {
// Find mode in supported list and store that pointer (not the input pointer)
const char *validated_mode = vector_find(this->custom_fan_modes_, mode);
if (validated_mode != nullptr) {
this->active_custom_fan_mode_ = validated_mode;
return true;
}
return false;
}
private:
std::vector<const char *> custom_fan_modes_; // Pointers to string literals in flash
const char *active_custom_fan_mode_{nullptr}; // Must point to entry in custom_fan_modes_
};
```
2. **Invariant coupling:** When multiple fields must remain synchronized to prevent buffer overflows or data corruption.
```cpp
class Buffer {
public:
void resize(size_t new_size) {
auto new_data = std::make_unique<uint8_t[]>(new_size);
if (this->data_) {
std::memcpy(new_data.get(), this->data_.get(), std::min(this->size_, new_size));
}
this->data_ = std::move(new_data);
this->size_ = new_size; // Must stay in sync with data_
}
private:
std::unique_ptr<uint8_t[]> data_;
size_t size_{0}; // Must match allocated size of data_
};
```
3. **Resource management:** When setters perform cleanup or registration operations that derived classes might skip.
* **Provide `protected` accessor methods:** When derived classes need controlled access to `private` members.
* **C++ Preprocessor Directives:**
* **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums.
* **Use `#define` only for:**
- Conditional compilation (`#ifdef`, `#ifndef`)
- Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`)
* **C++ Additional Conventions:**
* **Member access:** Prefix all class member access with `this->` (e.g., `this->value_` not `value_`)
* **Indentation:** Use spaces (two per indentation level), not tabs
* **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
* **Line length:** Wrap lines at no more than 120 characters
* **Component Structure:** * **Component Structure:**
* **Standard Files:** * **Standard Files:**
@@ -172,7 +100,8 @@ This document provides essential context for AI models interacting with this pro
* **C++ Class Pattern:** * **C++ Class Pattern:**
```cpp ```cpp
namespace esphome::my_component { namespace esphome {
namespace my_component {
class MyComponent : public Component { class MyComponent : public Component {
public: public:
@@ -188,7 +117,8 @@ This document provides essential context for AI models interacting with this pro
int param_{0}; int param_{0};
}; };
} // namespace esphome::my_component } // namespace my_component
} // namespace esphome
``` ```
* **Common Component Examples:** * **Common Component Examples:**
@@ -438,45 +368,3 @@ This document provides essential context for AI models interacting with this pro
* **Python:** When adding a new Python dependency, add it to the appropriate `requirements*.txt` file and `pyproject.toml`. * **Python:** When adding a new Python dependency, add it to the appropriate `requirements*.txt` file and `pyproject.toml`.
* **C++ / PlatformIO:** When adding a new C++ dependency, add it to `platformio.ini` and use `cg.add_library`. * **C++ / PlatformIO:** When adding a new C++ dependency, add it to `platformio.ini` and use `cg.add_library`.
* **Build Flags:** Use `cg.add_build_flag(...)` to add compiler flags. * **Build Flags:** Use `cg.add_build_flag(...)` to add compiler flags.
## 8. Public API and Breaking Changes
* **Public C++ API:**
* **Components**: Only documented features at [esphome.io](https://esphome.io) are public API. Undocumented `public` members are internal.
* **Core/Base Classes** (`esphome/core/`, `Component`, `Sensor`, etc.): All `public` members are public API.
* **Components with Global Accessors** (`global_api_server`, etc.): All `public` members are public API (except config setters).
* **Public Python API:**
* All documented configuration options at [esphome.io](https://esphome.io) are public API.
* Python code in `esphome/core/` actively used by existing core components is considered stable API.
* Other Python code is internal unless explicitly documented for external component use.
* **Breaking Changes Policy:**
* Aim for **6-month deprecation window** when possible
* Clean breaks allowed for: signature changes, deep refactorings, resource constraints
* Must document migration path in PR description (generates release notes)
* Blog post required for core/base class changes or significant architectural changes
* Full details: https://developers.esphome.io/contributing/code/#public-api-and-breaking-changes
* **Breaking Change Checklist:**
- [ ] Clear justification (RAM/flash savings, architectural improvement)
- [ ] Explored non-breaking alternatives
- [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++)
- [ ] Documented migration path in PR description with before/after examples
- [ ] Updated all internal usage and esphome-docs
- [ ] Tested backward compatibility during deprecation period
* **Deprecation Pattern (C++):**
```cpp
// Remove before 2026.6.0
ESPDEPRECATED("Use new_method() instead. Removed in 2026.6.0", "2025.12.0")
void old_method() { this->new_method(); }
```
* **Deprecation Pattern (Python):**
```python
# Remove before 2026.6.0
if CONF_OLD_KEY in config:
_LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0")
config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate
```

View File

@@ -196,7 +196,7 @@ jobs:
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: .temp/components_graph.json path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} key: components-graph-${{ hashFiles('esphome/components/**/__init__.py') }}
- name: Determine which tests to run - name: Determine which tests to run
id: determine id: determine
env: env:
@@ -222,11 +222,11 @@ jobs:
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $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 echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
- name: Save components graph cache - name: Save components graph cache
if: github.ref == 'refs/heads/dev' # if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: .temp/components_graph.json path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} key: components-graph-${{ hashFiles('esphome/components/**/__init__.py') }}
integration-tests: integration-tests:
name: Run integration tests name: Run integration tests

View File

@@ -21,7 +21,7 @@ permissions:
jobs: jobs:
request-codeowner-reviews: request-codeowner-reviews:
name: Run name: Run
if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }} if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Request reviews from component codeowners - name: Request reviews from component codeowners

View File

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

View File

@@ -155,7 +155,6 @@ esphome/components/esp32_ble_tracker/* @bdraco
esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_hosted/* @swoboda1337 esphome/components/esp32_hosted/* @swoboda1337
esphome/components/esp32_hosted/update/* @swoboda1337
esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_improv/* @jesserockz
esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz
@@ -181,7 +180,7 @@ esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval esphome/components/gl_r01_i2c/* @pkejval
esphome/components/globals/* @esphome/core esphome/components/globals/* @esphome/core
esphome/components/gp2y1010au0f/* @zry98 esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz @sebydocky esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core esphome/components/gpio/* @esphome/core
esphome/components/gpio/one_wire/* @ssieb esphome/components/gpio/one_wire/* @ssieb
esphome/components/gps/* @coogle @ximex esphome/components/gps/* @coogle @ximex
@@ -206,7 +205,6 @@ esphome/components/hdc2010/* @optimusprimespace @ssieb
esphome/components/he60r/* @clydebarrow esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hlk_fm22x/* @OnFreund
esphome/components/hm3301/* @freekode esphome/components/hm3301/* @freekode
esphome/components/hmac_md5/* @dwmw2 esphome/components/hmac_md5/* @dwmw2
esphome/components/homeassistant/* @esphome/core @OttoWinter esphome/components/homeassistant/* @esphome/core @OttoWinter
@@ -291,7 +289,6 @@ esphome/components/mcp23x17_base/* @jesserockz
esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz
esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp2515/* @danielschramm @mvturnho
esphome/components/mcp3204/* @rsumner esphome/components/mcp3204/* @rsumner
esphome/components/mcp3221/* @philippderdiedas
esphome/components/mcp4461/* @p1ngb4ck esphome/components/mcp4461/* @p1ngb4ck
esphome/components/mcp4728/* @berfenger esphome/components/mcp4728/* @berfenger
esphome/components/mcp47a1/* @jesserockz esphome/components/mcp47a1/* @jesserockz
@@ -396,7 +393,6 @@ esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet esphome/components/rtttl/* @glmnet
esphome/components/runtime_stats/* @bdraco esphome/components/runtime_stats/* @bdraco
esphome/components/rx8130/* @beormund
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core esphome/components/script/* @esphome/core
@@ -483,7 +479,6 @@ esphome/components/template/fan/* @ssieb
esphome/components/text/* @mauritskorse esphome/components/text/* @mauritskorse
esphome/components/thermostat/* @kbx81 esphome/components/thermostat/* @kbx81
esphome/components/time/* @esphome/core esphome/components/time/* @esphome/core
esphome/components/tinyusb/* @kbx81
esphome/components/tlc5947/* @rnauber esphome/components/tlc5947/* @rnauber
esphome/components/tlc5971/* @IJIJI esphome/components/tlc5971/* @IJIJI
esphome/components/tm1621/* @Philippe12 esphome/components/tm1621/* @Philippe12

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2025.11.0b1 PROJECT_NUMBER = 2025.11.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a

View File

@@ -15,7 +15,7 @@ from esphome.const import (
CONF_TYPE_ID, CONF_TYPE_ID,
CONF_UPDATE_INTERVAL, CONF_UPDATE_INTERVAL,
) )
from esphome.core import ID, Lambda from esphome.core import ID
from esphome.cpp_generator import ( from esphome.cpp_generator import (
LambdaExpression, LambdaExpression,
MockObj, MockObj,
@@ -310,30 +310,6 @@ async def for_condition_to_code(
return var return var
@register_condition(
"component.is_idle",
LambdaCondition,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.Component),
}
),
)
async def component_is_idle_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
lambda_ = await cg.process_lambda(
Lambda(f"return {comp}->is_idle();"), args, return_type=bool
)
return new_lambda_pvariable(
condition_id, lambda_, StatelessLambdaCondition, template_arg
)
@register_action( @register_action(
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds) "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
) )

View File

@@ -105,7 +105,7 @@ template<typename... Ts> class AGS10NewI2cAddressAction : public Action<Ts...>,
public: public:
TEMPLATABLE_VALUE(uint8_t, new_address) TEMPLATABLE_VALUE(uint8_t, new_address)
void play(const Ts &...x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); }
}; };
enum AGS10SetZeroPointActionMode { enum AGS10SetZeroPointActionMode {
@@ -122,7 +122,7 @@ template<typename... Ts> class AGS10SetZeroPointAction : public Action<Ts...>, p
TEMPLATABLE_VALUE(uint16_t, value) TEMPLATABLE_VALUE(uint16_t, value)
TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode) TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode)
void play(const Ts &...x) override { void play(Ts... x) override {
switch (this->mode_.value(x...)) { switch (this->mode_.value(x...)) {
case FACTORY_DEFAULT: case FACTORY_DEFAULT:
this->parent_->set_zero_point_with_factory_defaults(); this->parent_->set_zero_point_with_factory_defaults();

View File

@@ -13,7 +13,7 @@ template<typename... Ts> class SetAutoMuteAction : public Action<Ts...> {
TEMPLATABLE_VALUE(uint8_t, auto_mute_mode) TEMPLATABLE_VALUE(uint8_t, auto_mute_mode)
void play(const Ts &...x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); } void play(Ts... x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); }
protected: protected:
AIC3204 *aic3204_; AIC3204 *aic3204_;

View File

@@ -1,9 +1,7 @@
#include "alarm_control_panel.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include <utility> #include <utility>
#include "alarm_control_panel.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -36,9 +34,6 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state; this->current_state_ = state;
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_alarm_control_panel_update(this);
#endif
if (state == ACP_STATE_TRIGGERED) { if (state == ACP_STATE_TRIGGERED) {
this->triggered_callback_.call(); this->triggered_callback_.call();
} else if (state == ACP_STATE_ARMING) { } else if (state == ACP_STATE_ARMING) {

View File

@@ -89,7 +89,7 @@ template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->alarm_control_panel_->make_call(); auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...); auto code = this->code_.optional_value(x...);
if (code.has_value()) { if (code.has_value()) {
@@ -109,7 +109,7 @@ template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->alarm_control_panel_->make_call(); auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...); auto code = this->code_.optional_value(x...);
if (code.has_value()) { if (code.has_value()) {
@@ -129,7 +129,7 @@ template<typename... Ts> class ArmNightAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->alarm_control_panel_->make_call(); auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...); auto code = this->code_.optional_value(x...);
if (code.has_value()) { if (code.has_value()) {
@@ -149,7 +149,7 @@ template<typename... Ts> class DisarmAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } void play(Ts... x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); }
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -159,7 +159,7 @@ template<typename... Ts> class PendingAction : public Action<Ts...> {
public: public:
explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
void play(const Ts &...x) override { this->alarm_control_panel_->make_call().pending().perform(); } void play(Ts... x) override { this->alarm_control_panel_->make_call().pending().perform(); }
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -169,7 +169,7 @@ template<typename... Ts> class TriggeredAction : public Action<Ts...> {
public: public:
explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
void play(const Ts &...x) override { this->alarm_control_panel_->make_call().triggered().perform(); } void play(Ts... x) override { this->alarm_control_panel_->make_call().triggered().perform(); }
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -178,7 +178,7 @@ template<typename... Ts> class TriggeredAction : public Action<Ts...> {
template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts...> { template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts...> {
public: public:
AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {} AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {}
bool check(const Ts &...x) override { bool check(Ts... x) override {
return this->parent_->is_state_armed(this->parent_->get_state()) || return this->parent_->is_state_armed(this->parent_->get_state()) ||
this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED; this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED;
} }

View File

@@ -39,7 +39,7 @@ class Animation : public image::Image {
template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> { template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> {
public: public:
AnimationNextFrameAction(Animation *parent) : parent_(parent) {} AnimationNextFrameAction(Animation *parent) : parent_(parent) {}
void play(const Ts &...x) override { this->parent_->next_frame(); } void play(Ts... x) override { this->parent_->next_frame(); }
protected: protected:
Animation *parent_; Animation *parent_;
@@ -48,7 +48,7 @@ template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> {
template<typename... Ts> class AnimationPrevFrameAction : public Action<Ts...> { template<typename... Ts> class AnimationPrevFrameAction : public Action<Ts...> {
public: public:
AnimationPrevFrameAction(Animation *parent) : parent_(parent) {} AnimationPrevFrameAction(Animation *parent) : parent_(parent) {}
void play(const Ts &...x) override { this->parent_->prev_frame(); } void play(Ts... x) override { this->parent_->prev_frame(); }
protected: protected:
Animation *parent_; Animation *parent_;
@@ -58,7 +58,7 @@ template<typename... Ts> class AnimationSetFrameAction : public Action<Ts...> {
public: public:
AnimationSetFrameAction(Animation *parent) : parent_(parent) {} AnimationSetFrameAction(Animation *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(uint16_t, frame) TEMPLATABLE_VALUE(uint16_t, frame)
void play(const Ts &...x) override { this->parent_->set_frame(this->frame_.value(x...)); } void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); }
protected: protected:
Animation *parent_; Animation *parent_;

View File

@@ -227,7 +227,6 @@ CONFIG_SCHEMA = cv.All(
esp32=8, # More RAM, can buffer more esp32=8, # More RAM, can buffer more
rp2040=5, # Limited RAM rp2040=5, # Limited RAM
bk72xx=8, # Moderate RAM bk72xx=8, # Moderate RAM
nrf52=8, # Moderate RAM
rtl87xx=8, # Moderate RAM rtl87xx=8, # Moderate RAM
host=16, # Abundant resources host=16, # Abundant resources
ln882x=8, # Moderate RAM ln882x=8, # Moderate RAM
@@ -245,9 +244,6 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
# Track controller registration for StaticVector sizing
CORE.register_controller()
cg.add(var.set_port(config[CONF_PORT])) cg.add(var.set_port(config[CONF_PORT]))
if config[CONF_PASSWORD]: if config[CONF_PASSWORD]:
cg.add_define("USE_API_PASSWORD") cg.add_define("USE_API_PASSWORD")

View File

@@ -425,7 +425,7 @@ message ListEntitiesFanResponse {
bool disabled_by_default = 9; bool disabled_by_default = 9;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 11; EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"]; repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"];
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
} }
// Deprecated in API version 1.6 - only used in deprecated fields // Deprecated in API version 1.6 - only used in deprecated fields
@@ -1000,9 +1000,9 @@ message ListEntitiesClimateResponse {
bool supports_action = 12; // Deprecated: use feature_flags bool supports_action = 12; // Deprecated: use feature_flags
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"];
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"];
repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "std::vector<const char *>"]; repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"];
repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"]; repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"];
bool disabled_by_default = 18; bool disabled_by_default = 18;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20; EntityCategory entity_category = 20;
@@ -2147,7 +2147,7 @@ message ListEntitiesEventResponse {
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
string device_class = 8; string device_class = 8;
repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector<const char *>"]; repeated string event_types = 9;
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
} }
message EventResponse { message EventResponse {

View File

@@ -1,3 +1,4 @@
// X:
#include "api_connection.h" #include "api_connection.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@@ -410,8 +411,8 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
} }
if (traits.supports_direction()) if (traits.supports_direction())
msg.direction = static_cast<enums::FanDirection>(fan->direction); msg.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes() && fan->has_preset_mode()) if (traits.supports_preset_modes())
msg.set_preset_mode(StringRef(fan->get_preset_mode())); msg.set_preset_mode(StringRef(fan->preset_mode));
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -423,7 +424,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
msg.supports_speed = traits.supports_speed(); msg.supports_speed = traits.supports_speed();
msg.supports_direction = traits.supports_direction(); msg.supports_direction = traits.supports_direction();
msg.supported_speed_count = traits.supported_speed_count(); msg.supported_speed_count = traits.supported_speed_count();
msg.supported_preset_modes = &traits.supported_preset_modes(); msg.supported_preset_modes = &traits.supported_preset_modes_for_api_();
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::fan_command(const FanCommandRequest &msg) { void APIConnection::fan_command(const FanCommandRequest &msg) {
@@ -637,14 +638,14 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
} }
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value()); resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) { if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) {
resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode())); resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value()));
} }
if (traits.get_supports_presets() && climate->preset.has_value()) { if (traits.get_supports_presets() && climate->preset.has_value()) {
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value()); resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
} }
if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) { if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) {
resp.set_custom_preset(StringRef(climate->get_custom_preset())); resp.set_custom_preset(StringRef(climate->custom_preset.value()));
} }
if (traits.get_supports_swing_modes()) if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode); resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
@@ -877,7 +878,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
bool is_single) { bool is_single) {
auto *select = static_cast<select::Select *>(entity); auto *select = static_cast<select::Select *>(entity);
SelectStateResponse resp; SelectStateResponse resp;
resp.set_state(StringRef(select->current_option())); resp.set_state(StringRef(select->state));
resp.missing_state = !select->has_state(); resp.missing_state = !select->has_state();
return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@@ -1310,7 +1311,8 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
auto *event = static_cast<event::Event *>(entity); auto *event = static_cast<event::Event *>(entity);
ListEntitiesEventResponse msg; ListEntitiesEventResponse msg;
msg.set_device_class(event->get_device_class_ref()); msg.set_device_class(event->get_device_class_ref());
msg.event_types = &event->get_event_types(); for (const auto &event_type : event->get_event_types())
msg.event_types.push_back(event_type);
return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }
@@ -1467,8 +1469,6 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
static constexpr auto MANUFACTURER = StringRef::from_lit("Beken"); static constexpr auto MANUFACTURER = StringRef::from_lit("Beken");
#elif defined(USE_LN882X) #elif defined(USE_LN882X)
static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning"); static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning");
#elif defined(USE_NRF52)
static constexpr auto MANUFACTURER = StringRef::from_lit("Nordic Semiconductor");
#elif defined(USE_RTL87XX) #elif defined(USE_RTL87XX)
static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek"); static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek");
#elif defined(USE_HOST) #elif defined(USE_HOST)

View File

@@ -434,7 +434,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
return APIError::OK; return APIError::OK;
} }
uint8_t *buffer_data = buffer.get_buffer()->data(); std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear(); this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size()); this->reusable_iovs_.reserve(packets.size());

View File

@@ -230,7 +230,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
return APIError::OK; return APIError::OK;
} }
uint8_t *buffer_data = buffer.get_buffer()->data(); std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear(); this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size()); this->reusable_iovs_.reserve(packets.size());

View File

@@ -355,8 +355,8 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(10, this->icon_ref_); buffer.encode_string(10, this->icon_ref_);
#endif #endif
buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category)); buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category));
for (const char *it : *this->supported_preset_modes) { for (const auto &it : *this->supported_preset_modes) {
buffer.encode_string(12, it, strlen(it), true); buffer.encode_string(12, it, true);
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES
buffer.encode_uint32(13, this->device_id); buffer.encode_uint32(13, this->device_id);
@@ -376,8 +376,8 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const {
#endif #endif
size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
if (!this->supported_preset_modes->empty()) { if (!this->supported_preset_modes->empty()) {
for (const char *it : *this->supported_preset_modes) { for (const auto &it : *this->supported_preset_modes) {
size.add_length_force(1, strlen(it)); size.add_length_force(1, it.size());
} }
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES
@@ -1179,14 +1179,14 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
for (const auto &it : *this->supported_swing_modes) { for (const auto &it : *this->supported_swing_modes) {
buffer.encode_uint32(14, static_cast<uint32_t>(it), true); buffer.encode_uint32(14, static_cast<uint32_t>(it), true);
} }
for (const char *it : *this->supported_custom_fan_modes) { for (const auto &it : *this->supported_custom_fan_modes) {
buffer.encode_string(15, it, strlen(it), true); buffer.encode_string(15, it, true);
} }
for (const auto &it : *this->supported_presets) { for (const auto &it : *this->supported_presets) {
buffer.encode_uint32(16, static_cast<uint32_t>(it), true); buffer.encode_uint32(16, static_cast<uint32_t>(it), true);
} }
for (const char *it : *this->supported_custom_presets) { for (const auto &it : *this->supported_custom_presets) {
buffer.encode_string(17, it, strlen(it), true); buffer.encode_string(17, it, true);
} }
buffer.encode_bool(18, this->disabled_by_default); buffer.encode_bool(18, this->disabled_by_default);
#ifdef USE_ENTITY_ICON #ifdef USE_ENTITY_ICON
@@ -1229,8 +1229,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
} }
} }
if (!this->supported_custom_fan_modes->empty()) { if (!this->supported_custom_fan_modes->empty()) {
for (const char *it : *this->supported_custom_fan_modes) { for (const auto &it : *this->supported_custom_fan_modes) {
size.add_length_force(1, strlen(it)); size.add_length_force(1, it.size());
} }
} }
if (!this->supported_presets->empty()) { if (!this->supported_presets->empty()) {
@@ -1239,8 +1239,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
} }
} }
if (!this->supported_custom_presets->empty()) { if (!this->supported_custom_presets->empty()) {
for (const char *it : *this->supported_custom_presets) { for (const auto &it : *this->supported_custom_presets) {
size.add_length_force(2, strlen(it)); size.add_length_force(2, it.size());
} }
} }
size.add_bool(2, this->disabled_by_default); size.add_bool(2, this->disabled_by_default);
@@ -2877,8 +2877,8 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(6, this->disabled_by_default); buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category));
buffer.encode_string(8, this->device_class_ref_); buffer.encode_string(8, this->device_class_ref_);
for (const char *it : *this->event_types) { for (auto &it : this->event_types) {
buffer.encode_string(9, it, strlen(it), true); buffer.encode_string(9, it, true);
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES
buffer.encode_uint32(10, this->device_id); buffer.encode_uint32(10, this->device_id);
@@ -2894,9 +2894,9 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const {
size.add_bool(1, this->disabled_by_default); size.add_bool(1, this->disabled_by_default);
size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
size.add_length(1, this->device_class_ref_.size()); size.add_length(1, this->device_class_ref_.size());
if (!this->event_types->empty()) { if (!this->event_types.empty()) {
for (const char *it : *this->event_types) { for (const auto &it : this->event_types) {
size.add_length_force(1, strlen(it)); size.add_length_force(1, it.size());
} }
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES

View File

@@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
bool supports_speed{false}; bool supports_speed{false};
bool supports_direction{false}; bool supports_direction{false};
int32_t supported_speed_count{0}; int32_t supported_speed_count{0};
const std::vector<const char *> *supported_preset_modes{}; const std::set<std::string> *supported_preset_modes{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1384,9 +1384,9 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
bool supports_action{false}; bool supports_action{false};
const climate::ClimateFanModeMask *supported_fan_modes{}; const climate::ClimateFanModeMask *supported_fan_modes{};
const climate::ClimateSwingModeMask *supported_swing_modes{}; const climate::ClimateSwingModeMask *supported_swing_modes{};
const std::vector<const char *> *supported_custom_fan_modes{}; const std::vector<std::string> *supported_custom_fan_modes{};
const climate::ClimatePresetMask *supported_presets{}; const climate::ClimatePresetMask *supported_presets{};
const std::vector<const char *> *supported_custom_presets{}; const std::vector<std::string> *supported_custom_presets{};
float visual_current_temperature_step{0.0f}; float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false}; bool supports_current_humidity{false};
bool supports_target_humidity{false}; bool supports_target_humidity{false};
@@ -2788,7 +2788,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage {
#endif #endif
StringRef device_class_ref_{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
const FixedVector<const char *> *event_types{}; std::vector<std::string> event_types{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP

View File

@@ -2053,7 +2053,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const {
dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "device_class", this->device_class_ref_); dump_field(out, "device_class", this->device_class_ref_);
for (const auto &it : *this->event_types) { for (const auto &it : this->event_types) {
dump_field(out, "event_types", it, 4); dump_field(out, "event_types", it, 4);
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES

View File

@@ -5,7 +5,6 @@
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
@@ -35,7 +34,7 @@ APIServer::APIServer() {
} }
void APIServer::setup() { void APIServer::setup() {
ControllerRegistry::register_controller(this); this->setup_controller();
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
uint32_t hash = 88491486UL; uint32_t hash = 88491486UL;
@@ -225,7 +224,7 @@ void APIServer::dump_config() {
" Address: %s:%u\n" " Address: %s:%u\n"
" Listen backlog: %u\n" " Listen backlog: %u\n"
" Max connections: %u", " Max connections: %u",
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_); network::get_use_address().c_str(), this->port_, this->listen_backlog_, this->max_connections_);
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
if (!this->noise_ctx_->has_psk()) { if (!this->noise_ctx_->has_psk()) {
@@ -270,7 +269,7 @@ bool APIServer::check_password(const uint8_t *password_data, size_t password_len
void APIServer::handle_disconnect(APIConnection *conn) {} void APIServer::handle_disconnect(APIConnection *conn) {}
// Macro for controller update dispatch // Macro for entities without extra parameters
#define API_DISPATCH_UPDATE(entity_type, entity_name) \ #define API_DISPATCH_UPDATE(entity_type, entity_name) \
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \ if (obj->is_internal()) \
@@ -279,6 +278,15 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
c->send_##entity_name##_state(obj); \ c->send_##entity_name##_state(obj); \
} }
// Macro for entities with extra parameters (but parameters not used in send)
#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \
void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor) API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
#endif #endif
@@ -296,15 +304,15 @@ API_DISPATCH_UPDATE(light::LightState, light)
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
API_DISPATCH_UPDATE(sensor::Sensor, sensor) API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state)
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
API_DISPATCH_UPDATE(switch_::Switch, switch) API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state)
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
API_DISPATCH_UPDATE(text_sensor::TextSensor, text_sensor) API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state)
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
@@ -312,7 +320,7 @@ API_DISPATCH_UPDATE(climate::Climate, climate)
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
API_DISPATCH_UPDATE(number::Number, number) API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state)
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
@@ -328,11 +336,11 @@ API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
API_DISPATCH_UPDATE(text::Text, text) API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state)
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
API_DISPATCH_UPDATE(select::Select, select) API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index)
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
@@ -348,13 +356,12 @@ API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
// Event is a special case - unlike other entities with simple state fields, // Event is a special case - it's the only entity that passes extra parameters to the send method
// events store their state in a member accessed via obj->get_last_event_type() void APIServer::on_event(event::Event *obj, const std::string &event_type) {
void APIServer::on_event(event::Event *obj) {
if (obj->is_internal()) if (obj->is_internal())
return; return;
for (auto &c : this->clients_) for (auto &c : this->clients_)
c->send_event(obj, obj->get_last_event_type()); c->send_event(obj, event_type);
} }
#endif #endif

View File

@@ -72,19 +72,19 @@ class APIServer : public Component, public Controller {
void on_light_update(light::LightState *obj) override; void on_light_update(light::LightState *obj) override;
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
void on_sensor_update(sensor::Sensor *obj) override; void on_sensor_update(sensor::Sensor *obj, float state) override;
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
void on_switch_update(switch_::Switch *obj) override; void on_switch_update(switch_::Switch *obj, bool state) override;
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
void on_text_sensor_update(text_sensor::TextSensor *obj) override; void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override;
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
void on_climate_update(climate::Climate *obj) override; void on_climate_update(climate::Climate *obj) override;
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
void on_number_update(number::Number *obj) override; void on_number_update(number::Number *obj, float state) override;
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
void on_date_update(datetime::DateEntity *obj) override; void on_date_update(datetime::DateEntity *obj) override;
@@ -96,10 +96,10 @@ class APIServer : public Component, public Controller {
void on_datetime_update(datetime::DateTimeEntity *obj) override; void on_datetime_update(datetime::DateTimeEntity *obj) override;
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
void on_text_update(text::Text *obj) override; void on_text_update(text::Text *obj, const std::string &state) override;
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
void on_select_update(select::Select *obj) override; void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
void on_lock_update(lock::Lock *obj) override; void on_lock_update(lock::Lock *obj) override;
@@ -141,7 +141,7 @@ class APIServer : public Component, public Controller {
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override;
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
void on_event(event::Event *obj) override; void on_event(event::Event *obj, const std::string &event_type) override;
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
void on_update(update::UpdateEntity *obj) override; void on_update(update::UpdateEntity *obj) override;
@@ -237,7 +237,7 @@ extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-cons
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> { template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
public: public:
bool check(const Ts &...x) override { return global_api_server->is_connected(); } bool check(Ts... x) override { return global_api_server->is_connected(); }
}; };
} // namespace esphome::api } // namespace esphome::api

View File

@@ -9,11 +9,11 @@
namespace esphome::api { namespace esphome::api {
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceDynamic<Ts...> { template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
public: public:
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj, CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
void (T::*callback)(Ts...)) void (T::*callback)(Ts...))
: UserServiceDynamic<Ts...>(name, arg_names), obj_(obj), callback_(callback) {} : UserServiceBase<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
protected: protected:
void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT

View File

@@ -133,7 +133,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; } Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
void play(const Ts &...x) override { void play(Ts... x) override {
HomeassistantActionRequest resp; HomeassistantActionRequest resp;
std::string service_value = this->service_.value(x...); std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value)); resp.set_service(StringRef(service_value));

View File

@@ -23,57 +23,11 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
template<typename T> enums::ServiceArgType to_service_arg_type(); template<typename T> enums::ServiceArgType to_service_arg_type();
// Base class for YAML-defined services (most common case)
// Stores only pointers to string literals in flash - no heap allocation
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor { template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
public: public:
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names) UserServiceBase(std::string name, const std::array<std::string, sizeof...(Ts)> &arg_names)
: name_(name), arg_names_(arg_names) {
this->key_ = fnv1_hash(name);
}
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg;
msg.set_name(StringRef(this->name_));
msg.key = this->key_;
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
msg.args.init(sizeof...(Ts));
for (size_t i = 0; i < sizeof...(Ts); i++) {
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.set_name(StringRef(this->arg_names_[i]));
}
return msg;
}
bool execute_service(const ExecuteServiceRequest &req) override {
if (req.key != this->key_)
return false;
if (req.args.size() != sizeof...(Ts))
return false;
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
return true;
}
protected:
virtual void execute(Ts... x) = 0;
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...);
}
// Pointers to string literals in flash - no heap allocation
const char *name_;
std::array<const char *, sizeof...(Ts)> arg_names_;
uint32_t key_{0};
};
// Separate class for custom_api_device services (rare case)
// Stores copies of runtime-generated names
template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor {
public:
UserServiceDynamic(std::string name, const std::array<std::string, sizeof...(Ts)> &arg_names)
: name_(std::move(name)), arg_names_(arg_names) { : name_(std::move(name)), arg_names_(arg_names) {
this->key_ = fnv1_hash(this->name_.c_str()); this->key_ = fnv1_hash(this->name_);
} }
ListEntitiesServicesResponse encode_list_service_response() override { ListEntitiesServicesResponse encode_list_service_response() override {
@@ -93,7 +47,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
bool execute_service(const ExecuteServiceRequest &req) override { bool execute_service(const ExecuteServiceRequest &req) override {
if (req.key != this->key_) if (req.key != this->key_)
return false; return false;
if (req.args.size() != sizeof...(Ts)) if (req.args.size() != this->arg_names_.size())
return false; return false;
this->execute_(req.args, typename gens<sizeof...(Ts)>::type()); this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
return true; return true;
@@ -105,16 +59,14 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
this->execute((get_execute_arg_value<Ts>(args[S]))...); this->execute((get_execute_arg_value<Ts>(args[S]))...);
} }
// Heap-allocated strings for runtime-generated names
std::string name_; std::string name_;
std::array<std::string, sizeof...(Ts)> arg_names_;
uint32_t key_{0}; uint32_t key_{0};
std::array<std::string, sizeof...(Ts)> arg_names_;
}; };
template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> { template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
public: public:
// Constructor for static names (YAML-defined services - used by code generator) UserServiceTrigger(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names)
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names) {} : UserServiceBase<Ts...>(name, arg_names) {}
protected: protected:

View File

@@ -10,7 +10,7 @@ namespace at581x {
template<typename... Ts> class AT581XResetAction : public Action<Ts...>, public Parented<AT581XComponent> { template<typename... Ts> class AT581XResetAction : public Action<Ts...>, public Parented<AT581XComponent> {
public: public:
void play(const Ts &...x) { this->parent_->reset_hardware_frontend(); } void play(Ts... x) { this->parent_->reset_hardware_frontend(); }
}; };
template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, public Parented<AT581XComponent> { template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, public Parented<AT581XComponent> {
@@ -25,7 +25,7 @@ template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, publ
TEMPLATABLE_VALUE(int, trigger_keep) TEMPLATABLE_VALUE(int, trigger_keep)
TEMPLATABLE_VALUE(int, stage_gain) TEMPLATABLE_VALUE(int, stage_gain)
void play(const Ts &...x) { void play(Ts... x) {
if (this->frequency_.has_value()) { if (this->frequency_.has_value()) {
int v = this->frequency_.value(x...); int v = this->frequency_.value(x...);
this->parent_->set_frequency(v); this->parent_->set_frequency(v);

View File

@@ -13,7 +13,7 @@ template<typename... Ts> class SetMicGainAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, mic_gain) TEMPLATABLE_VALUE(float, mic_gain)
void play(const Ts &...x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); } void play(Ts... x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); }
protected: protected:
AudioAdc *audio_adc_; AudioAdc *audio_adc_;

View File

@@ -11,7 +11,7 @@ template<typename... Ts> class MuteOffAction : public Action<Ts...> {
public: public:
explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
void play(const Ts &...x) override { this->audio_dac_->set_mute_off(); } void play(Ts... x) override { this->audio_dac_->set_mute_off(); }
protected: protected:
AudioDac *audio_dac_; AudioDac *audio_dac_;
@@ -21,7 +21,7 @@ template<typename... Ts> class MuteOnAction : public Action<Ts...> {
public: public:
explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
void play(const Ts &...x) override { this->audio_dac_->set_mute_on(); } void play(Ts... x) override { this->audio_dac_->set_mute_on(); }
protected: protected:
AudioDac *audio_dac_; AudioDac *audio_dac_;
@@ -33,7 +33,7 @@ template<typename... Ts> class SetVolumeAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, volume) TEMPLATABLE_VALUE(float, volume)
void play(const Ts &...x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); } void play(Ts... x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); }
protected: protected:
AudioDac *audio_dac_; AudioDac *audio_dac_;

View File

@@ -100,6 +100,7 @@ enum BedjetCommand : uint8_t {
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
} // namespace bedjet } // namespace bedjet
} // namespace esphome } // namespace esphome

View File

@@ -8,15 +8,15 @@ namespace bedjet {
using namespace esphome::climate; using namespace esphome::climate;
static const char *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
if (fan_step < BEDJET_FAN_SPEED_COUNT) if (fan_step < BEDJET_FAN_SPEED_COUNT)
return BEDJET_FAN_STEP_NAMES[fan_step]; return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step];
return nullptr; return nullptr;
} }
static uint8_t bedjet_fan_speed_to_step(const char *fan_step_percent) { static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) { for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) {
if (strcmp(BEDJET_FAN_STEP_NAMES[i], fan_step_percent) == 0) { if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) {
return i; return i;
} }
} }
@@ -48,7 +48,7 @@ void BedJetClimate::dump_config() {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
} }
for (const auto &mode : traits.get_supported_custom_fan_modes()) { for (const auto &mode : traits.get_supported_custom_fan_modes()) {
ESP_LOGCONFIG(TAG, " - %s (c)", mode); ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str());
} }
ESP_LOGCONFIG(TAG, " Supported presets:"); ESP_LOGCONFIG(TAG, " Supported presets:");
@@ -56,7 +56,7 @@ void BedJetClimate::dump_config() {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset))); ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
} }
for (const auto &preset : traits.get_supported_custom_presets()) { for (const auto &preset : traits.get_supported_custom_presets()) {
ESP_LOGCONFIG(TAG, " - %s (c)", preset); ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str());
} }
} }
@@ -79,7 +79,7 @@ void BedJetClimate::reset_state_() {
this->target_temperature = NAN; this->target_temperature = NAN;
this->current_temperature = NAN; this->current_temperature = NAN;
this->preset.reset(); this->preset.reset();
this->clear_custom_preset_(); this->custom_preset.reset();
this->publish_state(); this->publish_state();
} }
@@ -120,7 +120,7 @@ void BedJetClimate::control(const ClimateCall &call) {
if (button_result) { if (button_result) {
this->mode = mode; this->mode = mode;
// We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
this->clear_custom_preset_(); this->custom_preset.reset();
this->preset.reset(); this->preset.reset();
} }
} }
@@ -144,7 +144,8 @@ void BedJetClimate::control(const ClimateCall &call) {
if (result) { if (result) {
this->mode = CLIMATE_MODE_HEAT; this->mode = CLIMATE_MODE_HEAT;
this->set_preset_(CLIMATE_PRESET_BOOST); this->preset = CLIMATE_PRESET_BOOST;
this->custom_preset.reset();
} }
} else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) { } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) { if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
@@ -152,7 +153,7 @@ void BedJetClimate::control(const ClimateCall &call) {
result = this->parent_->send_button(heat_button(this->heating_mode_)); result = this->parent_->send_button(heat_button(this->heating_mode_));
if (result) { if (result) {
this->preset.reset(); this->preset.reset();
this->clear_custom_preset_(); this->custom_preset.reset();
} }
} else { } else {
ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'", ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
@@ -163,27 +164,28 @@ void BedJetClimate::control(const ClimateCall &call) {
ESP_LOGW(TAG, "Unsupported preset: %d", preset); ESP_LOGW(TAG, "Unsupported preset: %d", preset);
return; return;
} }
} else if (call.has_custom_preset()) { } else if (call.get_custom_preset().has_value()) {
const char *preset = call.get_custom_preset(); std::string preset = *call.get_custom_preset();
bool result; bool result;
if (strcmp(preset, "M1") == 0) { if (preset == "M1") {
result = this->parent_->button_memory1(); result = this->parent_->button_memory1();
} else if (strcmp(preset, "M2") == 0) { } else if (preset == "M2") {
result = this->parent_->button_memory2(); result = this->parent_->button_memory2();
} else if (strcmp(preset, "M3") == 0) { } else if (preset == "M3") {
result = this->parent_->button_memory3(); result = this->parent_->button_memory3();
} else if (strcmp(preset, "LTD HT") == 0) { } else if (preset == "LTD HT") {
result = this->parent_->button_heat(); result = this->parent_->button_heat();
} else if (strcmp(preset, "EXT HT") == 0) { } else if (preset == "EXT HT") {
result = this->parent_->button_ext_heat(); result = this->parent_->button_ext_heat();
} else { } else {
ESP_LOGW(TAG, "Unsupported preset: %s", preset); ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
return; return;
} }
if (result) { if (result) {
this->set_custom_preset_(preset); this->custom_preset = preset;
this->preset.reset();
} }
} }
@@ -205,16 +207,19 @@ void BedJetClimate::control(const ClimateCall &call) {
} }
if (result) { if (result) {
this->set_fan_mode_(fan_mode); this->fan_mode = fan_mode;
this->custom_fan_mode.reset();
} }
} else if (call.has_custom_fan_mode()) { } else if (call.get_custom_fan_mode().has_value()) {
const char *fan_mode = call.get_custom_fan_mode(); auto fan_mode = *call.get_custom_fan_mode();
auto fan_index = bedjet_fan_speed_to_step(fan_mode); auto fan_index = bedjet_fan_speed_to_step(fan_mode);
if (fan_index <= 19) { if (fan_index <= 19) {
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, fan_index); ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(),
fan_index);
bool result = this->parent_->set_fan_index(fan_index); bool result = this->parent_->set_fan_index(fan_index);
if (result) { if (result) {
this->set_custom_fan_mode_(fan_mode); this->custom_fan_mode = fan_mode;
this->fan_mode.reset();
} }
} }
} }
@@ -240,7 +245,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
if (fan_mode_name != nullptr) { if (fan_mode_name != nullptr) {
this->set_custom_fan_mode_(fan_mode_name); this->custom_fan_mode = *fan_mode_name;
} }
// TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
@@ -250,7 +255,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
this->mode = CLIMATE_MODE_OFF; this->mode = CLIMATE_MODE_OFF;
this->action = CLIMATE_ACTION_IDLE; this->action = CLIMATE_ACTION_IDLE;
this->fan_mode = CLIMATE_FAN_OFF; this->fan_mode = CLIMATE_FAN_OFF;
this->clear_custom_preset_(); this->custom_preset.reset();
this->preset.reset(); this->preset.reset();
break; break;
@@ -261,7 +266,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
if (this->heating_mode_ == HEAT_MODE_EXTENDED) { if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
this->set_custom_preset_("LTD HT"); this->set_custom_preset_("LTD HT");
} else { } else {
this->clear_custom_preset_(); this->custom_preset.reset();
} }
break; break;
@@ -270,7 +275,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
this->action = CLIMATE_ACTION_HEATING; this->action = CLIMATE_ACTION_HEATING;
this->preset.reset(); this->preset.reset();
if (this->heating_mode_ == HEAT_MODE_EXTENDED) { if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
this->clear_custom_preset_(); this->custom_preset.reset();
} else { } else {
this->set_custom_preset_("EXT HT"); this->set_custom_preset_("EXT HT");
} }
@@ -279,19 +284,20 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
case MODE_COOL: case MODE_COOL:
this->mode = CLIMATE_MODE_FAN_ONLY; this->mode = CLIMATE_MODE_FAN_ONLY;
this->action = CLIMATE_ACTION_COOLING; this->action = CLIMATE_ACTION_COOLING;
this->clear_custom_preset_(); this->custom_preset.reset();
this->preset.reset(); this->preset.reset();
break; break;
case MODE_DRY: case MODE_DRY:
this->mode = CLIMATE_MODE_DRY; this->mode = CLIMATE_MODE_DRY;
this->action = CLIMATE_ACTION_DRYING; this->action = CLIMATE_ACTION_DRYING;
this->clear_custom_preset_(); this->custom_preset.reset();
this->preset.reset(); this->preset.reset();
break; break;
case MODE_TURBO: case MODE_TURBO:
this->set_preset_(CLIMATE_PRESET_BOOST); this->preset = CLIMATE_PRESET_BOOST;
this->custom_preset.reset();
this->mode = CLIMATE_MODE_HEAT; this->mode = CLIMATE_MODE_HEAT;
this->action = CLIMATE_ACTION_HEATING; this->action = CLIMATE_ACTION_HEATING;
break; break;

View File

@@ -50,13 +50,21 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
// Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
climate::CLIMATE_PRESET_BOOST, climate::CLIMATE_PRESET_BOOST,
}); });
// String literals are stored in rodata and valid for program lifetime
traits.set_supported_custom_presets({ traits.set_supported_custom_presets({
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT", // We could fetch biodata from bedjet and set these names that way.
// But then we have to invert the lookup in order to send the right preset.
// For now, we can leave them as M1-3 to match the remote buttons.
// EXT HT added to match remote button.
"EXT HT",
"M1", "M1",
"M2", "M2",
"M3", "M3",
}); });
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
traits.add_supported_custom_preset("LTD HT");
} else {
traits.add_supported_custom_preset("EXT HT");
}
traits.set_visual_min_temperature(19.0); traits.set_visual_min_temperature(19.0);
traits.set_visual_max_temperature(43.0); traits.set_visual_max_temperature(43.0);
traits.set_visual_temperature_step(1.0); traits.set_visual_temperature_step(1.0);

View File

@@ -141,7 +141,7 @@ class StateChangeTrigger : public Trigger<optional<bool>, optional<bool> > {
template<typename... Ts> class BinarySensorCondition : public Condition<Ts...> { template<typename... Ts> class BinarySensorCondition : public Condition<Ts...> {
public: public:
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {} BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
bool check(const Ts &...x) override { return this->parent_->state == this->state_; } bool check(Ts... x) override { return this->parent_->state == this->state_; }
protected: protected:
BinarySensor *parent_; BinarySensor *parent_;
@@ -153,7 +153,7 @@ template<typename... Ts> class BinarySensorPublishAction : public Action<Ts...>
explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {} explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {}
TEMPLATABLE_VALUE(bool, state) TEMPLATABLE_VALUE(bool, state)
void play(const Ts &...x) override { void play(Ts... x) override {
auto val = this->state_.value(x...); auto val = this->state_.value(x...);
this->sensor_->publish_state(val); this->sensor_->publish_state(val);
} }
@@ -166,7 +166,7 @@ template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts..
public: public:
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {} explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
void play(const Ts &...x) override { this->sensor_->invalidate_state(); } void play(Ts... x) override { this->sensor_->invalidate_state(); }
protected: protected:
BinarySensor *sensor_; BinarySensor *sensor_;

View File

@@ -1,6 +1,4 @@
#include "binary_sensor.h" #include "binary_sensor.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
@@ -39,9 +37,6 @@ void BinarySensor::send_state_internal(bool new_state) {
// Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed // Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed
if (this->set_state_(new_state)) { if (this->set_state_(new_state)) {
ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state)); ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state));
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_binary_sensor_update(this);
#endif
} }
} }

View File

@@ -89,7 +89,7 @@ class BL0906 : public PollingComponent, public uart::UARTDevice {
template<typename... Ts> class ResetEnergyAction : public Action<Ts...>, public Parented<BL0906> { template<typename... Ts> class ResetEnergyAction : public Action<Ts...>, public Parented<BL0906> {
public: public:
void play(const Ts &...x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); } void play(Ts... x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); }
}; };
} // namespace bl0906 } // namespace bl0906

View File

@@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number";
void CalibrationNumber::setup() { void CalibrationNumber::setup() {
float value = 0.0f; float value = 0.0f;
if (this->restore_value_) { if (this->restore_value_) {
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash()); this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
if (!this->pref_.load(&value)) { if (!this->pref_.load(&value)) {
value = 0.0f; value = 0.0f;
} }

View File

@@ -15,7 +15,6 @@ from esphome.const import (
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_VALUE, CONF_VALUE,
) )
from esphome.core import ID
AUTO_LOAD = ["esp32_ble_client"] AUTO_LOAD = ["esp32_ble_client"]
CODEOWNERS = ["@buxtronix", "@clydebarrow"] CODEOWNERS = ["@buxtronix", "@clydebarrow"]
@@ -199,12 +198,7 @@ async def ble_write_to_code(config, action_id, template_arg, args):
templ = await cg.templatable(value, args, cg.std_vector.template(cg.uint8)) templ = await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
cg.add(var.set_value_template(templ)) cg.add(var.set_value_template(templ))
else: else:
# Generate static array in flash to avoid RAM copy cg.add(var.set_value_simple(value))
if isinstance(value, bytes):
value = list(value)
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*value))
cg.add(var.set_value_simple(arr, len(value)))
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
cg.add( cg.add(

View File

@@ -96,8 +96,11 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
BLEClientWriteAction(BLEClient *ble_client) { BLEClientWriteAction(BLEClient *ble_client) {
ble_client->register_ble_node(this); ble_client->register_ble_node(this);
ble_client_ = ble_client; ble_client_ = ble_client;
this->construct_simple_value_();
} }
~BLEClientWriteAction() { this->destroy_simple_value_(); }
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
@@ -107,29 +110,25 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
void set_value_template(std::vector<uint8_t> (*func)(Ts...)) { void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
this->value_.func = func; this->destroy_simple_value_();
this->len_ = -1; // Sentinel value indicates template mode this->value_.template_func = func;
this->has_simple_value_ = false;
} }
// Store pointer to static data in flash (no RAM copy) void set_value_simple(const std::vector<uint8_t> &value) {
void set_value_simple(const uint8_t *data, size_t len) { if (!this->has_simple_value_) {
this->value_.data = data; this->construct_simple_value_();
this->len_ = len; // Length >= 0 indicates static mode }
this->value_.simple = value;
this->has_simple_value_ = true;
} }
void play(const Ts &...x) override {} void play(Ts... x) override {}
void play_complex(const Ts &...x) override { void play_complex(Ts... x) override {
this->num_running_++; this->num_running_++;
this->var_ = std::make_tuple(x...); this->var_ = std::make_tuple(x...);
std::vector<uint8_t> value; auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
if (this->len_ >= 0) {
// Static mode: copy from flash to vector
value.assign(this->value_.data, this->value_.data + this->len_);
} else {
// Template mode: call function
value = this->value_.func(x...);
}
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
if (!write(value)) if (!write(value))
this->play_next_(x...); this->play_next_(x...);
@@ -202,11 +201,21 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
} }
private: private:
void construct_simple_value_() { new (&this->value_.simple) std::vector<uint8_t>(); }
void destroy_simple_value_() {
if (this->has_simple_value_) {
this->value_.simple.~vector();
}
}
BLEClient *ble_client_; BLEClient *ble_client_;
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length bool has_simple_value_ = true;
union Value { union Value {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas) std::vector<uint8_t> simple;
const uint8_t *data; // Pointer to static data in flash std::vector<uint8_t> (*template_func)(Ts...);
Value() {} // trivial constructor
~Value() {} // trivial destructor - we manage lifetime via discriminator
} value_; } value_;
espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_; espbt::ESPBTUUID char_uuid_;
@@ -220,7 +229,7 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
public: public:
BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; } BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(const Ts &...x) override { void play(Ts... x) override {
uint32_t passkey; uint32_t passkey;
if (has_simple_value_) { if (has_simple_value_) {
passkey = this->value_.simple; passkey = this->value_.simple;
@@ -257,7 +266,7 @@ template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Ac
public: public:
BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; } BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(const Ts &...x) override { void play(Ts... x) override {
esp_bd_addr_t remote_bda; esp_bd_addr_t remote_bda;
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
if (has_simple_value_) { if (has_simple_value_) {
@@ -290,7 +299,7 @@ template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...>
public: public:
BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; } BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(const Ts &...x) override { void play(Ts... x) override {
esp_bd_addr_t remote_bda; esp_bd_addr_t remote_bda;
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
esp_ble_remove_bond_device(remote_bda); esp_ble_remove_bond_device(remote_bda);
@@ -325,9 +334,9 @@ template<typename... Ts> class BLEClientConnectAction : public Action<Ts...>, pu
} }
// not used since we override play_complex_ // not used since we override play_complex_
void play(const Ts &...x) override {} void play(Ts... x) override {}
void play_complex(const Ts &...x) override { void play_complex(Ts... x) override {
// it makes no sense to have multiple instances of this running at the same time. // it makes no sense to have multiple instances of this running at the same time.
// this would occur only if the same automation was re-triggered while still // this would occur only if the same automation was re-triggered while still
// running. So just cancel the second chain if this is detected. // running. So just cancel the second chain if this is detected.
@@ -370,9 +379,9 @@ template<typename... Ts> class BLEClientDisconnectAction : public Action<Ts...>,
} }
// not used since we override play_complex_ // not used since we override play_complex_
void play(const Ts &...x) override {} void play(Ts... x) override {}
void play_complex(const Ts &...x) override { void play_complex(Ts... x) override {
this->num_running_++; this->num_running_++;
if (this->node_state == espbt::ClientState::IDLE) { if (this->node_state == espbt::ClientState::IDLE) {
this->play_next_(x...); this->play_next_(x...);

View File

@@ -77,9 +77,6 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
} }
} else { } else {
this->node_state = espbt::ClientState::ESTABLISHED; this->node_state = espbt::ClientState::ESTABLISHED;
// For non-notify characteristics, trigger an immediate read after service discovery
// to avoid peripherals disconnecting due to inactivity
this->update();
} }
break; break;
} }

View File

@@ -79,9 +79,6 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
} }
} else { } else {
this->node_state = espbt::ClientState::ESTABLISHED; this->node_state = espbt::ClientState::ESTABLISHED;
// For non-notify characteristics, trigger an immediate read after service discovery
// to avoid peripherals disconnecting due to inactivity
this->update();
} }
break; break;
} }

View File

@@ -11,7 +11,7 @@ template<typename... Ts> class PressAction : public Action<Ts...> {
public: public:
explicit PressAction(Button *button) : button_(button) {} explicit PressAction(Button *button) : button_(button) {}
void play(const Ts &...x) override { this->button_->press(); } void play(Ts... x) override { this->button_->press(); }
protected: protected:
Button *button_; Button *button_;

View File

@@ -4,7 +4,7 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID
from esphome.core import CORE, ID from esphome.core import CORE
CODEOWNERS = ["@mvturnho", "@danielschramm"] CODEOWNERS = ["@mvturnho", "@danielschramm"]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@@ -176,8 +176,5 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
else: else:
if isinstance(data, bytes): if isinstance(data, bytes):
data = [int(x) for x in data] data = [int(x) for x in data]
# Generate static array in flash to avoid RAM copy cg.add(var.set_data_static(data))
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
cg.add(var.set_data_static(arr, len(data)))
return var return var

View File

@@ -112,16 +112,13 @@ class Canbus : public Component {
template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public Parented<Canbus> { template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public Parented<Canbus> {
public: public:
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) { void set_data_template(const std::function<std::vector<uint8_t>(Ts...)> func) {
// Stateless lambdas (generated by ESPHome) implicitly convert to function pointers this->data_func_ = func;
this->data_.func = func; this->static_ = false;
this->len_ = -1; // Sentinel value indicates template mode
} }
void set_data_static(const std::vector<uint8_t> &data) {
// Store pointer to static data in flash (no RAM copy) this->data_static_ = data;
void set_data_static(const uint8_t *data, size_t len) { this->static_ = true;
this->data_.data = data;
this->len_ = len; // Length >= 0 indicates static mode
} }
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
@@ -132,30 +129,25 @@ template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public P
this->remote_transmission_request_ = remote_transmission_request; this->remote_transmission_request_ = remote_transmission_request;
} }
void play(const Ts &...x) override { void play(Ts... x) override {
auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
auto use_extended_id = auto use_extended_id =
this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;
std::vector<uint8_t> data; if (this->static_) {
if (this->len_ >= 0) { this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, this->data_static_);
// Static mode: copy from flash to vector
data.assign(this->data_.data, this->data_.data + this->len_);
} else { } else {
// Template mode: call function auto val = this->data_func_(x...);
data = this->data_.func(x...); this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, val);
} }
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data);
} }
protected: protected:
optional<uint32_t> can_id_{}; optional<uint32_t> can_id_{};
optional<bool> use_extended_id_{}; optional<bool> use_extended_id_{};
bool remote_transmission_request_{false}; bool remote_transmission_request_{false};
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length bool static_{false};
union Data { std::function<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas) std::vector<uint8_t> data_static_{};
const uint8_t *data; // Pointer to static data in flash
} data_;
}; };
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component { class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {

View File

@@ -22,7 +22,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, custom_preset) TEMPLATABLE_VALUE(std::string, custom_preset)
TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->climate_->make_call(); auto call = this->climate_->make_call();
call.set_mode(this->mode_.optional_value(x...)); call.set_mode(this->mode_.optional_value(x...));
call.set_target_temperature(this->target_temperature_.optional_value(x...)); call.set_target_temperature(this->target_temperature_.optional_value(x...));

View File

@@ -1,6 +1,4 @@
#include "climate.h" #include "climate.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/macros.h" #include "esphome/core/macros.h"
namespace esphome { namespace esphome {
@@ -52,21 +50,21 @@ void ClimateCall::perform() {
const LogString *mode_s = climate_mode_to_string(*this->mode_); const LogString *mode_s = climate_mode_to_string(*this->mode_);
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s));
} }
if (this->custom_fan_mode_ != nullptr) { if (this->custom_fan_mode_.has_value()) {
this->fan_mode_.reset(); this->fan_mode_.reset();
ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_); ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_.value().c_str());
} }
if (this->fan_mode_.has_value()) { if (this->fan_mode_.has_value()) {
this->custom_fan_mode_ = nullptr; this->custom_fan_mode_.reset();
const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_);
ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s));
} }
if (this->custom_preset_ != nullptr) { if (this->custom_preset_.has_value()) {
this->preset_.reset(); this->preset_.reset();
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_.value().c_str());
} }
if (this->preset_.has_value()) { if (this->preset_.has_value()) {
this->custom_preset_ = nullptr; this->custom_preset_.reset();
const LogString *preset_s = climate_preset_to_string(*this->preset_); const LogString *preset_s = climate_preset_to_string(*this->preset_);
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s));
} }
@@ -98,10 +96,11 @@ void ClimateCall::validate_() {
this->mode_.reset(); this->mode_.reset();
} }
} }
if (this->custom_fan_mode_ != nullptr) { if (this->custom_fan_mode_.has_value()) {
if (!traits.supports_custom_fan_mode(this->custom_fan_mode_)) { auto custom_fan_mode = *this->custom_fan_mode_;
ESP_LOGW(TAG, " Fan Mode %s not supported", this->custom_fan_mode_); if (!traits.supports_custom_fan_mode(custom_fan_mode)) {
this->custom_fan_mode_ = nullptr; ESP_LOGW(TAG, " Fan Mode %s not supported", custom_fan_mode.c_str());
this->custom_fan_mode_.reset();
} }
} else if (this->fan_mode_.has_value()) { } else if (this->fan_mode_.has_value()) {
auto fan_mode = *this->fan_mode_; auto fan_mode = *this->fan_mode_;
@@ -110,10 +109,11 @@ void ClimateCall::validate_() {
this->fan_mode_.reset(); this->fan_mode_.reset();
} }
} }
if (this->custom_preset_ != nullptr) { if (this->custom_preset_.has_value()) {
if (!traits.supports_custom_preset(this->custom_preset_)) { auto custom_preset = *this->custom_preset_;
ESP_LOGW(TAG, " Preset %s not supported", this->custom_preset_); if (!traits.supports_custom_preset(custom_preset)) {
this->custom_preset_ = nullptr; ESP_LOGW(TAG, " Preset %s not supported", custom_preset.c_str());
this->custom_preset_.reset();
} }
} else if (this->preset_.has_value()) { } else if (this->preset_.has_value()) {
auto preset = *this->preset_; auto preset = *this->preset_;
@@ -186,29 +186,26 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) {
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
this->fan_mode_ = fan_mode; this->fan_mode_ = fan_mode;
this->custom_fan_mode_ = nullptr; this->custom_fan_mode_.reset();
return *this; return *this;
} }
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
// Check if it's a standard enum mode first
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) { for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) { if (str_equals_case_insensitive(fan_mode, mode_entry.str)) {
return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value)); this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
return *this;
} }
} }
// Find the matching pointer from parent climate device if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) { this->custom_fan_mode_ = fan_mode;
this->custom_fan_mode_ = mode_ptr;
this->fan_mode_.reset(); this->fan_mode_.reset();
return *this; } else {
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
} }
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode);
return *this; return *this;
} }
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); }
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) { ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
if (fan_mode.has_value()) { if (fan_mode.has_value()) {
this->set_fan_mode(fan_mode.value()); this->set_fan_mode(fan_mode.value());
@@ -218,29 +215,26 @@ ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
ClimateCall &ClimateCall::set_preset(ClimatePreset preset) { ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
this->preset_ = preset; this->preset_ = preset;
this->custom_preset_ = nullptr; this->custom_preset_.reset();
return *this; return *this;
} }
ClimateCall &ClimateCall::set_preset(const char *custom_preset) { ClimateCall &ClimateCall::set_preset(const std::string &preset) {
// Check if it's a standard enum preset first
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
if (str_equals_case_insensitive(custom_preset, preset_entry.str)) { if (str_equals_case_insensitive(preset, preset_entry.str)) {
return this->set_preset(static_cast<ClimatePreset>(preset_entry.value)); this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
return *this;
} }
} }
// Find the matching pointer from parent climate device if (this->parent_->get_traits().supports_custom_preset(preset)) {
if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) { this->custom_preset_ = preset;
this->custom_preset_ = preset_ptr;
this->preset_.reset(); this->preset_.reset();
return *this; } else {
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
} }
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset);
return *this; return *this;
} }
ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); }
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) { ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
if (preset.has_value()) { if (preset.has_value()) {
this->set_preset(preset.value()); this->set_preset(preset.value());
@@ -293,6 +287,8 @@ const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_;
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; } const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; } const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) { ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
this->target_temperature_high_ = target_temperature_high; this->target_temperature_high_ = target_temperature_high;
@@ -321,13 +317,13 @@ ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) { ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
this->fan_mode_ = fan_mode; this->fan_mode_ = fan_mode;
this->custom_fan_mode_ = nullptr; this->custom_fan_mode_.reset();
return *this; return *this;
} }
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) { ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
this->preset_ = preset; this->preset_ = preset;
this->custom_preset_ = nullptr; this->custom_preset_.reset();
return *this; return *this;
} }
@@ -386,13 +382,13 @@ void Climate::save_state_() {
state.uses_custom_fan_mode = false; state.uses_custom_fan_mode = false;
state.fan_mode = this->fan_mode.value(); state.fan_mode = this->fan_mode.value();
} }
if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
state.uses_custom_fan_mode = true; state.uses_custom_fan_mode = true;
const auto &supported = traits.get_supported_custom_fan_modes(); const auto &supported = traits.get_supported_custom_fan_modes();
// std::vector maintains insertion order // std::vector maintains insertion order
size_t i = 0; size_t i = 0;
for (const char *mode : supported) { for (const auto &mode : supported) {
if (strcmp(mode, this->custom_fan_mode_) == 0) { if (mode == custom_fan_mode) {
state.custom_fan_mode = i; state.custom_fan_mode = i;
break; break;
} }
@@ -403,13 +399,13 @@ void Climate::save_state_() {
state.uses_custom_preset = false; state.uses_custom_preset = false;
state.preset = this->preset.value(); state.preset = this->preset.value();
} }
if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
state.uses_custom_preset = true; state.uses_custom_preset = true;
const auto &supported = traits.get_supported_custom_presets(); const auto &supported = traits.get_supported_custom_presets();
// std::vector maintains insertion order // std::vector maintains insertion order
size_t i = 0; size_t i = 0;
for (const char *preset : supported) { for (const auto &preset : supported) {
if (strcmp(preset, this->custom_preset_) == 0) { if (preset == custom_preset) {
state.custom_preset = i; state.custom_preset = i;
break; break;
} }
@@ -434,14 +430,14 @@ void Climate::publish_state() {
if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value())));
} }
if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode.has_value()) {
ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode.value().c_str());
} }
if (traits.get_supports_presets() && this->preset.has_value()) { if (traits.get_supports_presets() && this->preset.has_value()) {
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value())));
} }
if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { if (!traits.get_supported_custom_presets().empty() && this->custom_preset.has_value()) {
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset.value().c_str());
} }
if (traits.get_supports_swing_modes()) { if (traits.get_supports_swing_modes()) {
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
@@ -465,9 +461,6 @@ void Climate::publish_state() {
// Send state to frontend // Send state to frontend
this->state_callback_.call(*this); this->state_callback_.call(*this);
#if defined(USE_CLIMATE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_climate_update(this);
#endif
// Save state // Save state
this->save_state_(); this->save_state_();
} }
@@ -534,7 +527,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
if (this->uses_custom_fan_mode) { if (this->uses_custom_fan_mode) {
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
call.fan_mode_.reset(); call.fan_mode_.reset();
call.custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; call.custom_fan_mode_ = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
} }
} else if (traits.supports_fan_mode(this->fan_mode)) { } else if (traits.supports_fan_mode(this->fan_mode)) {
call.set_fan_mode(this->fan_mode); call.set_fan_mode(this->fan_mode);
@@ -542,7 +535,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
if (this->uses_custom_preset) { if (this->uses_custom_preset) {
if (this->custom_preset < traits.get_supported_custom_presets().size()) { if (this->custom_preset < traits.get_supported_custom_presets().size()) {
call.preset_.reset(); call.preset_.reset();
call.custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; call.custom_preset_ = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
} }
} else if (traits.supports_preset(this->preset)) { } else if (traits.supports_preset(this->preset)) {
call.set_preset(this->preset); call.set_preset(this->preset);
@@ -569,20 +562,20 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
if (this->uses_custom_fan_mode) { if (this->uses_custom_fan_mode) {
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
climate->fan_mode.reset(); climate->fan_mode.reset();
climate->custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; climate->custom_fan_mode = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
} }
} else if (traits.supports_fan_mode(this->fan_mode)) { } else if (traits.supports_fan_mode(this->fan_mode)) {
climate->fan_mode = this->fan_mode; climate->fan_mode = this->fan_mode;
climate->clear_custom_fan_mode_(); climate->custom_fan_mode.reset();
} }
if (this->uses_custom_preset) { if (this->uses_custom_preset) {
if (this->custom_preset < traits.get_supported_custom_presets().size()) { if (this->custom_preset < traits.get_supported_custom_presets().size()) {
climate->preset.reset(); climate->preset.reset();
climate->custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; climate->custom_preset = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
} }
} else if (traits.supports_preset(this->preset)) { } else if (traits.supports_preset(this->preset)) {
climate->preset = this->preset; climate->preset = this->preset;
climate->clear_custom_preset_(); climate->custom_preset.reset();
} }
if (traits.supports_swing_mode(this->swing_mode)) { if (traits.supports_swing_mode(this->swing_mode)) {
climate->swing_mode = this->swing_mode; climate->swing_mode = this->swing_mode;
@@ -590,107 +583,28 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
climate->publish_state(); climate->publish_state();
} }
/** Template helper for setting primary modes (fan_mode, preset) with mutual exclusion. template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &alt, const T1 &src) {
* bool is_changed = alt.has_value();
* Climate devices have mutually exclusive mode pairs: alt.reset();
* - fan_mode (enum) vs custom_fan_mode_ (const char*) if (is_changed || dst != src) {
* - preset (enum) vs custom_preset_ (const char*) dst = src;
* is_changed = true;
* Only one mode in each pair can be active at a time. This helper ensures setting a primary
* mode automatically clears its corresponding custom mode.
*
* Example state transitions:
* Before: custom_fan_mode_="Turbo", fan_mode=nullopt
* Call: set_fan_mode_(CLIMATE_FAN_HIGH)
* After: custom_fan_mode_=nullptr, fan_mode=CLIMATE_FAN_HIGH
*
* @param primary The primary mode optional (fan_mode or preset)
* @param custom_ptr Reference to the custom mode pointer (custom_fan_mode_ or custom_preset_)
* @param value The new primary mode value to set
* @return true if state changed, false if already set to this value
*/
template<typename T> bool set_primary_mode(optional<T> &primary, const char *&custom_ptr, T value) {
// Clear the custom mode (mutual exclusion)
bool changed = custom_ptr != nullptr;
custom_ptr = nullptr;
// Set the primary mode
if (changed || !primary.has_value() || primary.value() != value) {
primary = value;
return true;
} }
return false; return is_changed;
}
/** Template helper for setting custom modes (custom_fan_mode_, custom_preset_) with mutual exclusion.
*
* This helper ensures setting a custom mode automatically clears its corresponding primary mode.
* It also validates that the custom mode exists in the device's supported modes (lifetime safety).
*
* Example state transitions:
* Before: fan_mode=CLIMATE_FAN_HIGH, custom_fan_mode_=nullptr
* Call: set_custom_fan_mode_("Turbo")
* After: fan_mode=nullopt, custom_fan_mode_="Turbo" (pointer from traits)
*
* Lifetime Safety:
* - found_ptr must come from traits.find_custom_*_mode_()
* - Only pointers found in traits are stored, ensuring they remain valid
* - Prevents dangling pointers from temporary strings
*
* @param custom_ptr Reference to the custom mode pointer to set
* @param primary The primary mode optional to clear
* @param found_ptr The validated pointer from traits (nullptr if not found)
* @param has_custom Whether a custom mode is currently active
* @return true if state changed, false otherwise
*/
template<typename T>
bool set_custom_mode(const char *&custom_ptr, optional<T> &primary, const char *found_ptr, bool has_custom) {
if (found_ptr != nullptr) {
// Clear the primary mode (mutual exclusion)
bool changed = primary.has_value();
primary.reset();
// Set the custom mode (pointer is validated by caller from traits)
if (changed || custom_ptr != found_ptr) {
custom_ptr = found_ptr;
return true;
}
return false;
}
// Mode not found in supported modes, clear it if currently set
if (has_custom) {
custom_ptr = nullptr;
return true;
}
return false;
} }
bool Climate::set_fan_mode_(ClimateFanMode mode) { bool Climate::set_fan_mode_(ClimateFanMode mode) {
return set_primary_mode(this->fan_mode, this->custom_fan_mode_, mode); return set_alternative(this->fan_mode, this->custom_fan_mode, mode);
} }
bool Climate::set_custom_fan_mode_(const char *mode) { bool Climate::set_custom_fan_mode_(const std::string &mode) {
auto traits = this->get_traits(); return set_alternative(this->custom_fan_mode, this->fan_mode, mode);
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode),
this->has_custom_fan_mode());
} }
void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); }
bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); } bool Climate::set_custom_preset_(const std::string &preset) {
return set_alternative(this->custom_preset, this->preset, preset);
bool Climate::set_custom_preset_(const char *preset) {
auto traits = this->get_traits();
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset),
this->has_custom_preset());
}
void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; }
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) {
return this->get_traits().find_custom_fan_mode_(custom_fan_mode);
}
const char *Climate::find_custom_preset_(const char *custom_preset) {
return this->get_traits().find_custom_preset_(custom_preset);
} }
void Climate::dump_traits_(const char *tag) { void Climate::dump_traits_(const char *tag) {
@@ -742,8 +656,8 @@ void Climate::dump_traits_(const char *tag) {
} }
if (!traits.get_supported_custom_fan_modes().empty()) { if (!traits.get_supported_custom_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported custom fan modes:"); ESP_LOGCONFIG(tag, " Supported custom fan modes:");
for (const char *s : traits.get_supported_custom_fan_modes()) for (const std::string &s : traits.get_supported_custom_fan_modes())
ESP_LOGCONFIG(tag, " - %s", s); ESP_LOGCONFIG(tag, " - %s", s.c_str());
} }
if (!traits.get_supported_presets().empty()) { if (!traits.get_supported_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported presets:"); ESP_LOGCONFIG(tag, " Supported presets:");
@@ -752,8 +666,8 @@ void Climate::dump_traits_(const char *tag) {
} }
if (!traits.get_supported_custom_presets().empty()) { if (!traits.get_supported_custom_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported custom presets:"); ESP_LOGCONFIG(tag, " Supported custom presets:");
for (const char *s : traits.get_supported_custom_presets()) for (const std::string &s : traits.get_supported_custom_presets())
ESP_LOGCONFIG(tag, " - %s", s); ESP_LOGCONFIG(tag, " - %s", s.c_str());
} }
if (!traits.get_supported_swing_modes().empty()) { if (!traits.get_supported_swing_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported swing modes:"); ESP_LOGCONFIG(tag, " Supported swing modes:");

View File

@@ -77,8 +77,6 @@ class ClimateCall {
ClimateCall &set_fan_mode(const std::string &fan_mode); ClimateCall &set_fan_mode(const std::string &fan_mode);
/// Set the fan mode of the climate device based on a string. /// Set the fan mode of the climate device based on a string.
ClimateCall &set_fan_mode(optional<std::string> fan_mode); ClimateCall &set_fan_mode(optional<std::string> fan_mode);
/// Set the custom fan mode of the climate device.
ClimateCall &set_fan_mode(const char *custom_fan_mode);
/// Set the swing mode of the climate device. /// Set the swing mode of the climate device.
ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
/// Set the swing mode of the climate device. /// Set the swing mode of the climate device.
@@ -93,8 +91,6 @@ class ClimateCall {
ClimateCall &set_preset(const std::string &preset); ClimateCall &set_preset(const std::string &preset);
/// Set the preset of the climate device based on a string. /// Set the preset of the climate device based on a string.
ClimateCall &set_preset(optional<std::string> preset); ClimateCall &set_preset(optional<std::string> preset);
/// Set the custom preset of the climate device.
ClimateCall &set_preset(const char *custom_preset);
void perform(); void perform();
@@ -107,10 +103,8 @@ class ClimateCall {
const optional<ClimateFanMode> &get_fan_mode() const; const optional<ClimateFanMode> &get_fan_mode() const;
const optional<ClimateSwingMode> &get_swing_mode() const; const optional<ClimateSwingMode> &get_swing_mode() const;
const optional<ClimatePreset> &get_preset() const; const optional<ClimatePreset> &get_preset() const;
const char *get_custom_fan_mode() const { return this->custom_fan_mode_; } const optional<std::string> &get_custom_fan_mode() const;
const char *get_custom_preset() const { return this->custom_preset_; } const optional<std::string> &get_custom_preset() const;
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
bool has_custom_preset() const { return this->custom_preset_ != nullptr; }
protected: protected:
void validate_(); void validate_();
@@ -124,10 +118,8 @@ class ClimateCall {
optional<ClimateFanMode> fan_mode_; optional<ClimateFanMode> fan_mode_;
optional<ClimateSwingMode> swing_mode_; optional<ClimateSwingMode> swing_mode_;
optional<ClimatePreset> preset_; optional<ClimatePreset> preset_;
optional<std::string> custom_fan_mode_;
private: optional<std::string> custom_preset_;
const char *custom_fan_mode_{nullptr};
const char *custom_preset_{nullptr};
}; };
/// Struct used to save the state of the climate device in restore memory. /// Struct used to save the state of the climate device in restore memory.
@@ -220,12 +212,6 @@ class Climate : public EntityBase {
void set_visual_min_humidity_override(float visual_min_humidity_override); void set_visual_min_humidity_override(float visual_min_humidity_override);
void set_visual_max_humidity_override(float visual_max_humidity_override); void set_visual_max_humidity_override(float visual_max_humidity_override);
/// Check if a custom fan mode is currently active.
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
/// Check if a custom preset is currently active.
bool has_custom_preset() const { return this->custom_preset_ != nullptr; }
/// The current temperature of the climate device, as reported from the integration. /// The current temperature of the climate device, as reported from the integration.
float current_temperature{NAN}; float current_temperature{NAN};
@@ -252,6 +238,12 @@ class Climate : public EntityBase {
/// The active preset of the climate device. /// The active preset of the climate device.
optional<ClimatePreset> preset; optional<ClimatePreset> preset;
/// The active custom fan mode of the climate device.
optional<std::string> custom_fan_mode;
/// The active custom preset mode of the climate device.
optional<std::string> custom_preset;
/// The active mode of the climate device. /// The active mode of the climate device.
ClimateMode mode{CLIMATE_MODE_OFF}; ClimateMode mode{CLIMATE_MODE_OFF};
@@ -261,37 +253,20 @@ class Climate : public EntityBase {
/// The active swing mode of the climate device. /// The active swing mode of the climate device.
ClimateSwingMode swing_mode{CLIMATE_SWING_OFF}; ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};
/// Get the active custom fan mode (read-only access).
const char *get_custom_fan_mode() const { return this->custom_fan_mode_; }
/// Get the active custom preset (read-only access).
const char *get_custom_preset() const { return this->custom_preset_; }
protected: protected:
friend ClimateCall; friend ClimateCall;
friend struct ClimateDeviceRestoreState;
/// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
bool set_fan_mode_(ClimateFanMode mode); bool set_fan_mode_(ClimateFanMode mode);
/// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
bool set_custom_fan_mode_(const char *mode); bool set_custom_fan_mode_(const std::string &mode);
/// Clear custom fan mode.
void clear_custom_fan_mode_();
/// Set preset. Reset custom preset. Return true if preset has been changed. /// Set preset. Reset custom preset. Return true if preset has been changed.
bool set_preset_(ClimatePreset preset); bool set_preset_(ClimatePreset preset);
/// Set custom preset. Reset primary preset. Return true if preset has been changed. /// Set custom preset. Reset primary preset. Return true if preset has been changed.
bool set_custom_preset_(const char *preset); bool set_custom_preset_(const std::string &preset);
/// Clear custom preset.
void clear_custom_preset_();
/// Find and return the matching custom fan mode pointer from traits, or nullptr if not found.
const char *find_custom_fan_mode_(const char *custom_fan_mode);
/// Find and return the matching custom preset pointer from traits, or nullptr if not found.
const char *find_custom_preset_(const char *custom_preset);
/** Get the default traits of this climate device. /** Get the default traits of this climate device.
* *
@@ -328,21 +303,6 @@ class Climate : public EntityBase {
optional<float> visual_current_temperature_step_override_{}; optional<float> visual_current_temperature_step_override_{};
optional<float> visual_min_humidity_override_{}; optional<float> visual_min_humidity_override_{};
optional<float> visual_max_humidity_override_{}; optional<float> visual_max_humidity_override_{};
private:
/** The active custom fan mode (private - enforces use of safe setters).
*
* Points to an entry in traits.supported_custom_fan_modes_ or nullptr.
* Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify.
*/
const char *custom_fan_mode_{nullptr};
/** The active custom preset (private - enforces use of safe setters).
*
* Points to an entry in traits.supported_custom_presets_ or nullptr.
* Use get_custom_preset() to read, set_custom_preset_() to modify.
*/
const char *custom_preset_{nullptr};
}; };
} // namespace climate } // namespace climate

View File

@@ -1,6 +1,5 @@
#pragma once #pragma once
#include <cstring>
#include <vector> #include <vector>
#include "climate_mode.h" #include "climate_mode.h"
#include "esphome/core/finite_set_mask.h" #include "esphome/core/finite_set_mask.h"
@@ -19,25 +18,16 @@ using ClimateSwingModeMask =
FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>; FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>;
using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>; using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>;
// Lightweight linear search for small vectors (1-20 items) of const char* pointers // Lightweight linear search for small vectors (1-20 items)
// Avoids std::find template overhead // Avoids std::find template overhead
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) { template<typename T> inline bool vector_contains(const std::vector<T> &vec, const T &value) {
for (const char *item : vec) { for (const auto &item : vec) {
if (strcmp(item, value) == 0) if (item == value)
return true; return true;
} }
return false; return false;
} }
// Find and return matching pointer from vector, or nullptr if not found
inline const char *vector_find(const std::vector<const char *> &vec, const char *value) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
return item;
}
return nullptr;
}
/** This class contains all static data for climate devices. /** This class contains all static data for climate devices.
* *
* All climate devices must support these features: * All climate devices must support these features:
@@ -65,11 +55,7 @@ inline const char *vector_find(const std::vector<const char *> &vec, const char
* - temperature step - the step with which to increase/decrease target temperature. * - temperature step - the step with which to increase/decrease target temperature.
* This also affects with how many decimal places the temperature is shown * This also affects with how many decimal places the temperature is shown
*/ */
class Climate; // Forward declaration
class ClimateTraits { class ClimateTraits {
friend class Climate; // Allow Climate to access protected find methods
public: public:
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h) /// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
uint32_t get_feature_flags() const { return this->feature_flags_; } uint32_t get_feature_flags() const { return this->feature_flags_; }
@@ -142,60 +128,46 @@ class ClimateTraits {
void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; }
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); }
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const { bool get_supports_fan_modes() const {
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
} }
const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) { void set_supported_custom_fan_modes(std::vector<std::string> supported_custom_fan_modes) {
this->supported_custom_fan_modes_ = modes; this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
} }
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) { void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) {
this->supported_custom_fan_modes_ = modes; this->supported_custom_fan_modes_ = modes;
} }
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) { template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->supported_custom_fan_modes_.assign(modes, modes + N); this->supported_custom_fan_modes_.assign(modes, modes + N);
} }
const std::vector<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_custom_fan_modes(const std::vector<std::string> &modes) = delete;
void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) = delete;
const std::vector<const char *> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
bool supports_custom_fan_mode(const char *custom_fan_mode) const {
return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
}
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
return this->supports_custom_fan_mode(custom_fan_mode.c_str()); return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
} }
void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); }
bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
bool get_supports_presets() const { return !this->supported_presets_.empty(); } bool get_supports_presets() const { return !this->supported_presets_.empty(); }
const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
void set_supported_custom_presets(std::initializer_list<const char *> presets) { void set_supported_custom_presets(std::vector<std::string> supported_custom_presets) {
this->supported_custom_presets_ = presets; this->supported_custom_presets_ = std::move(supported_custom_presets);
} }
void set_supported_custom_presets(const std::vector<const char *> &presets) { void set_supported_custom_presets(std::initializer_list<std::string> presets) {
this->supported_custom_presets_ = presets; this->supported_custom_presets_ = presets;
} }
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) { template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
this->supported_custom_presets_.assign(presets, presets + N); this->supported_custom_presets_.assign(presets, presets + N);
} }
const std::vector<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_custom_presets(const std::vector<std::string> &presets) = delete;
void set_supported_custom_presets(std::initializer_list<std::string> presets) = delete;
const std::vector<const char *> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
bool supports_custom_preset(const char *custom_preset) const {
return vector_contains(this->supported_custom_presets_, custom_preset);
}
bool supports_custom_preset(const std::string &custom_preset) const { bool supports_custom_preset(const std::string &custom_preset) const {
return this->supports_custom_preset(custom_preset.c_str()); return vector_contains(this->supported_custom_presets_, custom_preset);
} }
void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
@@ -255,18 +227,6 @@ class ClimateTraits {
} }
} }
/// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead
const char *find_custom_fan_mode_(const char *custom_fan_mode) const {
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode);
}
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead
const char *find_custom_preset_(const char *custom_preset) const {
return vector_find(this->supported_custom_presets_, custom_preset);
}
uint32_t feature_flags_{0}; uint32_t feature_flags_{0};
float visual_min_temperature_{10}; float visual_min_temperature_{10};
float visual_max_temperature_{30}; float visual_max_temperature_{30};
@@ -279,17 +239,8 @@ class ClimateTraits {
climate::ClimateFanModeMask supported_fan_modes_; climate::ClimateFanModeMask supported_fan_modes_;
climate::ClimateSwingModeMask supported_swing_modes_; climate::ClimateSwingModeMask supported_swing_modes_;
climate::ClimatePresetMask supported_presets_; climate::ClimatePresetMask supported_presets_;
std::vector<std::string> supported_custom_fan_modes_;
/** Custom mode storage using const char* pointers to eliminate std::string overhead. std::vector<std::string> supported_custom_presets_;
*
* Pointers must remain valid for the ClimateTraits lifetime. Safe patterns:
* - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"})
* - Static const data: static const char* MODE = "Eco";
*
* Climate class setters validate pointers are from these vectors before storing.
*/
std::vector<const char *> supported_custom_fan_modes_;
std::vector<const char *> supported_custom_presets_;
}; };
} // namespace climate } // namespace climate

View File

@@ -30,7 +30,7 @@ template<typename... Ts> class CM1106CalibrateZeroAction : public Action<Ts...>
public: public:
CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {} CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {}
void play(const Ts &...x) override { this->cm1106_->calibrate_zero(400); } void play(Ts... x) override { this->cm1106_->calibrate_zero(400); }
protected: protected:
CM1106Component *cm1106_; CM1106Component *cm1106_;

View File

@@ -8,10 +8,7 @@ BYTE_ORDER_BIG = "big_endian"
CONF_COLOR_DEPTH = "color_depth" CONF_COLOR_DEPTH = "color_depth"
CONF_DRAW_ROUNDING = "draw_rounding" CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLED = "enabled"
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_ON_RECEIVE = "on_receive" CONF_ON_RECEIVE = "on_receive"
CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers" CONF_REQUEST_HEADERS = "request_headers"
CONF_ROWS = "rows"
CONF_USE_PSRAM = "use_psram" CONF_USE_PSRAM = "use_psram"

View File

@@ -12,7 +12,7 @@ void CopyFan::setup() {
this->oscillating = source_->oscillating; this->oscillating = source_->oscillating;
this->speed = source_->speed; this->speed = source_->speed;
this->direction = source_->direction; this->direction = source_->direction;
this->set_preset_mode_(source_->get_preset_mode()); this->preset_mode = source_->preset_mode;
this->publish_state(); this->publish_state();
}); });
@@ -20,7 +20,7 @@ void CopyFan::setup() {
this->oscillating = source_->oscillating; this->oscillating = source_->oscillating;
this->speed = source_->speed; this->speed = source_->speed;
this->direction = source_->direction; this->direction = source_->direction;
this->set_preset_mode_(source_->get_preset_mode()); this->preset_mode = source_->preset_mode;
this->publish_state(); this->publish_state();
} }
@@ -49,7 +49,7 @@ void CopyFan::control(const fan::FanCall &call) {
call2.set_speed(*call.get_speed()); call2.set_speed(*call.get_speed());
if (call.get_direction().has_value()) if (call.get_direction().has_value())
call2.set_direction(*call.get_direction()); call2.set_direction(*call.get_direction());
if (call.has_preset_mode()) if (!call.get_preset_mode().empty())
call2.set_preset_mode(call.get_preset_mode()); call2.set_preset_mode(call.get_preset_mode());
call2.perform(); call2.perform();
} }

View File

@@ -7,19 +7,19 @@ namespace copy {
static const char *const TAG = "copy.select"; static const char *const TAG = "copy.select";
void CopySelect::setup() { void CopySelect::setup() {
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(index); }); source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
traits.set_options(source_->traits.get_options()); traits.set_options(source_->traits.get_options());
if (source_->has_state()) if (source_->has_state())
this->publish_state(source_->active_index().value()); this->publish_state(source_->state);
} }
void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); }
void CopySelect::control(size_t index) { void CopySelect::control(const std::string &value) {
auto call = source_->make_call(); auto call = source_->make_call();
call.set_index(index); call.set_option(value);
call.perform(); call.perform();
} }

View File

@@ -13,7 +13,7 @@ class CopySelect : public select::Select, public Component {
void dump_config() override; void dump_config() override;
protected: protected:
void control(size_t index) override; void control(const std::string &value) override;
select::Select *source_; select::Select *source_;
}; };

View File

@@ -11,7 +11,7 @@ template<typename... Ts> class OpenAction : public Action<Ts...> {
public: public:
explicit OpenAction(Cover *cover) : cover_(cover) {} explicit OpenAction(Cover *cover) : cover_(cover) {}
void play(const Ts &...x) override { this->cover_->make_call().set_command_open().perform(); } void play(Ts... x) override { this->cover_->make_call().set_command_open().perform(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -21,7 +21,7 @@ template<typename... Ts> class CloseAction : public Action<Ts...> {
public: public:
explicit CloseAction(Cover *cover) : cover_(cover) {} explicit CloseAction(Cover *cover) : cover_(cover) {}
void play(const Ts &...x) override { this->cover_->make_call().set_command_close().perform(); } void play(Ts... x) override { this->cover_->make_call().set_command_close().perform(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -31,7 +31,7 @@ template<typename... Ts> class StopAction : public Action<Ts...> {
public: public:
explicit StopAction(Cover *cover) : cover_(cover) {} explicit StopAction(Cover *cover) : cover_(cover) {}
void play(const Ts &...x) override { this->cover_->make_call().set_command_stop().perform(); } void play(Ts... x) override { this->cover_->make_call().set_command_stop().perform(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -41,7 +41,7 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> {
public: public:
explicit ToggleAction(Cover *cover) : cover_(cover) {} explicit ToggleAction(Cover *cover) : cover_(cover) {}
void play(const Ts &...x) override { this->cover_->make_call().set_command_toggle().perform(); } void play(Ts... x) override { this->cover_->make_call().set_command_toggle().perform(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -55,7 +55,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, position) TEMPLATABLE_VALUE(float, position)
TEMPLATABLE_VALUE(float, tilt) TEMPLATABLE_VALUE(float, tilt)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->cover_->make_call(); auto call = this->cover_->make_call();
if (this->stop_.has_value()) if (this->stop_.has_value())
call.set_stop(this->stop_.value(x...)); call.set_stop(this->stop_.value(x...));
@@ -77,7 +77,7 @@ template<typename... Ts> class CoverPublishAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, tilt) TEMPLATABLE_VALUE(float, tilt)
TEMPLATABLE_VALUE(CoverOperation, current_operation) TEMPLATABLE_VALUE(CoverOperation, current_operation)
void play(const Ts &...x) override { void play(Ts... x) override {
if (this->position_.has_value()) if (this->position_.has_value())
this->cover_->position = this->position_.value(x...); this->cover_->position = this->position_.value(x...);
if (this->tilt_.has_value()) if (this->tilt_.has_value())
@@ -94,7 +94,7 @@ template<typename... Ts> class CoverPublishAction : public Action<Ts...> {
template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> { template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> {
public: public:
CoverIsOpenCondition(Cover *cover) : cover_(cover) {} CoverIsOpenCondition(Cover *cover) : cover_(cover) {}
bool check(const Ts &...x) override { return this->cover_->is_fully_open(); } bool check(Ts... x) override { return this->cover_->is_fully_open(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -103,7 +103,7 @@ template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> {
template<typename... Ts> class CoverIsClosedCondition : public Condition<Ts...> { template<typename... Ts> class CoverIsClosedCondition : public Condition<Ts...> {
public: public:
CoverIsClosedCondition(Cover *cover) : cover_(cover) {} CoverIsClosedCondition(Cover *cover) : cover_(cover) {}
bool check(const Ts &...x) override { return this->cover_->is_fully_closed(); } bool check(Ts... x) override { return this->cover_->is_fully_closed(); }
protected: protected:
Cover *cover_; Cover *cover_;

View File

@@ -1,9 +1,5 @@
#include "cover.h" #include "cover.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include <strings.h> #include <strings.h>
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
@@ -173,9 +169,6 @@ void Cover::publish_state(bool save) {
ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation)); ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation));
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_cover_update(this);
#endif
if (save) { if (save) {
CoverRestoreState restore{}; CoverRestoreState restore{};

View File

@@ -114,7 +114,7 @@ template<typename... Ts> class CS5460ARestartAction : public Action<Ts...> {
public: public:
CS5460ARestartAction(CS5460AComponent *cs5460a) : cs5460a_(cs5460a) {} CS5460ARestartAction(CS5460AComponent *cs5460a) : cs5460a_(cs5460a) {}
void play(const Ts &...x) override { cs5460a_->restart(); } void play(Ts... x) override { cs5460a_->restart(); }
protected: protected:
CS5460AComponent *cs5460a_; CS5460AComponent *cs5460a_;

View File

@@ -70,7 +70,7 @@ bool DallasTemperatureSensor::read_scratch_pad_() {
} }
void DallasTemperatureSensor::setup() { void DallasTemperatureSensor::setup() {
if (!this->check_address_or_index_()) if (!this->check_address_())
return; return;
if (!this->read_scratch_pad_()) if (!this->read_scratch_pad_())
return; return;

View File

@@ -1,6 +1,5 @@
#include "date_entity.h" #include "date_entity.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -33,9 +32,6 @@ void DateEntity::publish_state() {
this->set_has_state(true); this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_date_update(this);
#endif
} }
DateCall DateEntity::make_call() { return DateCall(this); } DateCall DateEntity::make_call() { return DateCall(this); }

View File

@@ -101,7 +101,7 @@ template<typename... Ts> class DateSetAction : public Action<Ts...>, public Pare
public: public:
TEMPLATABLE_VALUE(ESPTime, date) TEMPLATABLE_VALUE(ESPTime, date)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->parent_->make_call(); auto call = this->parent_->make_call();
if (this->date_.has_value()) { if (this->date_.has_value()) {

View File

@@ -1,6 +1,5 @@
#include "datetime_entity.h" #include "datetime_entity.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -49,9 +48,6 @@ void DateTimeEntity::publish_state() {
ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_,
this->month_, this->day_, this->hour_, this->minute_, this->second_); this->month_, this->day_, this->hour_, this->minute_, this->second_);
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_datetime_update(this);
#endif
} }
DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); } DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); }

View File

@@ -124,7 +124,7 @@ template<typename... Ts> class DateTimeSetAction : public Action<Ts...>, public
public: public:
TEMPLATABLE_VALUE(ESPTime, datetime) TEMPLATABLE_VALUE(ESPTime, datetime)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->parent_->make_call(); auto call = this->parent_->make_call();
if (this->datetime_.has_value()) { if (this->datetime_.has_value()) {

View File

@@ -1,6 +1,5 @@
#include "time_entity.h" #include "time_entity.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -30,9 +29,6 @@ void TimeEntity::publish_state() {
ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_,
this->second_); this->second_);
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_time_update(this);
#endif
} }
TimeCall TimeEntity::make_call() { return TimeCall(this); } TimeCall TimeEntity::make_call() { return TimeCall(this); }

View File

@@ -103,7 +103,7 @@ template<typename... Ts> class TimeSetAction : public Action<Ts...>, public Pare
public: public:
TEMPLATABLE_VALUE(ESPTime, time) TEMPLATABLE_VALUE(ESPTime, time)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->parent_->make_call(); auto call = this->parent_->make_call();
if (this->time_.has_value()) { if (this->time_.has_value()) {

View File

@@ -59,7 +59,6 @@ async def to_code(config):
zephyr_add_prj_conf("SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL", True) zephyr_add_prj_conf("SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL", True)
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
cg.add_define("USE_DEBUG")
FILTER_SOURCE_FILES = filter_source_files_from_platform( FILTER_SOURCE_FILES = filter_source_files_from_platform(

View File

@@ -49,9 +49,9 @@ void DebugComponent::dump_config() {
} }
#endif // USE_TEXT_SENSOR #endif // USE_TEXT_SENSOR
#if defined(USE_ESP32) || defined(USE_ZEPHYR) #ifdef USE_ESP32
this->log_partition_info_(); // Log partition information this->log_partition_info_(); // Log partition information for ESP32
#endif #endif // USE_ESP32
} }
void DebugComponent::loop() { void DebugComponent::loop() {

View File

@@ -62,19 +62,19 @@ class DebugComponent : public PollingComponent {
sensor::Sensor *cpu_frequency_sensor_{nullptr}; sensor::Sensor *cpu_frequency_sensor_{nullptr};
#endif // USE_SENSOR #endif // USE_SENSOR
#if defined(USE_ESP32) || defined(USE_ZEPHYR) #ifdef USE_ESP32
/** /**
* @brief Logs information about the device's partition table. * @brief Logs information about the device's partition table.
* *
* This function iterates through the partition table and logs details * This function iterates through the ESP32's partition table and logs details
* about each partition, including its name, type, subtype, starting address, * about each partition, including its name, type, subtype, starting address,
* and size. The information is useful for diagnosing issues related to flash * and size. The information is useful for diagnosing issues related to flash
* memory or verifying the partition configuration dynamically at runtime. * memory or verifying the partition configuration dynamically at runtime.
* *
* Only available when compiled for ESP32 and ZEPHYR platforms. * Only available when compiled for ESP32 platforms.
*/ */
void log_partition_info_(); void log_partition_info_();
#endif #endif // USE_ESP32
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *device_info_{nullptr}; text_sensor::TextSensor *device_info_{nullptr};

View File

@@ -5,11 +5,11 @@
#include <zephyr/drivers/hwinfo.h> #include <zephyr/drivers/hwinfo.h>
#include <hal/nrf_power.h> #include <hal/nrf_power.h>
#include <cstdint> #include <cstdint>
#include <zephyr/storage/flash_map.h>
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0] #define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
namespace esphome::debug { namespace esphome {
namespace debug {
static const char *const TAG = "debug"; static const char *const TAG = "debug";
constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC;
@@ -87,37 +87,6 @@ std::string DebugComponent::get_reset_reason_() {
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; } uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
static void fa_cb(const struct flash_area *fa, void *user_data) {
#if CONFIG_FLASH_MAP_LABELS
const char *fa_label = flash_area_label(fa);
if (fa_label == nullptr) {
fa_label = "-";
}
ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s %-24.24s 0x%-10x 0x%-12x", (int) fa->fa_id,
sizeof(uintptr_t) * 2, (uintptr_t) fa->fa_dev, fa->fa_dev->name, fa_label, (uint32_t) fa->fa_off,
fa->fa_size);
#else
ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s 0x%-10x 0x%-12x", (int) fa->fa_id, sizeof(uintptr_t) * 2,
(uintptr_t) fa->fa_dev, fa->fa_dev->name, (uint32_t) fa->fa_off, fa->fa_size);
#endif
}
void DebugComponent::log_partition_info_() {
#if CONFIG_FLASH_MAP_LABELS
ESP_LOGCONFIG(TAG, "ID | Device | Device Name "
"| Label | Offset | Size");
ESP_LOGCONFIG(TAG, "--------------------------------------------"
"-----------------------------------------------");
#else
ESP_LOGCONFIG(TAG, "ID | Device | Device Name "
"| Offset | Size");
ESP_LOGCONFIG(TAG, "-----------------------------------------"
"------------------------------");
#endif
flash_area_foreach(fa_cb, nullptr);
}
void DebugComponent::get_device_info_(std::string &device_info) { void DebugComponent::get_device_info_(std::string &device_info) {
std::string supply = "Main supply status: "; std::string supply = "Main supply status: ";
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) { if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) {
@@ -312,18 +281,14 @@ void DebugComponent::get_device_info_(std::string &device_info) {
NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE)); NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE));
ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH,
(NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not "));
bool n_reset_enabled = NRF_UICR->PSELRESET[0] == NRF_UICR->PSELRESET[1] &&
(NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected
<< UICR_PSELRESET_CONNECT_Pos;
ESP_LOGD( ESP_LOGD(
TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s", TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s",
YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)), YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)),
YESNO(n_reset_enabled)); YESNO(((NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) !=
if (n_reset_enabled) { (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)) ||
uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos; ((NRF_UICR->PSELRESET[1] & UICR_PSELRESET_CONNECT_Msk) !=
uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos; (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos))));
ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin);
}
#ifdef USE_BOOTLOADER_MCUBOOT #ifdef USE_BOOTLOADER_MCUBOOT
ESP_LOGD(TAG, "bootloader: mcuboot"); ESP_LOGD(TAG, "bootloader: mcuboot");
#else #else
@@ -357,22 +322,10 @@ void DebugComponent::get_device_info_(std::string &device_info) {
#endif #endif
} }
#endif #endif
auto uicr = [](volatile uint32_t *data, uint8_t size) {
std::string res;
char buf[sizeof(uint32_t) * 2 + 1];
for (size_t i = 0; i < size; i++) {
if (i > 0) {
res += ' ';
}
res += format_hex_pretty<uint32_t>(data[i], '\0', false);
}
return res;
};
ESP_LOGD(TAG, "NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str());
ESP_LOGD(TAG, "NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str());
} }
void DebugComponent::update_platform_() {} void DebugComponent::update_platform_() {}
} // namespace esphome::debug } // namespace debug
} // namespace esphome
#endif #endif

View File

@@ -148,7 +148,7 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> {
void set_time(time::RealTimeClock *time) { this->time_ = time; } void set_time(time::RealTimeClock *time) { this->time_ = time; }
#endif #endif
void play(const Ts &...x) override { void play(Ts... x) override {
if (this->sleep_duration_.has_value()) { if (this->sleep_duration_.has_value()) {
this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...)); this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...));
} }
@@ -207,12 +207,12 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> {
template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> { template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
public: public:
void play(const Ts &...x) override { this->parent_->prevent_deep_sleep(); } void play(Ts... x) override { this->parent_->prevent_deep_sleep(); }
}; };
template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> { template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
public: public:
void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); } void play(Ts... x) override { this->parent_->allow_deep_sleep(); }
}; };
} // namespace deep_sleep } // namespace deep_sleep

View File

@@ -28,16 +28,16 @@ class DemoClimate : public climate::Climate, public Component {
this->mode = climate::CLIMATE_MODE_AUTO; this->mode = climate::CLIMATE_MODE_AUTO;
this->action = climate::CLIMATE_ACTION_COOLING; this->action = climate::CLIMATE_ACTION_COOLING;
this->fan_mode = climate::CLIMATE_FAN_HIGH; this->fan_mode = climate::CLIMATE_FAN_HIGH;
this->set_custom_preset_("My Preset"); this->custom_preset = {"My Preset"};
break; break;
case DemoClimateType::TYPE_3: case DemoClimateType::TYPE_3:
this->current_temperature = 21.5; this->current_temperature = 21.5;
this->target_temperature_low = 21.0; this->target_temperature_low = 21.0;
this->target_temperature_high = 22.5; this->target_temperature_high = 22.5;
this->mode = climate::CLIMATE_MODE_HEAT_COOL; this->mode = climate::CLIMATE_MODE_HEAT_COOL;
this->set_custom_fan_mode_("Auto Low"); this->custom_fan_mode = {"Auto Low"};
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
this->set_preset_(climate::CLIMATE_PRESET_AWAY); this->preset = climate::CLIMATE_PRESET_AWAY;
break; break;
} }
this->publish_state(); this->publish_state();
@@ -58,19 +58,23 @@ class DemoClimate : public climate::Climate, public Component {
this->target_temperature_high = *call.get_target_temperature_high(); this->target_temperature_high = *call.get_target_temperature_high();
} }
if (call.get_fan_mode().has_value()) { if (call.get_fan_mode().has_value()) {
this->set_fan_mode_(*call.get_fan_mode()); this->fan_mode = *call.get_fan_mode();
this->custom_fan_mode.reset();
} }
if (call.get_swing_mode().has_value()) { if (call.get_swing_mode().has_value()) {
this->swing_mode = *call.get_swing_mode(); this->swing_mode = *call.get_swing_mode();
} }
if (call.has_custom_fan_mode()) { if (call.get_custom_fan_mode().has_value()) {
this->set_custom_fan_mode_(call.get_custom_fan_mode()); this->custom_fan_mode = *call.get_custom_fan_mode();
this->fan_mode.reset();
} }
if (call.get_preset().has_value()) { if (call.get_preset().has_value()) {
this->set_preset_(*call.get_preset()); this->preset = *call.get_preset();
this->custom_preset.reset();
} }
if (call.has_custom_preset()) { if (call.get_custom_preset().has_value()) {
this->set_custom_preset_(call.get_custom_preset()); this->custom_preset = *call.get_custom_preset();
this->preset.reset();
} }
this->publish_state(); this->publish_state();
} }

View File

@@ -8,7 +8,7 @@ namespace demo {
class DemoSelect : public select::Select, public Component { class DemoSelect : public select::Select, public Component {
protected: protected:
void control(size_t index) override { this->publish_state(index); } void control(const std::string &value) override { this->publish_state(value); }
}; };
} // namespace demo } // namespace demo

View File

@@ -77,7 +77,7 @@ class DFPlayer : public uart::UARTDevice, public Component {
class ACTION_CLASS : /* NOLINT */ \ class ACTION_CLASS : /* NOLINT */ \
public Action<Ts...>, \ public Action<Ts...>, \
public Parented<DFPlayer> { \ public Parented<DFPlayer> { \
void play(const Ts &...x) override { this->parent_->ACTION_METHOD(); } \ void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \
}; };
DFPLAYER_SIMPLE_ACTION(NextAction, next) DFPLAYER_SIMPLE_ACTION(NextAction, next)
@@ -87,7 +87,7 @@ template<typename... Ts> class PlayMp3Action : public Action<Ts...>, public Pare
public: public:
TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(uint16_t, file)
void play(const Ts &...x) override { void play(Ts... x) override {
auto file = this->file_.value(x...); auto file = this->file_.value(x...);
this->parent_->play_mp3(file); this->parent_->play_mp3(file);
} }
@@ -98,7 +98,7 @@ template<typename... Ts> class PlayFileAction : public Action<Ts...>, public Par
TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(uint16_t, file)
TEMPLATABLE_VALUE(bool, loop) TEMPLATABLE_VALUE(bool, loop)
void play(const Ts &...x) override { void play(Ts... x) override {
auto file = this->file_.value(x...); auto file = this->file_.value(x...);
auto loop = this->loop_.value(x...); auto loop = this->loop_.value(x...);
if (loop) { if (loop) {
@@ -115,7 +115,7 @@ template<typename... Ts> class PlayFolderAction : public Action<Ts...>, public P
TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(uint16_t, file)
TEMPLATABLE_VALUE(bool, loop) TEMPLATABLE_VALUE(bool, loop)
void play(const Ts &...x) override { void play(Ts... x) override {
auto folder = this->folder_.value(x...); auto folder = this->folder_.value(x...);
auto file = this->file_.value(x...); auto file = this->file_.value(x...);
auto loop = this->loop_.value(x...); auto loop = this->loop_.value(x...);
@@ -131,7 +131,7 @@ template<typename... Ts> class SetDeviceAction : public Action<Ts...>, public Pa
public: public:
TEMPLATABLE_VALUE(Device, device) TEMPLATABLE_VALUE(Device, device)
void play(const Ts &...x) override { void play(Ts... x) override {
auto device = this->device_.value(x...); auto device = this->device_.value(x...);
this->parent_->set_device(device); this->parent_->set_device(device);
} }
@@ -141,7 +141,7 @@ template<typename... Ts> class SetVolumeAction : public Action<Ts...>, public Pa
public: public:
TEMPLATABLE_VALUE(uint8_t, volume) TEMPLATABLE_VALUE(uint8_t, volume)
void play(const Ts &...x) override { void play(Ts... x) override {
auto volume = this->volume_.value(x...); auto volume = this->volume_.value(x...);
this->parent_->set_volume(volume); this->parent_->set_volume(volume);
} }
@@ -151,7 +151,7 @@ template<typename... Ts> class SetEqAction : public Action<Ts...>, public Parent
public: public:
TEMPLATABLE_VALUE(EqPreset, eq) TEMPLATABLE_VALUE(EqPreset, eq)
void play(const Ts &...x) override { void play(Ts... x) override {
auto eq = this->eq_.value(x...); auto eq = this->eq_.value(x...);
this->parent_->set_eq(eq); this->parent_->set_eq(eq);
} }
@@ -168,7 +168,7 @@ DFPLAYER_SIMPLE_ACTION(VolumeDownAction, volume_down)
template<typename... Ts> class DFPlayerIsPlayingCondition : public Condition<Ts...>, public Parented<DFPlayer> { template<typename... Ts> class DFPlayerIsPlayingCondition : public Condition<Ts...>, public Parented<DFPlayer> {
public: public:
bool check(const Ts &...x) override { return this->parent_->is_playing(); } bool check(Ts... x) override { return this->parent_->is_playing(); }
}; };
class DFPlayerFinishedPlaybackTrigger : public Trigger<> { class DFPlayerFinishedPlaybackTrigger : public Trigger<> {

View File

@@ -11,7 +11,7 @@ namespace dfrobot_sen0395 {
template<typename... Ts> template<typename... Ts>
class DfrobotSen0395ResetAction : public Action<Ts...>, public Parented<DfrobotSen0395Component> { class DfrobotSen0395ResetAction : public Action<Ts...>, public Parented<DfrobotSen0395Component> {
public: public:
void play(const Ts &...x) { this->parent_->enqueue(make_unique<ResetSystemCommand>()); } void play(Ts... x) { this->parent_->enqueue(make_unique<ResetSystemCommand>()); }
}; };
template<typename... Ts> template<typename... Ts>
@@ -33,7 +33,7 @@ class DfrobotSen0395SettingsAction : public Action<Ts...>, public Parented<Dfrob
TEMPLATABLE_VALUE(float, det_min4) TEMPLATABLE_VALUE(float, det_min4)
TEMPLATABLE_VALUE(float, det_max4) TEMPLATABLE_VALUE(float, det_max4)
void play(const Ts &...x) { void play(Ts... x) {
this->parent_->enqueue(make_unique<PowerCommand>(0)); this->parent_->enqueue(make_unique<PowerCommand>(0));
if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) { if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) {
this->parent_->enqueue(make_unique<FactoryResetCommand>()); this->parent_->enqueue(make_unique<FactoryResetCommand>());

View File

@@ -176,117 +176,7 @@ class Display;
class DisplayPage; class DisplayPage;
class DisplayOnPageChangeTrigger; class DisplayOnPageChangeTrigger;
/** Optimized display writer that uses function pointers for stateless lambdas. using display_writer_t = std::function<void(Display &)>;
*
* Similar to TemplatableValue but specialized for display writer callbacks.
* Saves ~8 bytes per stateless lambda on 32-bit platforms (16 bytes std::function → ~8 bytes discriminator+pointer).
*
* Supports both:
* - Stateless lambdas (from YAML) → function pointer (4 bytes)
* - Stateful lambdas/std::function (from C++ code) → std::function* (heap allocated)
*
* @tparam T The display type (e.g., Display, Nextion, GPIOLCDDisplay)
*/
template<typename T> class DisplayWriter {
public:
DisplayWriter() : type_(NONE) {}
// For stateless lambdas (convertible to function pointer): use function pointer (4 bytes)
template<typename F>
DisplayWriter(F f) requires std::invocable<F, T &> && std::convertible_to<F, void (*)(T &)>
: type_(STATELESS_LAMBDA) {
this->stateless_f_ = f; // Implicit conversion to function pointer
}
// For stateful lambdas and std::function (not convertible to function pointer): use std::function* (heap allocated)
// This handles backwards compatibility with external components
template<typename F>
DisplayWriter(F f) requires std::invocable<F, T &> &&(!std::convertible_to<F, void (*)(T &)>) : type_(LAMBDA) {
this->f_ = new std::function<void(T &)>(std::move(f));
}
// Copy constructor
DisplayWriter(const DisplayWriter &other) : type_(other.type_) {
if (type_ == LAMBDA) {
this->f_ = new std::function<void(T &)>(*other.f_);
} else if (type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
}
}
// Move constructor
DisplayWriter(DisplayWriter &&other) noexcept : type_(other.type_) {
if (type_ == LAMBDA) {
this->f_ = other.f_;
other.f_ = nullptr;
} else if (type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
}
other.type_ = NONE;
}
// Assignment operators
DisplayWriter &operator=(const DisplayWriter &other) {
if (this != &other) {
this->~DisplayWriter();
new (this) DisplayWriter(other);
}
return *this;
}
DisplayWriter &operator=(DisplayWriter &&other) noexcept {
if (this != &other) {
this->~DisplayWriter();
new (this) DisplayWriter(std::move(other));
}
return *this;
}
~DisplayWriter() {
if (type_ == LAMBDA) {
delete this->f_;
}
// STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty)
}
bool has_value() const { return this->type_ != NONE; }
void call(T &display) const {
switch (this->type_) {
case STATELESS_LAMBDA:
this->stateless_f_(display); // Direct function pointer call
break;
case LAMBDA:
(*this->f_)(display); // std::function call
break;
case NONE:
default:
break;
}
}
// Operator() for convenience
void operator()(T &display) const { this->call(display); }
// Operator* for backwards compatibility with (*writer_)(*this) pattern
DisplayWriter &operator*() { return *this; }
const DisplayWriter &operator*() const { return *this; }
protected:
enum : uint8_t {
NONE,
LAMBDA,
STATELESS_LAMBDA,
} type_;
union {
std::function<void(T &)> *f_;
void (*stateless_f_)(T &);
};
};
// Type alias for Display writer - uses optimized DisplayWriter instead of std::function
using display_writer_t = DisplayWriter<Display>;
#define LOG_DISPLAY(prefix, type, obj) \ #define LOG_DISPLAY(prefix, type, obj) \
if ((obj) != nullptr) { \ if ((obj) != nullptr) { \
@@ -320,7 +210,7 @@ class Display : public PollingComponent {
/// Fill the entire screen with the given color. /// Fill the entire screen with the given color.
virtual void fill(Color color); virtual void fill(Color color);
/// Clear the entire screen by filling it with OFF pixels. /// Clear the entire screen by filling it with OFF pixels.
virtual void clear(); void clear();
/// Get the calculated width of the display in pixels with rotation applied. /// Get the calculated width of the display in pixels with rotation applied.
virtual int get_width() { return this->get_width_internal(); } virtual int get_width() { return this->get_width_internal(); }
@@ -788,7 +678,7 @@ class Display : public PollingComponent {
void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3); void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3);
DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES}; DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
display_writer_t writer_{}; optional<display_writer_t> writer_{};
DisplayPage *page_{nullptr}; DisplayPage *page_{nullptr};
DisplayPage *previous_page_{nullptr}; DisplayPage *previous_page_{nullptr};
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_; std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
@@ -819,7 +709,7 @@ template<typename... Ts> class DisplayPageShowAction : public Action<Ts...> {
public: public:
TEMPLATABLE_VALUE(DisplayPage *, page) TEMPLATABLE_VALUE(DisplayPage *, page)
void play(const Ts &...x) override { void play(Ts... x) override {
auto *page = this->page_.value(x...); auto *page = this->page_.value(x...);
if (page != nullptr) { if (page != nullptr) {
page->show(); page->show();
@@ -831,7 +721,7 @@ template<typename... Ts> class DisplayPageShowNextAction : public Action<Ts...>
public: public:
DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {} DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {}
void play(const Ts &...x) override { this->buffer_->show_next_page(); } void play(Ts... x) override { this->buffer_->show_next_page(); }
Display *buffer_; Display *buffer_;
}; };
@@ -840,7 +730,7 @@ template<typename... Ts> class DisplayPageShowPrevAction : public Action<Ts...>
public: public:
DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {} DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {}
void play(const Ts &...x) override { this->buffer_->show_prev_page(); } void play(Ts... x) override { this->buffer_->show_prev_page(); }
Display *buffer_; Display *buffer_;
}; };
@@ -850,7 +740,7 @@ template<typename... Ts> class DisplayIsDisplayingPageCondition : public Conditi
DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {} DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {}
void set_page(DisplayPage *page) { this->page_ = page; } void set_page(DisplayPage *page) { this->page_ = page; }
bool check(const Ts &...x) override { return this->parent_->get_active_page() == this->page_; } bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; }
protected: protected:
Display *parent_; Display *parent_;

View File

@@ -10,7 +10,7 @@ template<typename... Ts> class UpAction : public Action<Ts...> {
public: public:
explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->up(); } void play(Ts... x) override { this->menu_->up(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -20,7 +20,7 @@ template<typename... Ts> class DownAction : public Action<Ts...> {
public: public:
explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->down(); } void play(Ts... x) override { this->menu_->down(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -30,7 +30,7 @@ template<typename... Ts> class LeftAction : public Action<Ts...> {
public: public:
explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->left(); } void play(Ts... x) override { this->menu_->left(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -40,7 +40,7 @@ template<typename... Ts> class RightAction : public Action<Ts...> {
public: public:
explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->right(); } void play(Ts... x) override { this->menu_->right(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -50,7 +50,7 @@ template<typename... Ts> class EnterAction : public Action<Ts...> {
public: public:
explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->enter(); } void play(Ts... x) override { this->menu_->enter(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -60,7 +60,7 @@ template<typename... Ts> class ShowAction : public Action<Ts...> {
public: public:
explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->show(); } void play(Ts... x) override { this->menu_->show(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -70,7 +70,7 @@ template<typename... Ts> class HideAction : public Action<Ts...> {
public: public:
explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->hide(); } void play(Ts... x) override { this->menu_->hide(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -80,7 +80,7 @@ template<typename... Ts> class ShowMainAction : public Action<Ts...> {
public: public:
explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->show_main(); } void play(Ts... x) override { this->menu_->show_main(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -88,7 +88,7 @@ template<typename... Ts> class ShowMainAction : public Action<Ts...> {
template<typename... Ts> class IsActiveCondition : public Condition<Ts...> { template<typename... Ts> class IsActiveCondition : public Condition<Ts...> {
public: public:
explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {} explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {}
bool check(const Ts &...x) override { return this->menu_->is_active(); } bool check(Ts... x) override { return this->menu_->is_active(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;

View File

@@ -42,7 +42,7 @@ std::string MenuItemSelect::get_value_text() const {
result = this->value_getter_.value()(this); result = this->value_getter_.value()(this);
} else { } else {
if (this->select_var_ != nullptr) { if (this->select_var_ != nullptr) {
result = this->select_var_->current_option(); result = this->select_var_->state;
} }
} }

View File

@@ -23,7 +23,7 @@ void DS1307Component::dump_config() {
if (this->is_failed()) { if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
} }
RealTimeClock::dump_config(); ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
} }
float DS1307Component::get_setup_priority() const { return setup_priority::DATA; } float DS1307Component::get_setup_priority() const { return setup_priority::DATA; }

View File

@@ -59,12 +59,12 @@ class DS1307Component : public time::RealTimeClock, public i2c::I2CDevice {
template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<DS1307Component> { template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<DS1307Component> {
public: public:
void play(const Ts &...x) override { this->parent_->write_time(); } void play(Ts... x) override { this->parent_->write_time(); }
}; };
template<typename... Ts> class ReadAction : public Action<Ts...>, public Parented<DS1307Component> { template<typename... Ts> class ReadAction : public Action<Ts...>, public Parented<DS1307Component> {
public: public:
void play(const Ts &...x) override { this->parent_->read_time(); } void play(Ts... x) override { this->parent_->read_time(); }
}; };
} // namespace ds1307 } // namespace ds1307
} // namespace esphome } // namespace esphome

View File

@@ -51,15 +51,15 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent {
template<typename... Ts> class BaseAction : public Action<Ts...>, public Parented<DutyTimeSensor> {}; template<typename... Ts> class BaseAction : public Action<Ts...>, public Parented<DutyTimeSensor> {};
template<typename... Ts> class StartAction : public BaseAction<Ts...> { template<typename... Ts> class StartAction : public BaseAction<Ts...> {
void play(const Ts &...x) override { this->parent_->start(); } void play(Ts... x) override { this->parent_->start(); }
}; };
template<typename... Ts> class StopAction : public BaseAction<Ts...> { template<typename... Ts> class StopAction : public BaseAction<Ts...> {
void play(const Ts &...x) override { this->parent_->stop(); } void play(Ts... x) override { this->parent_->stop(); }
}; };
template<typename... Ts> class ResetAction : public BaseAction<Ts...> { template<typename... Ts> class ResetAction : public BaseAction<Ts...> {
void play(const Ts &...x) override { this->parent_->reset(); } void play(Ts... x) override { this->parent_->reset(); }
}; };
template<typename... Ts> class RunningCondition : public Condition<Ts...>, public Parented<DutyTimeSensor> { template<typename... Ts> class RunningCondition : public Condition<Ts...>, public Parented<DutyTimeSensor> {
@@ -67,7 +67,7 @@ template<typename... Ts> class RunningCondition : public Condition<Ts...>, publi
explicit RunningCondition(DutyTimeSensor *parent, bool state) : Parented(parent), state_(state) {} explicit RunningCondition(DutyTimeSensor *parent, bool state) : Parented(parent), state_(state) {}
protected: protected:
bool check(const Ts &...x) override { return this->parent_->is_running() == this->state_; } bool check(Ts... x) override { return this->parent_->is_running() == this->state_; }
bool state_; bool state_;
}; };

View File

@@ -1,35 +1,21 @@
import importlib
import pkgutil
from esphome import core, pins from esphome import core, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import display, spi from esphome.components import display, spi
from esphome.components.mipi import flatten_sequence, map_sequence
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BUSY_PIN, CONF_BUSY_PIN,
CONF_CS_PIN,
CONF_DATA_RATE,
CONF_DC_PIN, CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
CONF_HEIGHT,
CONF_ID, CONF_ID,
CONF_INIT_SEQUENCE,
CONF_LAMBDA, CONF_LAMBDA,
CONF_MODEL, CONF_MODEL,
CONF_PAGES,
CONF_RESET_DURATION, CONF_RESET_DURATION,
CONF_RESET_PIN, CONF_RESET_PIN,
CONF_WIDTH,
) )
from . import models
AUTO_LOAD = ["split_buffer"] AUTO_LOAD = ["split_buffer"]
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
CONF_INIT_SEQUENCE_ID = "init_sequence_id"
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi") epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
EPaperBase = epaper_spi_ns.class_( EPaperBase = epaper_spi_ns.class_(
"EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
@@ -38,78 +24,29 @@ EPaperBase = epaper_spi_ns.class_(
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
MODELS = {
# Import all models dynamically from the models package "7.3in-spectra-e6": EPaper7p3InSpectraE6,
for module_info in pkgutil.iter_modules(models.__path__): }
importlib.import_module(f".models.{module_info.name}", package=__package__)
MODELS = models.EpaperModel.models
DIMENSION_SCHEMA = cv.Schema(
{
cv.Required(CONF_WIDTH): cv.int_,
cv.Required(CONF_HEIGHT): cv.int_,
}
)
def model_schema(config): CONFIG_SCHEMA = cv.All(
model = MODELS[config[CONF_MODEL]] display.FULL_DISPLAY_SCHEMA.extend(
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
return (
display.FULL_DISPLAY_SCHEMA.extend(
spi.spi_device_schema(
cs_pin_required=False,
default_mode="MODE0",
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
)
)
.extend(
{
model.option(pin): pins.gpio_output_pin_schema
for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN)
}
)
.extend(
{
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema,
cv.GenerateID(): cv.declare_id(class_name),
cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8),
cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA,
model.option(CONF_ENABLE_PIN): cv.ensure_list(
pins.gpio_output_pin_schema
),
model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list(
map_sequence
),
model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(milliseconds=500)),
),
}
)
)
def customise_schema(config):
"""
Create a customised config schema for a specific model and validate the configuration.
:param config: The configuration dictionary to validate
:return: The validated configuration dictionary
:raises cv.Invalid: If the configuration is invalid
"""
config = cv.Schema(
{ {
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), cv.GenerateID(): cv.declare_id(EPaperBase),
}, cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
extra=cv.ALLOW_EXTRA, cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"),
)(config) cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
return model_schema(config)(config) cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_RESET_DURATION): cv.All(
cv.positive_time_period_milliseconds,
CONFIG_SCHEMA = customise_schema cv.Range(max=core.TimePeriod(milliseconds=500)),
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(spi.spi_device_schema()),
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
)
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
"epaper_spi", require_miso=False, require_mosi=True "epaper_spi", require_miso=False, require_mosi=True
@@ -119,23 +56,8 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config): async def to_code(config):
model = MODELS[config[CONF_MODEL]] model = MODELS[config[CONF_MODEL]]
init_sequence = config.get(CONF_INIT_SEQUENCE) rhs = model.new()
if init_sequence is None: var = cg.Pvariable(config[CONF_ID], rhs, model)
init_sequence = model.get_init_sequence(config)
init_sequence = flatten_sequence(init_sequence)
init_sequence_length = len(init_sequence)
init_sequence_id = cg.static_const_array(
config[CONF_INIT_SEQUENCE_ID], init_sequence
)
width, height = model.get_dimensions(config)
var = cg.new_Pvariable(
config[CONF_ID],
model.name,
width,
height,
init_sequence_id,
init_sequence_length,
)
await display.register_display(var, config) await display.register_display(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)

View File

@@ -8,20 +8,33 @@ namespace esphome::epaper_spi {
static const char *const TAG = "epaper_spi"; static const char *const TAG = "epaper_spi";
static constexpr const char *const EPAPER_STATE_STRINGS[] = { static const LogString *epaper_state_to_string(EPaperState state) {
"IDLE", "UPDATE", "RESET", "RESET_END", switch (state) {
case EPaperState::IDLE:
"SHOULD_WAIT", "INITIALISE", "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", return LOG_STR("IDLE");
}; case EPaperState::UPDATE:
return LOG_STR("UPDATE");
const char *EPaperBase::epaper_state_to_string_() { case EPaperState::RESET:
if (auto idx = static_cast<unsigned>(this->state_); idx < std::size(EPAPER_STATE_STRINGS)) return LOG_STR("RESET");
return EPAPER_STATE_STRINGS[idx]; case EPaperState::INITIALISE:
return "Unknown"; return LOG_STR("INITIALISE");
case EPaperState::TRANSFER_DATA:
return LOG_STR("TRANSFER_DATA");
case EPaperState::POWER_ON:
return LOG_STR("POWER_ON");
case EPaperState::REFRESH_SCREEN:
return LOG_STR("REFRESH_SCREEN");
case EPaperState::POWER_OFF:
return LOG_STR("POWER_OFF");
case EPaperState::DEEP_SLEEP:
return LOG_STR("DEEP_SLEEP");
default:
return LOG_STR("UNKNOWN");
}
} }
void EPaperBase::setup() { void EPaperBase::setup() {
if (!this->init_buffer_(this->buffer_length_)) { if (!this->init_buffer_(this->get_buffer_length())) {
this->mark_failed("Failed to initialise buffer"); this->mark_failed("Failed to initialise buffer");
return; return;
} }
@@ -37,7 +50,7 @@ bool EPaperBase::init_buffer_(size_t buffer_length) {
return true; return true;
} }
void EPaperBase::setup_pins_() const { void EPaperBase::setup_pins_() {
this->dc_pin_->setup(); // OUTPUT this->dc_pin_->setup(); // OUTPUT
this->dc_pin_->digital_write(false); this->dc_pin_->digital_write(false);
@@ -68,7 +81,11 @@ void EPaperBase::data(uint8_t value) {
// write a command followed by zero or more bytes of data. // write a command followed by zero or more bytes of data.
// The command is the first byte, length is the length of data only in the second byte, followed by the data. // The command is the first byte, length is the length of data only in the second byte, followed by the data.
// [COMMAND, LENGTH, DATA...] // [COMMAND, LENGTH, DATA...]
void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) { void EPaperBase::cmd_data(const uint8_t *data) {
const uint8_t command = data[0];
const uint8_t length = data[1];
const uint8_t *ptr = data + 2;
ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length,
format_hex_pretty(ptr, length, '.', false).c_str()); format_hex_pretty(ptr, length, '.', false).c_str());
@@ -82,146 +99,91 @@ void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) {
this->disable(); this->disable();
} }
bool EPaperBase::is_idle_() const { bool EPaperBase::is_idle_() {
if (this->busy_pin_ == nullptr) { if (this->busy_pin_ == nullptr) {
return true; return true;
} }
return !this->busy_pin_->digital_read(); return this->busy_pin_->digital_read();
} }
bool EPaperBase::reset_() const { void EPaperBase::reset() {
if (this->reset_pin_ != nullptr) { if (this->reset_pin_ != nullptr) {
if (this->state_ == EPaperState::RESET) { this->reset_pin_->digital_write(false);
this->reset_pin_->digital_write(false); this->disable_loop();
return false; this->set_timeout(this->reset_duration_, [this] {
} this->reset_pin_->digital_write(true);
this->reset_pin_->digital_write(true); this->set_timeout(20, [this] { this->enable_loop(); });
});
} }
return true;
} }
void EPaperBase::update() { void EPaperBase::update() {
if (this->state_ != EPaperState::IDLE) { if (!this->state_queue_.empty()) {
ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_()); ESP_LOGE(TAG, "Display update already in progress - %s",
LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front())));
return; return;
} }
this->set_state_(EPaperState::RESET);
this->state_queue_.push(EPaperState::UPDATE);
this->state_queue_.push(EPaperState::RESET);
this->state_queue_.push(EPaperState::INITIALISE);
this->state_queue_.push(EPaperState::TRANSFER_DATA);
this->state_queue_.push(EPaperState::POWER_ON);
this->state_queue_.push(EPaperState::REFRESH_SCREEN);
this->state_queue_.push(EPaperState::POWER_OFF);
this->state_queue_.push(EPaperState::DEEP_SLEEP);
this->state_queue_.push(EPaperState::IDLE);
this->enable_loop(); this->enable_loop();
} }
void EPaperBase::wait_for_idle_(bool should_wait) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
if (should_wait) {
this->waiting_for_idle_start_ = millis();
this->waiting_for_idle_last_print_ = this->waiting_for_idle_start_;
}
#endif
this->waiting_for_idle_ = should_wait;
}
/**
* Called during the loop task.
* First defer for any pending delays, then check if we are waiting for the display to become idle.
* If not waiting for idle, process the state machine.
*/
void EPaperBase::loop() { void EPaperBase::loop() {
auto now = millis();
if (this->delay_until_ != 0) {
// using modulus arithmetic to handle wrap-around
int diff = now - this->delay_until_;
if (diff < 0) {
return;
}
this->delay_until_ = 0;
}
if (this->waiting_for_idle_) { if (this->waiting_for_idle_) {
if (this->is_idle_()) { if (this->is_idle_()) {
this->waiting_for_idle_ = false; this->waiting_for_idle_ = false;
ESP_LOGV(TAG, "Screen now idle after %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
} else { } else {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) {
if (now - this->waiting_for_idle_last_print_ >= 1000) { ESP_LOGV(TAG, "Waiting for idle");
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_()); this->waiting_for_idle_last_print_ = App.get_loop_component_start_time();
this->waiting_for_idle_last_print_ = millis();
} }
#endif
return; return;
} }
} }
this->process_state_();
}
/** auto state = this->state_queue_.front();
* Process the state machine.
* Typical state sequence: switch (state) {
* IDLE -> RESET -> RESET_END -> UPDATE -> INITIALISE -> TRANSFER_DATA -> POWER_ON -> REFRESH_SCREEN -> POWER_OFF ->
* DEEP_SLEEP -> IDLE
*
* Should a subclassed class need to override this, the method will need to be made virtual.
*/
void EPaperBase::process_state_() {
ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_());
switch (this->state_) {
default:
ESP_LOGD(TAG, "Display is in unhandled state %s", epaper_state_to_string_());
this->disable_loop();
break;
case EPaperState::IDLE: case EPaperState::IDLE:
this->disable_loop(); this->disable_loop();
break; break;
case EPaperState::RESET:
case EPaperState::RESET_END:
if (this->reset_()) {
this->set_state_(EPaperState::UPDATE);
} else {
this->set_state_(EPaperState::RESET_END);
}
break;
case EPaperState::UPDATE: case EPaperState::UPDATE:
this->do_update_(); // Calls ESPHome (current page) lambda this->do_update_(); // Calls ESPHome (current page) lambda
this->set_state_(EPaperState::INITIALISE); break;
case EPaperState::RESET:
this->reset();
break; break;
case EPaperState::INITIALISE: case EPaperState::INITIALISE:
this->initialise_(); this->initialise_();
this->set_state_(EPaperState::TRANSFER_DATA);
break; break;
case EPaperState::TRANSFER_DATA: case EPaperState::TRANSFER_DATA:
if (!this->transfer_data()) { if (!this->transfer_data()) {
return; // Not done yet, come back next loop return; // Not done yet, come back next loop
} }
this->set_state_(EPaperState::POWER_ON);
break; break;
case EPaperState::POWER_ON: case EPaperState::POWER_ON:
this->power_on(); this->power_on();
this->set_state_(EPaperState::REFRESH_SCREEN);
break; break;
case EPaperState::REFRESH_SCREEN: case EPaperState::REFRESH_SCREEN:
this->refresh_screen(); this->refresh_screen();
this->set_state_(EPaperState::POWER_OFF);
break; break;
case EPaperState::POWER_OFF: case EPaperState::POWER_OFF:
this->power_off(); this->power_off();
this->set_state_(EPaperState::DEEP_SLEEP);
break; break;
case EPaperState::DEEP_SLEEP: case EPaperState::DEEP_SLEEP:
this->deep_sleep(); this->deep_sleep();
this->set_state_(EPaperState::IDLE);
break; break;
} }
} this->state_queue_.pop();
void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_());
this->state_ = state;
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
if (delay != 0) {
this->delay_until_ = millis() + delay;
} else {
this->delay_until_ = 0;
}
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
TRUEFALSE(this->waiting_for_idle_));
} }
void EPaperBase::start_command_() { void EPaperBase::start_command_() {
@@ -241,39 +203,25 @@ void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
void EPaperBase::initialise_() { void EPaperBase::initialise_() {
size_t index = 0; size_t index = 0;
const auto &sequence = this->init_sequence_;
auto *sequence = this->init_sequence_; const size_t sequence_size = this->init_sequence_length_;
auto length = this->init_sequence_length_; while (index != sequence_size) {
while (index != length) { if (sequence_size - index < 2) {
if (length - index < 2) {
this->mark_failed("Malformed init sequence"); this->mark_failed("Malformed init sequence");
return; return;
} }
const uint8_t cmd = sequence[index++]; const auto *ptr = sequence + index;
if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) { const uint8_t length = ptr[1];
ESP_LOGV(TAG, "Delay %dms", cmd); if (sequence_size - index < length + 2) {
delay(cmd); this->mark_failed("Malformed init sequence");
} else { return;
const uint8_t num_args = x & 0x7F;
if (length - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence, cmd = %X, num_args = %u", cmd, num_args);
this->mark_failed();
return;
}
ESP_LOGV(TAG, "Command %02X, length %d", cmd, num_args);
this->cmd_data(cmd, sequence + index, num_args);
index += num_args;
} }
}
}
void EPaperBase::dump_config() { this->cmd_data(ptr);
LOG_DISPLAY("", "E-Paper SPI", this); index += length + 2;
ESP_LOGCONFIG(TAG, " Model: %s", this->name_); }
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_); this->power_on();
LOG_PIN(" Busy Pin: ", this->busy_pin_);
LOG_UPDATE_INTERVAL(this);
} }
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -8,48 +8,36 @@
#include <queue> #include <queue>
namespace esphome::epaper_spi { namespace esphome::epaper_spi {
using namespace display;
enum class EPaperState : uint8_t { enum class EPaperState : uint8_t {
IDLE, // not doing anything IDLE,
UPDATE, // update the buffer UPDATE,
RESET, // drive reset low (active) RESET,
RESET_END, // drive reset high (inactive) INITIALISE,
TRANSFER_DATA,
SHOULD_WAIT, // states higher than this should wait for the display to be not busy POWER_ON,
INITIALISE, // send the init sequence REFRESH_SCREEN,
TRANSFER_DATA, // transfer data to the display POWER_OFF,
POWER_ON, // power on the display DEEP_SLEEP,
REFRESH_SCREEN, // send refresh command
POWER_OFF, // power off the display
DEEP_SLEEP, // deep sleep the display
}; };
static constexpr uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run static const uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
static constexpr uint8_t DELAY_FLAG = 0xFF;
class EPaperBase : public DisplayBuffer, class EPaperBase : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_2MHZ> { spi::DATA_RATE_2MHZ> {
public: public:
EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length)
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY) : init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {}
: name_(name),
width_(width),
height_(height),
init_sequence_(init_sequence),
init_sequence_length_(init_sequence_length),
display_type_(display_type) {}
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
float get_setup_priority() const override; float get_setup_priority() const override;
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
void dump_config() override;
void command(uint8_t value); void command(uint8_t value);
void data(uint8_t value); void data(uint8_t value);
void cmd_data(uint8_t command, const uint8_t *ptr, size_t length); void cmd_data(const uint8_t *data);
void update() override; void update() override;
void loop() override; void loop() override;
@@ -58,84 +46,48 @@ class EPaperBase : public DisplayBuffer,
void on_safe_shutdown() override; void on_safe_shutdown() override;
DisplayType get_display_type() override { return this->display_type_; };
protected: protected:
int get_height_internal() override { return this->height_; }; bool is_idle_();
int get_width_internal() override { return this->width_; }; void setup_pins_();
void process_state_(); virtual void reset();
const char *epaper_state_to_string_();
bool is_idle_() const;
void setup_pins_() const;
bool reset_() const;
void initialise_(); void initialise_();
void wait_for_idle_(bool should_wait);
bool init_buffer_(size_t buffer_length); bool init_buffer_(size_t buffer_length);
virtual int get_width_controller() { return this->get_width_internal(); }; virtual int get_width_controller() { return this->get_width_internal(); };
virtual void deep_sleep() = 0;
/**
* Methods that must be implemented by concrete classes to control the display
*/
/** /**
* Send data to the device via SPI * Send data to the device via SPI
* @return true if done, false if it should be called next loop * @return true if done, false if should be called next loop
*/ */
virtual bool transfer_data() = 0; virtual bool transfer_data() = 0;
/**
* Refresh the screen after data transfer
*/
virtual void refresh_screen() = 0; virtual void refresh_screen() = 0;
/**
* Power the display on
*/
virtual void power_on() = 0; virtual void power_on() = 0;
/**
* Power the display off
*/
virtual void power_off() = 0; virtual void power_off() = 0;
virtual uint32_t get_buffer_length() = 0;
/**
* Place the display into deep sleep
*/
virtual void deep_sleep() = 0;
void set_state_(EPaperState state, uint16_t delay = 0);
void start_command_(); void start_command_();
void end_command_(); void end_command_();
void start_data_(); void start_data_();
void end_data_(); void end_data_();
// properties initialised in the constructor const size_t init_sequence_length_{0};
const char *name_;
uint16_t width_;
uint16_t height_;
const uint8_t *init_sequence_;
size_t init_sequence_length_;
DisplayType display_type_;
size_t buffer_length_{}; size_t current_data_index_{0};
size_t current_data_index_{0}; // used by data transfer to track progress
uint32_t reset_duration_{200}; uint32_t reset_duration_{200};
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
uint32_t transfer_start_time_{};
uint32_t waiting_for_idle_last_print_{0}; uint32_t waiting_for_idle_last_print_{0};
uint32_t waiting_for_idle_start_{0};
#endif
GPIOPin *dc_pin_{}; GPIOPin *dc_pin_;
GPIOPin *busy_pin_{}; GPIOPin *busy_pin_{nullptr};
GPIOPin *reset_pin_{}; GPIOPin *reset_pin_{nullptr};
const uint8_t *init_sequence_{nullptr};
bool waiting_for_idle_{false}; bool waiting_for_idle_{false};
uint32_t delay_until_{0};
split_buffer::SplitBuffer buffer_; split_buffer::SplitBuffer buffer_;
EPaperState state_{EPaperState::IDLE}; std::queue<EPaperState> state_queue_{{EPaperState::IDLE}};
}; };
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -0,0 +1,42 @@
#include "epaper_spi_model_7p3in_spectra_e6.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6";
void EPaper7p3InSpectraE6::power_on() {
ESP_LOGI(TAG, "Power on");
this->command(0x04);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::power_off() {
ESP_LOGI(TAG, "Power off");
this->command(0x02);
this->data(0x00);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::refresh_screen() {
ESP_LOGI(TAG, "Refresh");
this->command(0x12);
this->data(0x00);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::deep_sleep() {
ESP_LOGI(TAG, "Deep sleep");
this->command(0x07);
this->data(0xA5);
}
void EPaper7p3InSpectraE6::dump_config() {
LOG_DISPLAY("", "E-Paper SPI", this);
ESP_LOGCONFIG(TAG, " Model: 7.3in Spectra E6");
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
LOG_PIN(" Busy Pin: ", this->busy_pin_);
LOG_UPDATE_INTERVAL(this);
}
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,45 @@
#pragma once
#include "epaper_spi_spectra_e6.h"
namespace esphome::epaper_spi {
class EPaper7p3InSpectraE6 : public EPaperSpectraE6 {
static constexpr const uint16_t WIDTH = 800;
static constexpr const uint16_t HEIGHT = 480;
// clang-format off
// Command, data length, data
static constexpr uint8_t INIT_SEQUENCE[] = {
0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,
0x01, 1, 0x3F,
0x00, 2, 0x5F, 0x69,
0x03, 4, 0x00, 0x54, 0x00, 0x44,
0x05, 4, 0x40, 0x1F, 0x1F, 0x2C,
0x06, 4, 0x6F, 0x1F, 0x17, 0x49,
0x08, 4, 0x6F, 0x1F, 0x1F, 0x22,
0x30, 1, 0x03,
0x50, 1, 0x3F,
0x60, 2, 0x02, 0x00,
0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256,
0x84, 1, 0x01,
0xE3, 1, 0x2F,
};
// clang-format on
public:
EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {}
void dump_config() override;
protected:
int get_width_internal() override { return WIDTH; };
int get_height_internal() override { return HEIGHT; };
void refresh_screen() override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
};
} // namespace esphome::epaper_spi

View File

@@ -1,166 +1,135 @@
#include "epaper_spi_spectra_e6.h" #include "epaper_spi_spectra_e6.h"
#include <algorithm>
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::epaper_spi { namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.6c"; static constexpr const char *const TAG = "epaper_spi.6c";
static constexpr size_t MAX_TRANSFER_SIZE = 128;
static constexpr unsigned char GRAY_THRESHOLD = 50;
enum E6Color { static inline uint8_t color_to_hex(Color color) {
BLACK, if (color.red > 127) {
WHITE, if (color.green > 170) {
YELLOW, if (color.blue > 127) {
RED, return 0x1; // White
SKIP_1, } else {
BLUE, return 0x2; // Yellow
GREEN, }
CYAN, } else {
SKIP_2, return 0x3; // Red (or Magenta)
}; }
} else {
static uint8_t color_to_hex(Color color) { if (color.green > 127) {
// --- Step 1: Check for Grayscale (Black or White) --- if (color.blue > 127) {
// We define "grayscale" as a color where the min and max components return 0x5; // Cyan -> Blue
// are close to each other. } else {
unsigned char max_rgb = std::max({color.r, color.g, color.b}); return 0x6; // Green
unsigned char min_rgb = std::min({color.r, color.g, color.b}); }
} else {
if ((max_rgb - min_rgb) < GRAY_THRESHOLD) { if (color.blue > 127) {
// It's a shade of gray. Map to BLACK or WHITE. return 0x5; // Blue
// We split the luminance at the halfway point (382 = (255*3)/2) } else {
if ((static_cast<int>(color.r) + color.g + color.b) > 382) { return 0x0; // Black
return WHITE; }
} }
return BLACK;
} }
// --- Step 2: Check for Primary/Secondary Colors ---
// If it's not gray, it's a color. We check which components are
// "on" (over 128) vs "off". This divides the RGB cube into 8 corners.
bool r_on = (color.r > 128);
bool g_on = (color.g > 128);
bool b_on = (color.b > 128);
if (r_on && g_on && !b_on) {
return YELLOW;
}
if (r_on && !g_on && !b_on) {
return RED;
}
if (!r_on && g_on && !b_on) {
return GREEN;
}
if (!r_on && !g_on && b_on) {
return BLUE;
}
// Handle "impure" colors (Cyan, Magenta)
if (!r_on && g_on && b_on) {
// Cyan (G+B) -> Closest is Green or Blue. Pick Green.
return GREEN;
}
if (r_on && !g_on) {
// Magenta (R+B) -> Closest is Red or Blue. Pick Red.
return RED;
}
// Handle the remaining corners (White-ish, Black-ish)
if (r_on) {
// All high (but not gray) -> White
return WHITE;
}
// !r_on && !g_on && !b_on
// All low (but not gray) -> Black
return BLACK;
}
void EPaperSpectraE6::power_on() {
ESP_LOGD(TAG, "Power on");
this->command(0x04);
}
void EPaperSpectraE6::power_off() {
ESP_LOGD(TAG, "Power off");
this->command(0x02);
this->data(0x00);
}
void EPaperSpectraE6::refresh_screen() {
ESP_LOGD(TAG, "Refresh");
this->command(0x12);
this->data(0x00);
}
void EPaperSpectraE6::deep_sleep() {
ESP_LOGD(TAG, "Deep sleep");
this->command(0x07);
this->data(0xA5);
} }
void EPaperSpectraE6::fill(Color color) { void EPaperSpectraE6::fill(Color color) {
auto pixel_color = color_to_hex(color); uint8_t pixel_color;
if (color.is_on()) {
pixel_color = color_to_hex(color);
} else {
pixel_color = 0x1;
}
// We store 2 pixels per byte // We store 8 bitset<3> in 3 bytes
this->buffer_.fill(pixel_color + (pixel_color << 4)); // | byte 1 | byte 2 | byte 3 |
// |aaabbbaa|abbbaaab|bbaaabbb|
uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1;
uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2;
uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0;
const size_t buffer_length = this->get_buffer_length();
for (size_t i = 0; i < buffer_length; i += 3) {
this->buffer_[i + 0] = byte_1;
this->buffer_[i + 1] = byte_2;
this->buffer_[i + 2] = byte_3;
}
} }
void EPaperSpectraE6::clear() { uint32_t EPaperSpectraE6::get_buffer_length() {
// clear buffer to white, just like real paper. // 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes
this->fill(COLOR_ON); return this->get_width_controller() * this->get_height_internal() / 8u * 3u;
} }
void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) { void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0)
return; return;
auto pixel_bits = color_to_hex(color); uint8_t pixel_bits = color_to_hex(color);
uint32_t pixel_position = x + y * this->get_width_controller(); uint32_t pixel_position = x + y * this->get_width_controller();
uint32_t byte_position = pixel_position / 2; uint32_t first_bit_position = pixel_position * 3;
auto original = this->buffer_[byte_position]; uint32_t byte_position = first_bit_position / 8u;
if ((pixel_position & 1) != 0) { uint32_t byte_subposition = first_bit_position % 8u;
this->buffer_[byte_position] = (original & 0xF0) | pixel_bits;
if (byte_subposition <= 5) {
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) |
(pixel_bits << (5 - byte_subposition));
} else { } else {
this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4); this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) |
(pixel_bits >> (byte_subposition - 5));
this->buffer_[byte_position + 1] =
(this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) |
(pixel_bits << (13 - byte_subposition));
} }
} }
bool HOT EPaperSpectraE6::transfer_data() { bool HOT EPaperSpectraE6::transfer_data() {
const uint32_t start_time = App.get_loop_component_start_time(); const uint32_t start_time = App.get_loop_component_start_time();
const size_t buffer_length = this->buffer_length_;
if (this->current_data_index_ == 0) { if (this->current_data_index_ == 0) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, "Sending data");
this->transfer_start_time_ = millis();
#endif
ESP_LOGV(TAG, "Start sending data at %ums", (unsigned) millis());
this->command(0x10); this->command(0x10);
} }
size_t buf_idx = 0; uint8_t bytes_to_send[4]{0};
uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; const size_t buffer_length = this->get_buffer_length();
while (this->current_data_index_ != buffer_length) { for (size_t i = this->current_data_index_; i < buffer_length; i += 3) {
bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++]; const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]);
// 8 pixels are stored in 3 bytes
// |aaabbbaa|abbbaaab|bbaaabbb|
// | byte 1 | byte 2 | byte 3 |
bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111);
bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111);
bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111);
bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111);
if (buf_idx == sizeof bytes_to_send) { this->start_data_();
this->start_data_(); this->write_array(bytes_to_send, sizeof(bytes_to_send));
this->write_array(bytes_to_send, buf_idx); this->end_data_();
this->end_data_();
ESP_LOGV(TAG, "Wrote %d bytes at %ums", buf_idx, (unsigned) millis());
buf_idx = 0;
if (millis() - start_time > MAX_TRANSFER_TIME) { if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop // Let the main loop run and come back next loop
return false; this->current_data_index_ = i + 3;
} return false;
} }
} }
// Finished the entire dataset // Finished the entire dataset
if (buf_idx != 0) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->end_data_();
}
this->current_data_index_ = 0; this->current_data_index_ = 0;
ESP_LOGV(TAG, "Sent data in %" PRIu32 " ms", millis() - this->transfer_start_time_);
return true; return true;
} }
void EPaperSpectraE6::reset() {
if (this->reset_pin_ != nullptr) {
this->disable_loop();
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] {
this->reset_pin_->digital_write(false);
delay(2);
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] { this->enable_loop(); });
});
}
}
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -6,23 +6,18 @@ namespace esphome::epaper_spi {
class EPaperSpectraE6 : public EPaperBase { class EPaperSpectraE6 : public EPaperBase {
public: public:
EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length)
size_t init_sequence_length) : EPaperBase(init_sequence, init_sequence_length) {}
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) {
this->buffer_length_ = width * height / 2; // 2 pixels per byte
}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
void fill(Color color) override; void fill(Color color) override;
void clear() override;
protected: protected:
void refresh_screen() override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
void draw_absolute_pixel_internal(int x, int y, Color color) override; void draw_absolute_pixel_internal(int x, int y, Color color) override;
uint32_t get_buffer_length() override;
bool transfer_data() override; bool transfer_data() override;
void reset() override;
}; };
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -1,65 +0,0 @@
from typing import Any, Self
import esphome.config_validation as cv
from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_WIDTH
class EpaperModel:
models: dict[str, Self] = {}
def __init__(
self,
name: str,
class_name: str,
initsequence=None,
**defaults,
):
name = name.upper()
self.name = name
self.class_name = class_name
self.initsequence = initsequence
self.defaults = defaults
EpaperModel.models[name] = self
def get_default(self, key, fallback: Any = False) -> Any:
return self.defaults.get(key, fallback)
def get_init_sequence(self, config: dict):
return self.initsequence
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
if fallback is None and self.get_default(name, None) is None:
return cv.Required(name)
return cv.Optional(name, default=self.get_default(name, fallback))
def get_dimensions(self, config) -> tuple[int, int]:
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
width = dimensions[CONF_WIDTH]
height = dimensions[CONF_HEIGHT]
else:
(width, height) = dimensions
else:
# Default dimensions, use model defaults
width = self.get_default(CONF_WIDTH)
height = self.get_default(CONF_HEIGHT)
return width, height
def extend(self, name, **kwargs) -> "EpaperModel":
"""
Extend the current model with additional parameters or a modified init sequence.
Parameters supplied here will override the defaults of the current model.
if the initsequence is not provided, the current model's initsequence will be used.
If add_init_sequence is provided, it will be appended to the current initsequence.
:param name:
:param kwargs:
:return:
"""
initsequence = list(kwargs.pop("initsequence", self.initsequence) or ())
initsequence.extend(kwargs.pop("add_init_sequence", ()))
defaults = self.defaults.copy()
defaults.update(kwargs)
return self.__class__(name, initsequence=tuple(initsequence), **defaults)

View File

@@ -1,51 +0,0 @@
from typing import Any
from . import EpaperModel
class SpectraE6(EpaperModel):
def __init__(self, name, class_name="EPaperSpectraE6", **kwargs):
super().__init__(name, class_name, **kwargs)
# fmt: off
def get_init_sequence(self, config: dict):
width, height = self.get_dimensions(config)
return (
(0xAA, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,),
(0x01, 0x3F,),
(0x00, 0x5F, 0x69,),
(0x03, 0x00, 0x54, 0x00, 0x44,),
(0x05, 0x40, 0x1F, 0x1F, 0x2C,),
(0x06, 0x6F, 0x1F, 0x17, 0x49,),
(0x08, 0x6F, 0x1F, 0x1F, 0x22,),
(0x30, 0x03,),
(0x50, 0x3F,),
(0x60, 0x02, 0x00,),
(0x61, width // 256, width % 256, height // 256, height % 256,),
(0x84, 0x01,),
(0xE3, 0x2F,),
)
def get_default(self, key, fallback: Any = False) -> Any:
return self.defaults.get(key, fallback)
spectra_e6 = SpectraE6("spectra-e6")
spectra_e6.extend(
"Seeed-reTerminal-E1002",
width=800,
height=480,
data_rate="20MHz",
cs_pin=10,
dc_pin=11,
reset_pin=12,
busy_pin={
"number": 13,
"inverted": True,
"mode": {
"input": True,
"pullup": True,
},
},
)

View File

@@ -3,9 +3,9 @@
namespace esphome { namespace esphome {
namespace es8388 { namespace es8388 {
void ADCInputMicSelect::control(size_t index) { void ADCInputMicSelect::control(const std::string &value) {
this->publish_state(index); this->publish_state(value);
this->parent_->set_adc_input_mic(static_cast<AdcInputMicLine>(index)); this->parent_->set_adc_input_mic(static_cast<AdcInputMicLine>(this->index_of(value).value()));
} }
} // namespace es8388 } // namespace es8388

View File

@@ -8,7 +8,7 @@ namespace es8388 {
class ADCInputMicSelect : public select::Select, public Parented<ES8388> { class ADCInputMicSelect : public select::Select, public Parented<ES8388> {
protected: protected:
void control(size_t index) override; void control(const std::string &value) override;
}; };
} // namespace es8388 } // namespace es8388

View File

@@ -3,9 +3,9 @@
namespace esphome { namespace esphome {
namespace es8388 { namespace es8388 {
void DacOutputSelect::control(size_t index) { void DacOutputSelect::control(const std::string &value) {
this->publish_state(index); this->publish_state(value);
this->parent_->set_dac_output(static_cast<DacOutputLine>(index)); this->parent_->set_dac_output(static_cast<DacOutputLine>(this->index_of(value).value()));
} }
} // namespace es8388 } // namespace es8388

View File

@@ -8,7 +8,7 @@ namespace es8388 {
class DacOutputSelect : public select::Select, public Parented<ES8388> { class DacOutputSelect : public select::Select, public Parented<ES8388> {
protected: protected:
void control(size_t index) override; void control(const std::string &value) override;
}; };
} // namespace es8388 } // namespace es8388

View File

@@ -558,7 +558,6 @@ CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios"
CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select"
CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir"
CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
# VFS requirement tracking # VFS requirement tracking
# Components that need VFS features can call require_vfs_select() or require_vfs_dir() # Components that need VFS features can call require_vfs_select() or require_vfs_dir()
@@ -655,9 +654,6 @@ FRAMEWORK_SCHEMA = cv.All(
): cv.boolean, ): cv.boolean,
cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean,
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range(
min=8192, max=32768
),
} }
), ),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
@@ -930,10 +926,6 @@ async def to_code(config):
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
), ),
) )
add_idf_sdkconfig_option(
"CONFIG_ARDUINO_LOOP_STACK_SIZE",
conf[CONF_ADVANCED][CONF_LOOP_TASK_STACK_SIZE],
)
add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True) add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
@@ -1079,10 +1071,6 @@ async def to_code(config):
) )
add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True) add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True)
cg.add_define(
"ESPHOME_LOOP_TASK_STACK_SIZE", advanced.get(CONF_LOOP_TASK_STACK_SIZE)
)
cg.add_define( cg.add_define(
"USE_ESP_IDF_VERSION_CODE", "USE_ESP_IDF_VERSION_CODE",
cg.RawExpression( cg.RawExpression(

View File

@@ -1,6 +1,5 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "esphome/core/defines.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "preferences.h" #include "preferences.h"
@@ -97,11 +96,7 @@ void loop_task(void *pv_params) {
extern "C" void app_main() { extern "C" void app_main() {
esp32::setup_preferences(); esp32::setup_preferences();
#if CONFIG_FREERTOS_UNICORE xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle);
xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle);
#else
xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1);
#endif
} }
#endif // USE_ESP_IDF #endif // USE_ESP_IDF

View File

@@ -7,7 +7,6 @@ from typing import Any
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import socket
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -22,7 +21,6 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
import esphome.final_validate as fv import esphome.final_validate as fv
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
AUTO_LOAD = ["socket"]
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
DOMAIN = "esp32_ble" DOMAIN = "esp32_ble"
@@ -483,11 +481,6 @@ async def to_code(config):
cg.add(var.set_name(name)) cg.add(var.set_name(name))
await cg.register_component(var, config) await cg.register_component(var, config)
# BLE uses the socket wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks
# This enables low-latency (~12μs) BLE event processing instead of waiting for
# select() timeout (0-16ms). The wake socket is shared across all components.
socket.require_wake_loop_threadsafe()
# Define max connections for use in C++ code (e.g., ble_server.h) # Define max connections for use in C++ code (e.g., ble_server.h)
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections) cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)

View File

@@ -31,26 +31,6 @@ namespace esphome::esp32_ble {
static const char *const TAG = "esp32_ble"; static const char *const TAG = "esp32_ble";
// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_
#define GAP_SCAN_COMPLETE_EVENTS \
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: \
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT
#define GAP_ADV_COMPLETE_EVENTS \
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: \
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: \
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: \
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: \
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT
#define GAP_SECURITY_EVENTS \
case ESP_GAP_BLE_AUTH_CMPL_EVT: \
case ESP_GAP_BLE_SEC_REQ_EVT: \
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: \
case ESP_GAP_BLE_PASSKEY_REQ_EVT: \
case ESP_GAP_BLE_NC_REQ_EVT
void ESP32BLE::setup() { void ESP32BLE::setup() {
global_ble = this; global_ble = this;
if (!ble_pre_setup_()) { if (!ble_pre_setup_()) {
@@ -434,48 +414,60 @@ void ESP32BLE::loop() {
break; break;
// Scan complete events // Scan complete events
GAP_SCAN_COMPLETE_EVENTS: case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
// Advertising complete events case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
GAP_ADV_COMPLETE_EVENTS: case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
// RSSI complete event // All three scan complete events have the same structure with just status
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
// Security events // This is verified at compile-time by static_assert checks in ble_event.h
GAP_SECURITY_EVENTS: // The struct already contains our copy of the status (copied in BLEEvent constructor)
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
{ for (auto *gap_handler : this->gap_event_handlers_) {
esp_ble_gap_cb_param_t *param; gap_handler->gap_event_handler(
// clang-format off gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete));
switch (gap_event) { }
// All three scan complete events have the same structure with just status #endif
// The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe break;
// This is verified at compile-time by static_assert checks in ble_event.h
// The struct already contains our copy of the status (copied in BLEEvent constructor)
GAP_SCAN_COMPLETE_EVENTS:
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete);
break;
// All advertising complete events have the same structure with just status // Advertising complete events
GAP_ADV_COMPLETE_EVENTS: case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete); case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
break; case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
// All advertising complete events have the same structure with just status
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete));
}
#endif
break;
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: // RSSI complete event
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete); case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
break; ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete));
}
#endif
break;
GAP_SECURITY_EVENTS: // Security events
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security); case ESP_GAP_BLE_AUTH_CMPL_EVT:
break; case ESP_GAP_BLE_SEC_REQ_EVT:
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
default: case ESP_GAP_BLE_PASSKEY_REQ_EVT:
break; case ESP_GAP_BLE_NC_REQ_EVT:
} ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
// clang-format on #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
// Dispatch to all registered handlers for (auto *gap_handler : this->gap_event_handlers_) {
for (auto *gap_handler : this->gap_event_handlers_) { gap_handler->gap_event_handler(
gap_handler->gap_event_handler(gap_event, param); gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security));
}
} }
#endif #endif
break; break;
@@ -555,22 +547,24 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
// Queue GAP events that components need to handle // Queue GAP events that components need to handle
// Scanning events - used by esp32_ble_tracker // Scanning events - used by esp32_ble_tracker
case ESP_GAP_BLE_SCAN_RESULT_EVT: case ESP_GAP_BLE_SCAN_RESULT_EVT:
GAP_SCAN_COMPLETE_EVENTS: case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
// Advertising events - used by esp32_ble_beacon and esp32_ble server // Advertising events - used by esp32_ble_beacon and esp32_ble server
GAP_ADV_COMPLETE_EVENTS: case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
// Connection events - used by ble_client // Connection events - used by ble_client
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
enqueue_ble_event(event, param);
return;
// Security events - used by ble_client and bluetooth_proxy // Security events - used by ble_client and bluetooth_proxy
// These are rare but interactive (pairing/bonding), so notify immediately case ESP_GAP_BLE_AUTH_CMPL_EVT:
GAP_SECURITY_EVENTS: case ESP_GAP_BLE_SEC_REQ_EVT:
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
case ESP_GAP_BLE_NC_REQ_EVT:
enqueue_ble_event(event, param); enqueue_ble_event(event, param);
// Wake up main loop to process security event immediately
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
return; return;
// Ignore these GAP events as they are not relevant for our use case // Ignore these GAP events as they are not relevant for our use case
@@ -590,10 +584,6 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) { esp_ble_gatts_cb_param_t *param) {
enqueue_ble_event(event, gatts_if, param); enqueue_ble_event(event, gatts_if, param);
// Wake up main loop to process GATT event immediately
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
} }
#endif #endif
@@ -601,10 +591,6 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
enqueue_ble_event(event, gattc_if, param); enqueue_ble_event(event, gattc_if, param);
// Wake up main loop to process GATT event immediately
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
} }
#endif #endif

View File

@@ -162,11 +162,6 @@ class ESP32BLE : public Component {
void advertising_init_(); void advertising_init_();
#endif #endif
// BLE uses the core wake_loop_threadsafe() mechanism to wake the main event loop
// from BLE tasks. This enables low-latency (~12μs) event processing instead of
// waiting for select() timeout (0-16ms). The wake socket is shared with other
// components that need this functionality.
private: private:
template<typename... Args> friend void enqueue_ble_event(Args... args); template<typename... Args> friend void enqueue_ble_event(Args... args);
@@ -214,17 +209,17 @@ extern ESP32BLE *global_ble;
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> { template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
public: public:
bool check(const Ts &...x) override { return global_ble->is_active(); } bool check(Ts... x) override { return global_ble->is_active(); }
}; };
template<typename... Ts> class BLEEnableAction : public Action<Ts...> { template<typename... Ts> class BLEEnableAction : public Action<Ts...> {
public: public:
void play(const Ts &...x) override { global_ble->enable(); } void play(Ts... x) override { global_ble->enable(); }
}; };
template<typename... Ts> class BLEDisableAction : public Action<Ts...> { template<typename... Ts> class BLEDisableAction : public Action<Ts...> {
public: public:
void play(const Ts &...x) override { global_ble->disable(); } void play(Ts... x) override { global_ble->disable(); }
}; };
} // namespace esphome::esp32_ble } // namespace esphome::esp32_ble

View File

@@ -71,7 +71,7 @@ template<typename... Ts> class BLECharacteristicSetValueAction : public Action<T
BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {} BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer) TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(const Ts &...x) override { void play(Ts... x) override {
// If the listener is already set, do nothing // If the listener is already set, do nothing
if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_)) if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_))
return; return;
@@ -96,7 +96,7 @@ template<typename... Ts> class BLECharacteristicSetValueAction : public Action<T
template<typename... Ts> class BLECharacteristicNotifyAction : public Action<Ts...> { template<typename... Ts> class BLECharacteristicNotifyAction : public Action<Ts...> {
public: public:
BLECharacteristicNotifyAction(BLECharacteristic *characteristic) : parent_(characteristic) {} BLECharacteristicNotifyAction(BLECharacteristic *characteristic) : parent_(characteristic) {}
void play(const Ts &...x) override { void play(Ts... x) override {
#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION
// Call the pre-notify event // Call the pre-notify event
BLECharacteristicSetValueActionManager::get_instance()->emit_pre_notify(this->parent_); BLECharacteristicSetValueActionManager::get_instance()->emit_pre_notify(this->parent_);
@@ -116,7 +116,7 @@ template<typename... Ts> class BLEDescriptorSetValueAction : public Action<Ts...
BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {} BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer) TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); } void play(Ts... x) override { this->parent_->set_value(this->buffer_.value(x...)); }
protected: protected:
BLEDescriptor *parent_; BLEDescriptor *parent_;

View File

@@ -96,7 +96,7 @@ template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> {
public: public:
ESP32BLEStartScanAction(ESP32BLETracker *parent) : parent_(parent) {} ESP32BLEStartScanAction(ESP32BLETracker *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(bool, continuous) TEMPLATABLE_VALUE(bool, continuous)
void play(const Ts &...x) override { void play(Ts... x) override {
this->parent_->set_scan_continuous(this->continuous_.value(x...)); this->parent_->set_scan_continuous(this->continuous_.value(x...));
this->parent_->start_scan(); this->parent_->start_scan();
} }
@@ -107,7 +107,7 @@ template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> {
template<typename... Ts> class ESP32BLEStopScanAction : public Action<Ts...>, public Parented<ESP32BLETracker> { template<typename... Ts> class ESP32BLEStopScanAction : public Action<Ts...>, public Parented<ESP32BLETracker> {
public: public:
void play(const Ts &...x) override { this->parent_->stop_scan(); } void play(Ts... x) override { this->parent_->stop_scan(); }
}; };
} // namespace esphome::esp32_ble_tracker } // namespace esphome::esp32_ble_tracker

View File

@@ -4,7 +4,6 @@ from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import i2c from esphome.components import i2c
from esphome.components.esp32 import add_idf_component from esphome.components.esp32 import add_idf_component
from esphome.components.psram import DOMAIN as psram_domain
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BRIGHTNESS, CONF_BRIGHTNESS,
@@ -27,9 +26,10 @@ import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["camera"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
AUTO_LOAD = ["camera", "psram"]
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData") ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData")
@@ -163,14 +163,6 @@ CONF_ON_IMAGE = "on_image"
camera_range_param = cv.int_range(min=-2, max=2) camera_range_param = cv.int_range(min=-2, max=2)
def validate_fb_location_(value):
validator = cv.enum(ENUM_FB_LOCATION, upper=True)
if value.lower() == psram_domain:
validator = cv.All(validator, cv.requires_component(psram_domain))
return validator(value)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.ENTITY_BASE_SCHEMA.extend( cv.ENTITY_BASE_SCHEMA.extend(
{ {
@@ -244,9 +236,9 @@ CONFIG_SCHEMA = cv.All(
cv.framerate, cv.Range(min=0, max=1) cv.framerate, cv.Range(min=0, max=1)
), ),
cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2),
cv.Optional( cv.Optional(CONF_FRAME_BUFFER_LOCATION, default="PSRAM"): cv.enum(
CONF_FRAME_BUFFER_LOCATION, default="PSRAM" ENUM_FB_LOCATION, upper=True
): validate_fb_location_, ),
cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(

View File

@@ -1,78 +0,0 @@
import hashlib
from typing import Any
import esphome.codegen as cg
from esphome.components import esp32, update
import esphome.config_validation as cv
from esphome.const import CONF_PATH, CONF_RAW_DATA_ID
from esphome.core import CORE, HexInt
CODEOWNERS = ["@swoboda1337"]
AUTO_LOAD = ["sha256", "watchdog"]
DEPENDENCIES = ["esp32_hosted"]
CONF_SHA256 = "sha256"
esp32_hosted_ns = cg.esphome_ns.namespace("esp32_hosted")
Esp32HostedUpdate = esp32_hosted_ns.class_(
"Esp32HostedUpdate", update.UpdateEntity, cg.Component
)
def _validate_sha256(value: Any) -> str:
value = cv.string_strict(value)
if len(value) != 64:
raise cv.Invalid("SHA256 must be 64 hexadecimal characters")
try:
bytes.fromhex(value)
except ValueError as e:
raise cv.Invalid(f"SHA256 must be valid hexadecimal: {e}") from e
return value
CONFIG_SCHEMA = cv.All(
update.update_schema(Esp32HostedUpdate, device_class="firmware").extend(
{
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
cv.Required(CONF_PATH): cv.file_,
cv.Required(CONF_SHA256): _validate_sha256,
}
),
esp32.only_on_variant(
supported=[
esp32.const.VARIANT_ESP32H2,
esp32.const.VARIANT_ESP32P4,
]
),
)
def _validate_firmware(config: dict[str, Any]) -> None:
path = CORE.relative_config_path(config[CONF_PATH])
with open(path, "rb") as f:
firmware_data = f.read()
calculated = hashlib.sha256(firmware_data).hexdigest()
expected = config[CONF_SHA256].lower()
if calculated != expected:
raise cv.Invalid(
f"SHA256 mismatch for {config[CONF_PATH]}: expected {expected}, got {calculated}"
)
FINAL_VALIDATE_SCHEMA = _validate_firmware
async def to_code(config: dict[str, Any]) -> None:
var = await update.new_update(config)
path = config[CONF_PATH]
with open(CORE.relative_config_path(path), "rb") as f:
firmware_data = f.read()
rhs = [HexInt(x) for x in firmware_data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
sha256_bytes = bytes.fromhex(config[CONF_SHA256])
cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes]))
cg.add(var.set_firmware_data(prog_arr))
cg.add(var.set_firmware_size(len(firmware_data)))
await cg.register_component(var, config)

View File

@@ -1,164 +0,0 @@
#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4)
#include "esp32_hosted_update.h"
#include "esphome/components/watchdog/watchdog.h"
#include "esphome/components/sha256/sha256.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include <esp_image_format.h>
#include <esp_app_desc.h>
#include <esp_hosted.h>
extern "C" {
#include <esp_hosted_ota.h>
}
namespace esphome::esp32_hosted {
static const char *const TAG = "esp32_hosted.update";
// older coprocessor firmware versions have a 1500-byte limit per RPC call
constexpr size_t CHUNK_SIZE = 1500;
void Esp32HostedUpdate::setup() {
this->update_info_.title = "ESP32 Hosted Coprocessor";
// get coprocessor version
esp_hosted_coprocessor_fwver_t ver_info;
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
} else {
this->update_info_.current_version = "unknown";
}
ESP_LOGD(TAG, "Coprocessor version: %s", this->update_info_.current_version.c_str());
// get image version
const int app_desc_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t);
if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) {
esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset);
if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) {
ESP_LOGD(TAG, "Firmware version: %s", app_desc->version);
ESP_LOGD(TAG, "Project name: %s", app_desc->project_name);
ESP_LOGD(TAG, "Build date: %s", app_desc->date);
ESP_LOGD(TAG, "Build time: %s", app_desc->time);
ESP_LOGD(TAG, "IDF version: %s", app_desc->idf_ver);
this->update_info_.latest_version = app_desc->version;
if (this->update_info_.latest_version != this->update_info_.current_version) {
this->state_ = update::UPDATE_STATE_AVAILABLE;
} else {
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
} else {
ESP_LOGW(TAG, "Invalid app description magic word: 0x%08x (expected 0x%08x)", app_desc->magic_word,
ESP_APP_DESC_MAGIC_WORD);
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
} else {
ESP_LOGW(TAG, "Firmware too small to contain app description");
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
// publish state
this->status_clear_error();
this->publish_state();
}
void Esp32HostedUpdate::dump_config() {
ESP_LOGCONFIG(TAG,
"ESP32 Hosted Update:\n"
" Current Version: %s\n"
" Latest Version: %s\n"
" Latest Size: %zu bytes",
this->update_info_.current_version.c_str(), this->update_info_.latest_version.c_str(),
this->firmware_size_);
}
void Esp32HostedUpdate::perform(bool force) {
if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
ESP_LOGW(TAG, "Update not available");
return;
}
if (this->firmware_data_ == nullptr || this->firmware_size_ == 0) {
ESP_LOGE(TAG, "No firmware data available");
return;
}
sha256::SHA256 hasher;
hasher.init();
hasher.add(this->firmware_data_, this->firmware_size_);
hasher.calculate();
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
this->status_set_error("SHA256 verification failed");
this->publish_state();
return;
}
ESP_LOGI(TAG, "Starting OTA update (%zu bytes)", this->firmware_size_);
watchdog::WatchdogManager watchdog(20000);
update::UpdateState prev_state = this->state_;
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = false;
this->publish_state();
esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error("Failed to begin OTA");
this->publish_state();
return;
}
uint8_t chunk[CHUNK_SIZE];
const uint8_t *data_ptr = this->firmware_data_;
size_t remaining = this->firmware_size_;
while (remaining > 0) {
size_t chunk_size = std::min(remaining, static_cast<size_t>(CHUNK_SIZE));
memcpy(chunk, data_ptr, chunk_size);
err = esp_hosted_slave_ota_write(chunk, chunk_size); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT
this->state_ = prev_state;
this->status_set_error("Failed to write OTA data");
this->publish_state();
return;
}
data_ptr += chunk_size;
remaining -= chunk_size;
App.feed_wdt();
}
err = esp_hosted_slave_ota_end(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error("Failed to end OTA");
this->publish_state();
return;
}
// activate new firmware
err = esp_hosted_slave_ota_activate(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error("Failed to activate OTA");
this->publish_state();
return;
}
// update state
ESP_LOGI(TAG, "OTA update successful");
this->state_ = update::UPDATE_STATE_NO_UPDATE;
this->status_clear_error();
this->publish_state();
// schedule a restart to ensure everything is in sync
ESP_LOGI(TAG, "Restarting in 1 second");
this->set_timeout(1000, []() { App.safe_reboot(); });
}
} // namespace esphome::esp32_hosted
#endif

View File

@@ -1,32 +0,0 @@
#pragma once
#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4)
#include "esphome/core/component.h"
#include "esphome/components/update/update_entity.h"
#include <array>
namespace esphome::esp32_hosted {
class Esp32HostedUpdate : public update::UpdateEntity, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void perform(bool force) override;
void check() override {}
void set_firmware_data(const uint8_t *data) { this->firmware_data_ = data; }
void set_firmware_size(size_t size) { this->firmware_size_ = size; }
void set_firmware_sha256(const std::array<uint8_t, 32> &sha256) { this->firmware_sha256_ = sha256; }
protected:
const uint8_t *firmware_data_{nullptr};
size_t firmware_size_{0};
std::array<uint8_t, 32> firmware_sha256_;
};
} // namespace esphome::esp32_hosted
#endif

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