1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-12 02:32:15 +00:00

Compare commits

...

64 Commits

Author SHA1 Message Date
J. Nick Koston
b4c0d89550 [logger] RP2040: Use write() with known length instead of println() 2025-12-21 12:20:50 -10:00
J. Nick Koston
1839f3f927 Merge branch 'dev' of https://github.com/esphome/esphome into dev 2025-12-21 12:12:44 -10:00
Douwe
39926909af [water_heater] (1/4) Implement API/Core/component for new water_heater component (#12498)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-21 11:36:34 -10:00
J. Nick Koston
637e032528 [esp32_camera] Throttle frame logging to reduce overhead and improve throughput (#12586) 2025-12-21 09:04:43 -10:00
Anna Oake
d89eaf5bf6 [cc1101] Fix option defaults and move them to YAML (#12608) 2025-12-21 13:04:17 -05:00
J. Nick Koston
bf617c3279 [web_server] Replace str_sprintf with stack buffers (#12592) 2025-12-21 07:32:05 -10:00
J. Nick Koston
c70eab931e [api] Add zero-copy support for Home Assistant state response messages (#12585) 2025-12-21 07:31:54 -10:00
J. Nick Koston
a799ac6488 [syslog] Eliminate heap allocations in log path (#12589) 2025-12-21 07:10:27 -10:00
polyfloyd
5a36cea5ec Add nix files to gitignore (#12604) 2025-12-21 09:26:03 -05:00
J. Nick Koston
60756db06d [syslog] Use C++17 nested namespace syntax (#12594) 2025-12-21 02:47:37 -06:00
Keith Burzinski
2113858f89 [sprinkler] Squash a few bugs + minor optimization (#12436) 2025-12-21 02:45:24 -06:00
Steve
afbec0ba78 Merge branch 'dev' into dev 2025-12-21 12:46:44 +11:00
dependabot[bot]
e89fe9b945 Bump aioesphomeapi from 43.4.0 to 43.5.0 (#12599)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 23:59:48 +00:00
dependabot[bot]
f1362cd9fe Bump aioesphomeapi from 43.3.0 to 43.4.0 (#12597)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 22:37:10 +00:00
Frédéric Metrich
bf554a58ef [const] Add CONF_ON_DATA and consolidate definitions (#12595)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 12:17:09 -10:00
J. Nick Koston
644e806afd [zwave_proxy] Add missing USE_API guards for clang-tidy (#12590) 2025-12-20 10:40:43 -10:00
Leo Bergolth
6c2d255230 send NIL ("-") as timestamp if time source is not valid (#12588) 2025-12-20 10:04:59 -10:00
Stuart Parmenter
6f3bfc2060 [hub75] Bump esp-hub75 version to 0.1.7 (#12564) 2025-12-20 13:18:20 -05:00
J. Nick Koston
40eb898814 [api] Add zero-copy support for noise encryption key requests (#12405) 2025-12-20 06:47:30 -10:00
J. Nick Koston
64269334ce [text_sensor] Avoid string copies in callbacks by passing const ref (#12503)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-20 06:46:13 -10:00
Eduard Llull
121375ff39 [display_menu_base] Call on_value_ after updating the select (#12584) 2025-12-20 10:59:14 -05:00
J. Nick Koston
48cdf9e036 [tests] Fix race condition in alarm control panel state transitions test (#12581) 2025-12-20 10:47:29 -05:00
kabongsteve
97f311e84f Correct code update 2025-12-20 18:35:28 +11:00
kabongsteve
5528174590 Fix second location and compatibility issue 2025-12-20 18:01:15 +11:00
pre-commit-ci-lite[bot]
3ad6d860f0 [pre-commit.ci lite] apply automatic fixes 2025-12-20 05:54:51 +00:00
Steve
51ea938eea Merge branch 'dev' into dev 2025-12-20 16:53:01 +11:00
kabongsteve
2a1c24ba15 Update code after code review 2025-12-20 16:52:31 +11:00
J. Nick Koston
3e313014e1 [core] Migrate entities to use lazy callbacks (#12580) 2025-12-19 19:04:21 -10:00
Martin Ebner
be6c1e4ec0 [sen5x][sgp4x] Move configuration keys from SEN5x and SGP4x to const.py (#12567)
Co-authored-by: Martin Ebner <martinebner@me.com>
2025-12-19 21:29:02 -05:00
Keith Burzinski
730bf206de [wifi] Fix for wifi_info when static IP is configured (#12576) 2025-12-19 21:25:16 -05:00
J. Nick Koston
c9fccdff25 [fan] Add zero-copy support for API preset mode commands (#12404)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 22:05:52 +00:00
J. Nick Koston
ada6c42f3f [alarm_control_panel] Remove redundant per-state callbacks (#12171)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 11:48:14 -10:00
J. Nick Koston
988b888c63 [ota] Replace std::function callbacks with listener interface (#12167) 2025-12-19 11:19:07 -10:00
J. Nick Koston
940afdbb12 [climate] Add zero-copy support for API custom fan mode and preset commands (#12402) 2025-12-19 11:18:50 -10:00
J. Nick Koston
81e91c2a8f [esp32_ble] Add stack-based UUID formatting to avoid heap allocations (#12510) 2025-12-19 11:18:32 -10:00
J. Nick Koston
ebc3d28ade [wifi] Replace optional with sentinel values to reduce RAM and clarify API (#12446) 2025-12-19 11:18:15 -10:00
Rene Guca
25cebedcfc [dht] Fix "Falling edge for bit 39 failed!" for Sonoff THS01 (#9225)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-19 15:42:39 -05:00
dependabot[bot]
98ed679b19 Bump ruff from 0.14.9 to 0.14.10 (#12572)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-19 19:22:56 +00:00
dependabot[bot]
59b38d79b4 Bump docker/setup-buildx-action from 3.11.1 to 3.12.0 in the docker-actions group (#12574)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 09:18:52 -10:00
dependabot[bot]
26c16f4ca2 Bump voluptuous from 0.15.2 to 0.16.0 (#12573)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 09:18:33 -10:00
Jas Strong
940e619481 [aqi, hm3301, pmsx003] Air Quality Index improvements (#12203)
Co-authored-by: jas <jas@asspa.in>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-12-19 13:42:11 -05:00
Jonathan Swoboda
eaca81c3ab Merge branch 'release' into dev 2025-12-19 10:53:18 -05:00
Jonathan Swoboda
93e38f2608 Merge pull request #12569 from esphome/bump-2025.12.1
2025.12.1
2025-12-19 10:53:05 -05:00
Jonathan Swoboda
3a888326d8 Bump version to 2025.12.1 2025-12-19 10:13:35 -05:00
Keith Burzinski
f0d0ea60a7 [esp32_ble, esp32_ble_tracker] Fix crash, error messages when ble.disable called during boot (#12560) 2025-12-19 10:13:35 -05:00
Jonathan Swoboda
7ca11764ab [template.alarm_control_panel] Fix compile without binary_sensor (#12548)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-19 10:13:35 -05:00
Jonathan Swoboda
3e38a5e630 [esp32_camera] Fix I2C driver conflict with other components (#12533)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-19 10:13:35 -05:00
Jonathan Swoboda
636be92c97 [bme68x_bsec2_i2c] Add MULTI_CONF support for multiple sensors (#12535)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-19 10:13:35 -05:00
Jack Wilsdon
195b1c6323 [pm1006] Fix "never" update interval detection (#12529) 2025-12-19 10:13:35 -05:00
Anna Oake
7e08092012 [cc1101] Fix default frequencies (#12539) 2025-12-19 10:13:35 -05:00
pixelgrb
f962497db1 [mmc5603] enable AUTO_SR_en to compensate for temperature drift (#12556)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-18 23:13:36 -05:00
Keith Burzinski
7ae3a11d6b [esp32_ble, esp32_ble_tracker] Fix crash, error messages when ble.disable called during boot (#12560) 2025-12-18 19:42:47 -06:00
dependabot[bot]
1c50c2b672 Bump ruamel-yaml from 0.18.16 to 0.18.17 (#12555)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 11:19:19 -10:00
Jonathan Swoboda
41fd1762e9 [core] Deprecate custom_components folder (#12552)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-18 11:46:16 -05:00
J. Nick Koston
2cf6ed2af7 [socket] Refactor socket implementations for memory efficiency and code quality (#12550) 2025-12-18 09:07:35 -07:00
J. Nick Koston
b47b7d43fd [api] Remove unused force parameter from encode_message (#12551) 2025-12-18 09:06:16 -07:00
Jonathan Swoboda
663a4304e0 [libretiny] Fix millis() ambiguity on BK72XX (#12534)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-18 07:50:31 -05:00
Jonathan Swoboda
ca47bad90a [template.alarm_control_panel] Fix compile without binary_sensor (#12548)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 23:34:04 -05:00
J. Nick Koston
4f821a6d76 [wifi] Reduce scan logging to prevent blocking loop during connection (#12544) 2025-12-17 18:21:46 -10:00
J. Nick Koston
426305836d [esp32][libretiny] Avoid duplicate snprintf when syncing preferences (#12542) 2025-12-17 18:16:14 -10:00
dependabot[bot]
1b5af7d21d Bump github/codeql-action from 4.31.8 to 4.31.9 (#12524)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 15:22:19 -10:00
David Woodhouse
9de7df7b5b Add build info to image (#12425)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-18 00:06:52 +00:00
kabongsteve
2eff8850c5 Fix clang-tidy issue 2025-12-17 16:25:03 +11:00
kabongsteve
26c4d749df Fix issue when esp_http_client_fetch_headers returns error 2025-12-17 15:50:41 +11:00
180 changed files with 4879 additions and 1059 deletions

View File

@@ -1 +1 @@
6857423aecf90accd0a8bf584d36ee094a4938f872447a4efc05a2efc6dc6481
4268ab0b5150f79ab1c317e8f3834c8bb0b4c8122da4f6b1fd67c49d0f2098c9

View File

@@ -49,7 +49,7 @@ jobs:
with:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Set TAG
run: |

View File

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

View File

@@ -99,7 +99,7 @@ jobs:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to docker hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
@@ -178,7 +178,7 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'

4
.gitignore vendored
View File

@@ -91,6 +91,10 @@ venv-*/
# mypy
.mypy_cache/
# nix
/default.nix
/shell.nix
.pioenvs
.piolibdeps
.pio

View File

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

View File

@@ -42,6 +42,7 @@ esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix
esphome/components/apds9306/* @aodrenah
esphome/components/api/* @esphome/core
esphome/components/aqi/* @freekode @jasstrong @ximex
esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr
@@ -536,6 +537,7 @@ esphome/components/version/* @esphome/core
esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher
esphome/components/water_heater/* @dhoeben
esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/web_server/ota/* @esphome/core
esphome/components/web_server_base/* @esphome/core

View File

@@ -518,10 +518,49 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
# Check if firmware was rebuilt and emit build_info + create manifest
_check_and_emit_build_info()
idedata = platformio_api.get_idedata(config)
return 0 if idedata is not None else 1
def _check_and_emit_build_info() -> None:
"""Check if firmware was rebuilt and emit build_info."""
import json
firmware_path = CORE.firmware_bin
build_info_json_path = CORE.relative_build_path("build_info.json")
# Check if both files exist
if not firmware_path.exists() or not build_info_json_path.exists():
return
# Check if firmware is newer than build_info (indicating a relink occurred)
if firmware_path.stat().st_mtime <= build_info_json_path.stat().st_mtime:
return
# Read build_info from JSON
try:
with open(build_info_json_path, encoding="utf-8") as f:
build_info = json.load(f)
except (OSError, json.JSONDecodeError) as e:
_LOGGER.debug("Failed to read build_info: %s", e)
return
config_hash = build_info.get("config_hash")
build_time_str = build_info.get("build_time_str")
if config_hash is None or build_time_str is None:
return
# Emit build_info with human-readable time
_LOGGER.info(
"Build Info: config_hash=0x%08x build_time_str=%s", config_hash, build_time_str
)
def upload_using_esptool(
config: ConfigType, port: str, file: str, speed: int
) -> str | int:

View File

@@ -35,26 +35,12 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state;
// Single state callback - triggers check get_state() for specific states
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) {
this->triggered_callback_.call();
} else if (state == ACP_STATE_ARMING) {
this->arming_callback_.call();
} else if (state == ACP_STATE_PENDING) {
this->pending_callback_.call();
} else if (state == ACP_STATE_ARMED_HOME) {
this->armed_home_callback_.call();
} else if (state == ACP_STATE_ARMED_NIGHT) {
this->armed_night_callback_.call();
} else if (state == ACP_STATE_ARMED_AWAY) {
this->armed_away_callback_.call();
} else if (state == ACP_STATE_DISARMED) {
this->disarmed_callback_.call();
}
// Cleared fires when leaving TRIGGERED state
if (prev_state == ACP_STATE_TRIGGERED) {
this->cleared_callback_.call();
}
@@ -69,34 +55,6 @@ void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback)
this->state_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_triggered_callback(std::function<void()> &&callback) {
this->triggered_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_arming_callback(std::function<void()> &&callback) {
this->arming_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_home_callback(std::function<void()> &&callback) {
this->armed_home_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_night_callback(std::function<void()> &&callback) {
this->armed_night_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_away_callback(std::function<void()> &&callback) {
this->armed_away_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_pending_callback(std::function<void()> &&callback) {
this->pending_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_disarmed_callback(std::function<void()> &&callback) {
this->disarmed_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
this->cleared_callback_.add(std::move(callback));
}

View File

@@ -35,54 +35,13 @@ class AlarmControlPanel : public EntityBase {
*/
void publish_state(AlarmControlPanelState state);
/** Add a callback for when the state of the alarm_control_panel changes
/** Add a callback for when the state of the alarm_control_panel changes.
* Triggers can check get_state() to determine the new state.
*
* @param callback The callback function
*/
void add_on_state_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel chanes to triggered
*
* @param callback The callback function
*/
void add_on_triggered_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel chanes to arming
*
* @param callback The callback function
*/
void add_on_arming_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to pending
*
* @param callback The callback function
*/
void add_on_pending_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_home
*
* @param callback The callback function
*/
void add_on_armed_home_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_night
*
* @param callback The callback function
*/
void add_on_armed_night_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_away
*
* @param callback The callback function
*/
void add_on_armed_away_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to disarmed
*
* @param callback The callback function
*/
void add_on_disarmed_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel clears from triggered
*
* @param callback The callback function
@@ -172,28 +131,14 @@ class AlarmControlPanel : public EntityBase {
uint32_t last_update_;
// the call control function
virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback
CallbackManager<void()> state_callback_{};
// trigger callback
CallbackManager<void()> triggered_callback_{};
// arming callback
CallbackManager<void()> arming_callback_{};
// pending callback
CallbackManager<void()> pending_callback_{};
// armed_home callback
CallbackManager<void()> armed_home_callback_{};
// armed_night callback
CallbackManager<void()> armed_night_callback_{};
// armed_away callback
CallbackManager<void()> armed_away_callback_{};
// disarmed callback
CallbackManager<void()> disarmed_callback_{};
// clear callback
CallbackManager<void()> cleared_callback_{};
// state callback - triggers check get_state() for specific state
LazyCallbackManager<void()> state_callback_{};
// clear callback - fires when leaving TRIGGERED state
LazyCallbackManager<void()> cleared_callback_{};
// chime callback
CallbackManager<void()> chime_callback_{};
LazyCallbackManager<void()> chime_callback_{};
// ready callback
CallbackManager<void()> ready_callback_{};
LazyCallbackManager<void()> ready_callback_{};
};
} // namespace alarm_control_panel

View File

@@ -6,6 +6,7 @@
namespace esphome {
namespace alarm_control_panel {
/// Trigger on any state change
class StateTrigger : public Trigger<> {
public:
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -13,55 +14,30 @@ class StateTrigger : public Trigger<> {
}
};
class TriggeredTrigger : public Trigger<> {
/// Template trigger that fires when entering a specific state
template<AlarmControlPanelState State> class StateEnterTrigger : public Trigger<> {
public:
explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); });
explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {
alarm_control_panel->add_on_state_callback([this]() {
if (this->alarm_control_panel_->get_state() == State)
this->trigger();
});
}
protected:
AlarmControlPanel *alarm_control_panel_;
};
class ArmingTrigger : public Trigger<> {
public:
explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_arming_callback([this]() { this->trigger(); });
}
};
class PendingTrigger : public Trigger<> {
public:
explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_pending_callback([this]() { this->trigger(); });
}
};
class ArmedHomeTrigger : public Trigger<> {
public:
explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_home_callback([this]() { this->trigger(); });
}
};
class ArmedNightTrigger : public Trigger<> {
public:
explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_night_callback([this]() { this->trigger(); });
}
};
class ArmedAwayTrigger : public Trigger<> {
public:
explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_away_callback([this]() { this->trigger(); });
}
};
class DisarmedTrigger : public Trigger<> {
public:
explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_disarmed_callback([this]() { this->trigger(); });
}
};
// Type aliases for state-specific triggers
using TriggeredTrigger = StateEnterTrigger<ACP_STATE_TRIGGERED>;
using ArmingTrigger = StateEnterTrigger<ACP_STATE_ARMING>;
using PendingTrigger = StateEnterTrigger<ACP_STATE_PENDING>;
using ArmedHomeTrigger = StateEnterTrigger<ACP_STATE_ARMED_HOME>;
using ArmedNightTrigger = StateEnterTrigger<ACP_STATE_ARMED_NIGHT>;
using ArmedAwayTrigger = StateEnterTrigger<ACP_STATE_ARMED_AWAY>;
using DisarmedTrigger = StateEnterTrigger<ACP_STATE_DISARMED>;
/// Trigger when leaving TRIGGERED state (alarm cleared)
class ClearedTrigger : public Trigger<> {
public:
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -69,6 +45,7 @@ class ClearedTrigger : public Trigger<> {
}
};
/// Trigger on chime event (zone opened while disarmed)
class ChimeTrigger : public Trigger<> {
public:
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -76,6 +53,7 @@ class ChimeTrigger : public Trigger<> {
}
};
/// Trigger on ready state change
class ReadyTrigger : public Trigger<> {
public:
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {

View File

@@ -477,7 +477,7 @@ message FanCommandRequest {
bool has_speed_level = 10;
int32 speed_level = 11;
bool has_preset_mode = 12;
string preset_mode = 13;
string preset_mode = 13 [(pointer_to_buffer) = true];
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
}
@@ -747,7 +747,7 @@ message NoiseEncryptionSetKeyRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_API_NOISE";
bytes key = 1;
bytes key = 1 [(pointer_to_buffer) = true];
}
message NoiseEncryptionSetKeyResponse {
@@ -824,9 +824,9 @@ message HomeAssistantStateResponse {
option (no_delay) = true;
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
string entity_id = 1;
string state = 2;
string attribute = 3;
string entity_id = 1 [(pointer_to_buffer) = true];
string state = 2 [(pointer_to_buffer) = true];
string attribute = 3 [(pointer_to_buffer) = true];
}
// ==================== IMPORT TIME ====================
@@ -1091,16 +1091,95 @@ message ClimateCommandRequest {
bool has_swing_mode = 14;
ClimateSwingMode swing_mode = 15;
bool has_custom_fan_mode = 16;
string custom_fan_mode = 17;
string custom_fan_mode = 17 [(pointer_to_buffer) = true];
bool has_preset = 18;
ClimatePreset preset = 19;
bool has_custom_preset = 20;
string custom_preset = 21;
string custom_preset = 21 [(pointer_to_buffer) = true];
bool has_target_humidity = 22;
float target_humidity = 23;
uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== WATER_HEATER ====================
enum WaterHeaterMode {
WATER_HEATER_MODE_OFF = 0;
WATER_HEATER_MODE_ECO = 1;
WATER_HEATER_MODE_ELECTRIC = 2;
WATER_HEATER_MODE_PERFORMANCE = 3;
WATER_HEATER_MODE_HIGH_DEMAND = 4;
WATER_HEATER_MODE_HEAT_PUMP = 5;
WATER_HEATER_MODE_GAS = 6;
}
message ListEntitiesWaterHeaterResponse {
option (id) = 132;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
float min_temperature = 8;
float max_temperature = 9;
float target_temperature_step = 10;
repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"];
// Bitmask of WaterHeaterFeature flags
uint32 supported_features = 12;
}
message WaterHeaterStateResponse {
option (id) = 133;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
fixed32 key = 1;
float current_temperature = 2;
float target_temperature = 3;
WaterHeaterMode mode = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// Bitmask of current state flags (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// Bitmask for WaterHeaterCommandRequest.has_fields
enum WaterHeaterCommandHasField {
WATER_HEATER_COMMAND_HAS_NONE = 0;
WATER_HEATER_COMMAND_HAS_MODE = 1;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
WATER_HEATER_COMMAND_HAS_STATE = 4;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
}
message WaterHeaterCommandRequest {
option (id) = 134;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
// Bitmask of which fields are set (see WaterHeaterCommandHasField)
uint32 has_fields = 2;
WaterHeaterMode mode = 3;
float target_temperature = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// State flags bitmask (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// ==================== NUMBER ====================
enum NumberMode {
NUMBER_MODE_AUTO = 0;

View File

@@ -42,6 +42,9 @@
#ifdef USE_ZWAVE_PROXY
#include "esphome/components/zwave_proxy/zwave_proxy.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
namespace esphome::api {
@@ -447,7 +450,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
if (msg.has_direction)
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
if (msg.has_preset_mode)
call.set_preset_mode(msg.preset_mode);
call.set_preset_mode(reinterpret_cast<const char *>(msg.preset_mode), msg.preset_mode_len);
call.perform();
}
#endif
@@ -712,11 +715,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
if (msg.has_fan_mode)
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
if (msg.has_custom_fan_mode)
call.set_fan_mode(msg.custom_fan_mode);
call.set_fan_mode(reinterpret_cast<const char *>(msg.custom_fan_mode), msg.custom_fan_mode_len);
if (msg.has_preset)
call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
if (msg.has_custom_preset)
call.set_preset(msg.custom_preset);
call.set_preset(reinterpret_cast<const char *>(msg.custom_preset), msg.custom_preset_len);
if (msg.has_swing_mode)
call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
call.perform();
@@ -1306,6 +1309,57 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
}
#endif
#ifdef USE_WATER_HEATER
bool APIConnection::send_water_heater_state(water_heater::WaterHeater *water_heater) {
return this->send_message_smart_(water_heater, &APIConnection::try_send_water_heater_state,
WaterHeaterStateResponse::MESSAGE_TYPE, WaterHeaterStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
auto *wh = static_cast<water_heater::WaterHeater *>(entity);
WaterHeaterStateResponse resp;
resp.mode = static_cast<enums::WaterHeaterMode>(wh->get_mode());
resp.current_temperature = wh->get_current_temperature();
resp.target_temperature = wh->get_target_temperature();
resp.target_temperature_low = wh->get_target_temperature_low();
resp.target_temperature_high = wh->get_target_temperature_high();
resp.state = wh->get_state();
resp.key = wh->get_object_id_hash();
return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
auto *wh = static_cast<water_heater::WaterHeater *>(entity);
ListEntitiesWaterHeaterResponse msg;
auto traits = wh->get_traits();
msg.min_temperature = traits.get_min_temperature();
msg.max_temperature = traits.get_max_temperature();
msg.target_temperature_step = traits.get_target_temperature_step();
msg.supported_modes = &traits.get_supported_modes();
msg.supported_features = traits.get_feature_flags();
return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size,
is_single);
}
void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE)
call.set_target_temperature(msg.target_temperature);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW)
call.set_target_temperature_low(msg.target_temperature_low);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
call.set_target_temperature_high(msg.target_temperature_high);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) {
call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
}
call.perform();
}
#endif
#ifdef USE_EVENT
void APIConnection::send_event(event::Event *event, const char *event_type) {
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
@@ -1472,7 +1526,10 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
resp.set_esphome_version(ESPHOME_VERSION_REF);
resp.set_compilation_time(App.get_compilation_time_ref());
// Stack buffer for build time string
char build_time_str[Application::BUILD_TIME_STR_SIZE];
App.get_build_time_string(build_time_str);
resp.set_compilation_time(StringRef(build_time_str));
// Manufacturer string - define once, handle ESP8266 PROGMEM separately
#if defined(USE_ESP8266) || defined(USE_ESP32)
@@ -1579,15 +1636,29 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
#ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
for (auto &it : this->parent_->get_state_subs()) {
// Compare entity_id and attribute with message fields
bool entity_match = (strcmp(it.entity_id, msg.entity_id.c_str()) == 0);
bool attribute_match = (it.attribute != nullptr && strcmp(it.attribute, msg.attribute.c_str()) == 0) ||
(it.attribute == nullptr && msg.attribute.empty());
// Skip if entity_id is empty (invalid message)
if (msg.entity_id_len == 0) {
return;
}
if (entity_match && attribute_match) {
it.callback(msg.state);
for (auto &it : this->parent_->get_state_subs()) {
// Compare entity_id: check length matches and content matches
size_t entity_id_len = strlen(it.entity_id);
if (entity_id_len != msg.entity_id_len || memcmp(it.entity_id, msg.entity_id, msg.entity_id_len) != 0) {
continue;
}
// Compare attribute: either both have matching attribute, or both have none
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
if (sub_attr_len != msg.attribute_len ||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute, sub_attr_len) != 0)) {
continue;
}
// Create temporary string for callback (callback takes const std::string &)
// Handle empty state (nullptr with len=0)
std::string state(msg.state_len > 0 ? reinterpret_cast<const char *>(msg.state) : "", msg.state_len);
it.callback(state);
}
}
#endif
@@ -1663,13 +1734,13 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
resp.success = false;
psk_t psk{};
if (msg.key.empty()) {
if (msg.key_len == 0) {
if (this->parent_->clear_noise_psk(true)) {
resp.success = true;
} else {
ESP_LOGW(TAG, "Failed to clear encryption key");
}
} else if (base64_decode(msg.key, psk.data(), psk.size()) != psk.size()) {
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length");
} else if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key");

View File

@@ -176,6 +176,11 @@ class APIConnection final : public APIServerConnection {
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif
#ifdef USE_WATER_HEATER
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
#endif
#ifdef USE_EVENT
void send_event(event::Event *event, const char *event_type);
#endif
@@ -456,6 +461,12 @@ class APIConnection final : public APIServerConnection {
static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_WATER_HEATER
static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_EVENT
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single);

View File

@@ -124,12 +124,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
#endif
#ifdef USE_DEVICES
for (const auto &it : this->devices) {
buffer.encode_message(20, it, true);
buffer.encode_message(20, it);
}
#endif
#ifdef USE_AREAS
for (const auto &it : this->areas) {
buffer.encode_message(21, it, true);
buffer.encode_message(21, it);
}
#endif
#ifdef USE_AREAS
@@ -447,9 +447,12 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
}
bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 13:
this->preset_mode = value.as_string();
case 13: {
// Use raw data directly to avoid allocation
this->preset_mode = value.data();
this->preset_mode_len = value.size();
break;
}
default:
return false;
}
@@ -855,9 +858,12 @@ void SubscribeLogsResponse::calculate_size(ProtoSize &size) const {
#ifdef USE_API_NOISE
bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1:
this->key = value.as_string();
case 1: {
// Use raw data directly to avoid allocation
this->key = value.data();
this->key_len = value.size();
break;
}
default:
return false;
}
@@ -878,13 +884,13 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const {
void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->service_ref_);
for (auto &it : this->data) {
buffer.encode_message(2, it, true);
buffer.encode_message(2, it);
}
for (auto &it : this->data_template) {
buffer.encode_message(3, it, true);
buffer.encode_message(3, it);
}
for (auto &it : this->variables) {
buffer.encode_message(4, it, true);
buffer.encode_message(4, it);
}
buffer.encode_bool(5, this->is_event);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -960,15 +966,24 @@ void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const
}
bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1:
this->entity_id = value.as_string();
case 1: {
// Use raw data directly to avoid allocation
this->entity_id = value.data();
this->entity_id_len = value.size();
break;
case 2:
this->state = value.as_string();
}
case 2: {
// Use raw data directly to avoid allocation
this->state = value.data();
this->state_len = value.size();
break;
case 3:
this->attribute = value.as_string();
}
case 3: {
// Use raw data directly to avoid allocation
this->attribute = value.data();
this->attribute_len = value.size();
break;
}
default:
return false;
}
@@ -1011,7 +1026,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->name_ref_);
buffer.encode_fixed32(2, this->key);
for (auto &it : this->args) {
buffer.encode_message(3, it, true);
buffer.encode_message(3, it);
}
buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response));
}
@@ -1392,12 +1407,18 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
}
bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 17:
this->custom_fan_mode = value.as_string();
case 17: {
// Use raw data directly to avoid allocation
this->custom_fan_mode = value.data();
this->custom_fan_mode_len = value.size();
break;
case 21:
this->custom_preset = value.as_string();
}
case 21: {
// Use raw data directly to avoid allocation
this->custom_preset = value.data();
this->custom_preset_len = value.size();
break;
}
default:
return false;
}
@@ -1426,6 +1447,114 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
return true;
}
#endif
#ifdef USE_WATER_HEATER
void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id_ref_);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name_ref_);
#ifdef USE_ENTITY_ICON
buffer.encode_string(4, this->icon_ref_);
#endif
buffer.encode_bool(5, this->disabled_by_default);
buffer.encode_uint32(6, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
buffer.encode_uint32(7, this->device_id);
#endif
buffer.encode_float(8, this->min_temperature);
buffer.encode_float(9, this->max_temperature);
buffer.encode_float(10, this->target_temperature_step);
for (const auto &it : *this->supported_modes) {
buffer.encode_uint32(11, static_cast<uint32_t>(it), true);
}
buffer.encode_uint32(12, this->supported_features);
}
void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->object_id_ref_.size());
size.add_fixed32(1, this->key);
size.add_length(1, this->name_ref_.size());
#ifdef USE_ENTITY_ICON
size.add_length(1, this->icon_ref_.size());
#endif
size.add_bool(1, this->disabled_by_default);
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
size.add_uint32(1, this->device_id);
#endif
size.add_float(1, this->min_temperature);
size.add_float(1, this->max_temperature);
size.add_float(1, this->target_temperature_step);
if (!this->supported_modes->empty()) {
for (const auto &it : *this->supported_modes) {
size.add_uint32_force(1, static_cast<uint32_t>(it));
}
}
size.add_uint32(1, this->supported_features);
}
void WaterHeaterStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_float(2, this->current_temperature);
buffer.encode_float(3, this->target_temperature);
buffer.encode_uint32(4, static_cast<uint32_t>(this->mode));
#ifdef USE_DEVICES
buffer.encode_uint32(5, this->device_id);
#endif
buffer.encode_uint32(6, this->state);
buffer.encode_float(7, this->target_temperature_low);
buffer.encode_float(8, this->target_temperature_high);
}
void WaterHeaterStateResponse::calculate_size(ProtoSize &size) const {
size.add_fixed32(1, this->key);
size.add_float(1, this->current_temperature);
size.add_float(1, this->target_temperature);
size.add_uint32(1, static_cast<uint32_t>(this->mode));
#ifdef USE_DEVICES
size.add_uint32(1, this->device_id);
#endif
size.add_uint32(1, this->state);
size.add_float(1, this->target_temperature_low);
size.add_float(1, this->target_temperature_high);
}
bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2:
this->has_fields = value.as_uint32();
break;
case 3:
this->mode = static_cast<enums::WaterHeaterMode>(value.as_uint32());
break;
#ifdef USE_DEVICES
case 5:
this->device_id = value.as_uint32();
break;
#endif
case 6:
this->state = value.as_uint32();
break;
default:
return false;
}
return true;
}
bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1:
this->key = value.as_fixed32();
break;
case 4:
this->target_temperature = value.as_float();
break;
case 7:
this->target_temperature_low = value.as_float();
break;
case 8:
this->target_temperature_high = value.as_float();
break;
default:
return false;
}
return true;
}
#endif
#ifdef USE_NUMBER
void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id_ref_);
@@ -1867,7 +1996,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category));
buffer.encode_bool(8, this->supports_pause);
for (auto &it : this->supported_formats) {
buffer.encode_message(9, it, true);
buffer.encode_message(9, it);
}
#ifdef USE_DEVICES
buffer.encode_uint32(10, this->device_id);
@@ -1987,7 +2116,7 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const {
}
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const {
for (uint16_t i = 0; i < this->advertisements_len; i++) {
buffer.encode_message(1, this->advertisements[i], true);
buffer.encode_message(1, this->advertisements[i]);
}
}
void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const {
@@ -2060,7 +2189,7 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(2, this->handle);
buffer.encode_uint32(3, this->properties);
for (auto &it : this->descriptors) {
buffer.encode_message(4, it, true);
buffer.encode_message(4, it);
}
buffer.encode_uint32(5, this->short_uuid);
}
@@ -2081,7 +2210,7 @@ void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const {
}
buffer.encode_uint32(2, this->handle);
for (auto &it : this->characteristics) {
buffer.encode_message(3, it, true);
buffer.encode_message(3, it);
}
buffer.encode_uint32(4, this->short_uuid);
}
@@ -2097,7 +2226,7 @@ void BluetoothGATTService::calculate_size(ProtoSize &size) const {
void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint64(1, this->address);
for (auto &it : this->services) {
buffer.encode_message(2, it, true);
buffer.encode_message(2, it);
}
}
void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const {
@@ -2557,7 +2686,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL
}
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->available_wake_words) {
buffer.encode_message(1, it, true);
buffer.encode_message(1, it);
}
for (const auto &it : *this->active_wake_words) {
buffer.encode_string(2, it, true);

View File

@@ -129,6 +129,25 @@ enum ClimatePreset : uint32_t {
CLIMATE_PRESET_ACTIVITY = 7,
};
#endif
#ifdef USE_WATER_HEATER
enum WaterHeaterMode : uint32_t {
WATER_HEATER_MODE_OFF = 0,
WATER_HEATER_MODE_ECO = 1,
WATER_HEATER_MODE_ELECTRIC = 2,
WATER_HEATER_MODE_PERFORMANCE = 3,
WATER_HEATER_MODE_HIGH_DEMAND = 4,
WATER_HEATER_MODE_HEAT_PUMP = 5,
WATER_HEATER_MODE_GAS = 6,
};
#endif
enum WaterHeaterCommandHasField : uint32_t {
WATER_HEATER_COMMAND_HAS_NONE = 0,
WATER_HEATER_COMMAND_HAS_MODE = 1,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2,
WATER_HEATER_COMMAND_HAS_STATE = 4,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
};
#ifdef USE_NUMBER
enum NumberMode : uint32_t {
NUMBER_MODE_AUTO = 0,
@@ -765,7 +784,7 @@ class FanStateResponse final : public StateResponseProtoMessage {
class FanCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 31;
static constexpr uint8_t ESTIMATED_SIZE = 38;
static constexpr uint8_t ESTIMATED_SIZE = 48;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "fan_command_request"; }
#endif
@@ -778,7 +797,8 @@ class FanCommandRequest final : public CommandProtoMessage {
bool has_speed_level{false};
int32_t speed_level{0};
bool has_preset_mode{false};
std::string preset_mode{};
const uint8_t *preset_mode{nullptr};
uint16_t preset_mode_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1053,11 +1073,12 @@ class SubscribeLogsResponse final : public ProtoMessage {
class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 124;
static constexpr uint8_t ESTIMATED_SIZE = 9;
static constexpr uint8_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "noise_encryption_set_key_request"; }
#endif
std::string key{};
const uint8_t *key{nullptr};
uint16_t key_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1201,13 +1222,16 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
class HomeAssistantStateResponse final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 40;
static constexpr uint8_t ESTIMATED_SIZE = 27;
static constexpr uint8_t ESTIMATED_SIZE = 57;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "home_assistant_state_response"; }
#endif
std::string entity_id{};
std::string state{};
std::string attribute{};
const uint8_t *entity_id{nullptr};
uint16_t entity_id_len{0};
const uint8_t *state{nullptr};
uint16_t state_len{0};
const uint8_t *attribute{nullptr};
uint16_t attribute_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1475,7 +1499,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
class ClimateCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 48;
static constexpr uint8_t ESTIMATED_SIZE = 84;
static constexpr uint8_t ESTIMATED_SIZE = 104;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "climate_command_request"; }
#endif
@@ -1492,11 +1516,13 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool has_swing_mode{false};
enums::ClimateSwingMode swing_mode{};
bool has_custom_fan_mode{false};
std::string custom_fan_mode{};
const uint8_t *custom_fan_mode{nullptr};
uint16_t custom_fan_mode_len{0};
bool has_preset{false};
enums::ClimatePreset preset{};
bool has_custom_preset{false};
std::string custom_preset{};
const uint8_t *custom_preset{nullptr};
uint16_t custom_preset_len{0};
bool has_target_humidity{false};
float target_humidity{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1509,6 +1535,70 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_WATER_HEATER
class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 132;
static constexpr uint8_t ESTIMATED_SIZE = 63;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_water_heater_response"; }
#endif
float min_temperature{0.0f};
float max_temperature{0.0f};
float target_temperature_step{0.0f};
const water_heater::WaterHeaterModeMask *supported_modes{};
uint32_t supported_features{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class WaterHeaterStateResponse final : public StateResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 133;
static constexpr uint8_t ESTIMATED_SIZE = 35;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "water_heater_state_response"; }
#endif
float current_temperature{0.0f};
float target_temperature{0.0f};
enums::WaterHeaterMode mode{};
uint32_t state{0};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class WaterHeaterCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 134;
static constexpr uint8_t ESTIMATED_SIZE = 34;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "water_heater_command_request"; }
#endif
uint32_t has_fields{0};
enums::WaterHeaterMode mode{};
float target_temperature{0.0f};
uint32_t state{0};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_NUMBER
class ListEntitiesNumberResponse final : public InfoResponseProtoMessage {
public:

View File

@@ -348,6 +348,47 @@ template<> const char *proto_enum_to_string<enums::ClimatePreset>(enums::Climate
}
}
#endif
#ifdef USE_WATER_HEATER
template<> const char *proto_enum_to_string<enums::WaterHeaterMode>(enums::WaterHeaterMode value) {
switch (value) {
case enums::WATER_HEATER_MODE_OFF:
return "WATER_HEATER_MODE_OFF";
case enums::WATER_HEATER_MODE_ECO:
return "WATER_HEATER_MODE_ECO";
case enums::WATER_HEATER_MODE_ELECTRIC:
return "WATER_HEATER_MODE_ELECTRIC";
case enums::WATER_HEATER_MODE_PERFORMANCE:
return "WATER_HEATER_MODE_PERFORMANCE";
case enums::WATER_HEATER_MODE_HIGH_DEMAND:
return "WATER_HEATER_MODE_HIGH_DEMAND";
case enums::WATER_HEATER_MODE_HEAT_PUMP:
return "WATER_HEATER_MODE_HEAT_PUMP";
case enums::WATER_HEATER_MODE_GAS:
return "WATER_HEATER_MODE_GAS";
default:
return "UNKNOWN";
}
}
#endif
template<>
const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::WaterHeaterCommandHasField value) {
switch (value) {
case enums::WATER_HEATER_COMMAND_HAS_NONE:
return "WATER_HEATER_COMMAND_HAS_NONE";
case enums::WATER_HEATER_COMMAND_HAS_MODE:
return "WATER_HEATER_COMMAND_HAS_MODE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE";
case enums::WATER_HEATER_COMMAND_HAS_STATE:
return "WATER_HEATER_COMMAND_HAS_STATE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH";
default:
return "UNKNOWN";
}
}
#ifdef USE_NUMBER
template<> const char *proto_enum_to_string<enums::NumberMode>(enums::NumberMode value) {
switch (value) {
@@ -923,7 +964,9 @@ void FanCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_speed_level", this->has_speed_level);
dump_field(out, "speed_level", this->speed_level);
dump_field(out, "has_preset_mode", this->has_preset_mode);
dump_field(out, "preset_mode", this->preset_mode);
out.append(" preset_mode: ");
out.append(format_hex_pretty(this->preset_mode, this->preset_mode_len));
out.append("\n");
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -1113,7 +1156,7 @@ void SubscribeLogsResponse::dump_to(std::string &out) const {
void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest");
out.append(" key: ");
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->key.data()), this->key.size()));
out.append(format_hex_pretty(this->key, this->key_len));
out.append("\n");
}
void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); }
@@ -1182,9 +1225,15 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
}
void HomeAssistantStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeAssistantStateResponse");
dump_field(out, "entity_id", this->entity_id);
dump_field(out, "state", this->state);
dump_field(out, "attribute", this->attribute);
out.append(" entity_id: ");
out.append(format_hex_pretty(this->entity_id, this->entity_id_len));
out.append("\n");
out.append(" state: ");
out.append(format_hex_pretty(this->state, this->state_len));
out.append("\n");
out.append(" attribute: ");
out.append(format_hex_pretty(this->attribute, this->attribute_len));
out.append("\n");
}
#endif
void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); }
@@ -1374,11 +1423,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_swing_mode", this->has_swing_mode);
dump_field(out, "swing_mode", static_cast<enums::ClimateSwingMode>(this->swing_mode));
dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode);
dump_field(out, "custom_fan_mode", this->custom_fan_mode);
out.append(" custom_fan_mode: ");
out.append(format_hex_pretty(this->custom_fan_mode, this->custom_fan_mode_len));
out.append("\n");
dump_field(out, "has_preset", this->has_preset);
dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset));
dump_field(out, "has_custom_preset", this->has_custom_preset);
dump_field(out, "custom_preset", this->custom_preset);
out.append(" custom_preset: ");
out.append(format_hex_pretty(this->custom_preset, this->custom_preset_len));
out.append("\n");
dump_field(out, "has_target_humidity", this->has_target_humidity);
dump_field(out, "target_humidity", this->target_humidity);
#ifdef USE_DEVICES
@@ -1386,6 +1439,55 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
#endif
}
#endif
#ifdef USE_WATER_HEATER
void ListEntitiesWaterHeaterResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesWaterHeaterResponse");
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "min_temperature", this->min_temperature);
dump_field(out, "max_temperature", this->max_temperature);
dump_field(out, "target_temperature_step", this->target_temperature_step);
for (const auto &it : *this->supported_modes) {
dump_field(out, "supported_modes", static_cast<enums::WaterHeaterMode>(it), 4);
}
dump_field(out, "supported_features", this->supported_features);
}
void WaterHeaterStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "WaterHeaterStateResponse");
dump_field(out, "key", this->key);
dump_field(out, "current_temperature", this->current_temperature);
dump_field(out, "target_temperature", this->target_temperature);
dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "state", this->state);
dump_field(out, "target_temperature_low", this->target_temperature_low);
dump_field(out, "target_temperature_high", this->target_temperature_high);
}
void WaterHeaterCommandRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "WaterHeaterCommandRequest");
dump_field(out, "key", this->key);
dump_field(out, "has_fields", this->has_fields);
dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode));
dump_field(out, "target_temperature", this->target_temperature);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "state", this->state);
dump_field(out, "target_temperature_low", this->target_temperature_low);
dump_field(out, "target_temperature_high", this->target_temperature_high);
}
#endif
#ifdef USE_NUMBER
void ListEntitiesNumberResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesNumberResponse");

View File

@@ -10,6 +10,10 @@
#include "esphome/components/climate/climate_traits.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_LIGHT
#include "esphome/components/light/light_traits.h"
#endif

View File

@@ -621,6 +621,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_homeassistant_action_response(msg);
break;
}
#endif
#ifdef USE_WATER_HEATER
case WaterHeaterCommandRequest::MESSAGE_TYPE: {
WaterHeaterCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_water_heater_command_request: %s", msg.dump().c_str());
#endif
this->on_water_heater_command_request(msg);
break;
}
#endif
default:
break;

View File

@@ -91,6 +91,10 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_climate_command_request(const ClimateCommandRequest &value){};
#endif
#ifdef USE_WATER_HEATER
virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
#endif
#ifdef USE_NUMBER
virtual void on_number_command_request(const NumberCommandRequest &value){};
#endif

View File

@@ -335,6 +335,10 @@ API_DISPATCH_UPDATE(valve::Valve, valve)
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
#endif
#ifdef USE_WATER_HEATER
API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
#endif
#ifdef USE_EVENT
// Event is a special case - unlike other entities with simple state fields,
// events store their state in a member accessed via obj->get_last_event_type()

View File

@@ -133,6 +133,9 @@ class APIServer : public Component,
#ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif
#ifdef USE_WATER_HEATER
void on_water_heater_update(water_heater::WaterHeater *obj) override;
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_action(const HomeassistantActionRequest &call);

View File

@@ -73,6 +73,9 @@ LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMedia
LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel,
ListEntitiesAlarmControlPanelResponse)
#endif
#ifdef USE_WATER_HEATER
LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse)
#endif
#ifdef USE_EVENT
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
#endif

View File

@@ -82,6 +82,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *entity) override;
#endif

View File

@@ -334,7 +334,7 @@ class ProtoWriteBuffer {
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
this->encode_uint64(field_id, encode_zigzag64(value), force);
}
void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false);
void encode_message(uint32_t field_id, const ProtoMessage &value);
std::vector<uint8_t> *get_buffer() const { return buffer_; }
protected:
@@ -795,7 +795,7 @@ class ProtoSize {
};
// Implementation of encode_message - must be after ProtoMessage is defined
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) {
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
// Calculate the message size first

View File

@@ -60,6 +60,9 @@ INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer)
#ifdef USE_ALARM_CONTROL_PANEL
INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel)
#endif
#ifdef USE_WATER_HEATER
INITIAL_STATE_HANDLER(water_heater, water_heater::WaterHeater)
#endif
#ifdef USE_UPDATE
INITIAL_STATE_HANDLER(update, update::UpdateEntity)
#endif

View File

@@ -76,6 +76,9 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *event) override { return true; };
#endif

View File

@@ -0,0 +1,14 @@
import esphome.codegen as cg
CODEOWNERS = ["@jasstrong", "@ximex", "@freekode"]
aqi_ns = cg.esphome_ns.namespace("aqi")
AQICalculatorType = aqi_ns.enum("AQICalculatorType")
CONF_AQI = "aqi"
CONF_CALCULATION_TYPE = "calculation_type"
AQI_CALCULATION_TYPE = {
"CAQI": AQICalculatorType.CAQI_TYPE,
"AQI": AQICalculatorType.AQI_TYPE,
}

View File

@@ -2,13 +2,11 @@
#include <cstdint>
namespace esphome {
namespace hm3301 {
namespace esphome::aqi {
class AbstractAQICalculator {
public:
virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0;
};
} // namespace hm3301
} // namespace esphome
} // namespace esphome::aqi

View File

@@ -1,10 +1,11 @@
#pragma once
#include <climits>
#include "abstract_aqi_calculator.h"
// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
namespace esphome {
namespace hm3301 {
namespace esphome::aqi {
class AQICalculator : public AbstractAQICalculator {
public:
@@ -28,6 +29,9 @@ class AQICalculator : public AbstractAQICalculator {
int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) {
int grid_index = get_grid_index_(value, array);
if (grid_index == -1) {
return -1;
}
int aqi_lo = index_grid_[grid_index][0];
int aqi_hi = index_grid_[grid_index][1];
int conc_lo = array[grid_index][0];
@@ -46,5 +50,4 @@ class AQICalculator : public AbstractAQICalculator {
}
};
} // namespace hm3301
} // namespace esphome
} // namespace esphome::aqi

View File

@@ -3,8 +3,7 @@
#include "caqi_calculator.h"
#include "aqi_calculator.h"
namespace esphome {
namespace hm3301 {
namespace esphome::aqi {
enum AQICalculatorType { CAQI_TYPE = 0, AQI_TYPE = 1 };
@@ -12,18 +11,17 @@ class AQICalculatorFactory {
public:
AbstractAQICalculator *get_calculator(AQICalculatorType type) {
if (type == 0) {
return caqi_calculator_;
return &this->caqi_calculator_;
} else if (type == 1) {
return aqi_calculator_;
return &this->aqi_calculator_;
}
return nullptr;
}
protected:
CAQICalculator *caqi_calculator_ = new CAQICalculator();
AQICalculator *aqi_calculator_ = new AQICalculator();
CAQICalculator caqi_calculator_;
AQICalculator aqi_calculator_;
};
} // namespace hm3301
} // namespace esphome
} // namespace esphome::aqi

View File

@@ -3,8 +3,7 @@
#include "esphome/core/log.h"
#include "abstract_aqi_calculator.h"
namespace esphome {
namespace hm3301 {
namespace esphome::aqi {
class CAQICalculator : public AbstractAQICalculator {
public:
@@ -48,5 +47,4 @@ class CAQICalculator : public AbstractAQICalculator {
}
};
} // namespace hm3301
} // namespace esphome
} // namespace esphome::aqi

View File

@@ -41,7 +41,7 @@ class Button : public EntityBase, public EntityBase_DeviceClass {
*/
virtual void press_action() = 0;
CallbackManager<void()> press_callback_{};
LazyCallbackManager<void()> press_callback_{};
};
} // namespace esphome::button

View File

@@ -160,41 +160,63 @@ HYST_LEVEL = {
"High": HystLevel.HYST_LEVEL_HIGH,
}
# Config key -> Validator mapping
# Optional settings to generate setter calls for
CONFIG_MAP = {
CONF_OUTPUT_POWER: cv.float_range(min=-30.0, max=11.0),
CONF_RX_ATTENUATION: cv.enum(RX_ATTENUATION, upper=False),
CONF_DC_BLOCKING_FILTER: cv.boolean,
CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)),
CONF_IF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=25000, max=788000)),
CONF_FILTER_BANDWIDTH: cv.All(cv.frequency, cv.float_range(min=58000, max=812000)),
CONF_CHANNEL: cv.uint8_t,
CONF_CHANNEL_SPACING: cv.All(cv.frequency, cv.float_range(min=25000, max=405000)),
CONF_FSK_DEVIATION: cv.All(cv.frequency, cv.float_range(min=1500, max=381000)),
CONF_MSK_DEVIATION: cv.int_range(min=1, max=8),
CONF_SYMBOL_RATE: cv.float_range(min=600, max=500000),
CONF_SYNC_MODE: cv.enum(SYNC_MODE, upper=False),
CONF_CARRIER_SENSE_ABOVE_THRESHOLD: cv.boolean,
CONF_MODULATION_TYPE: cv.enum(MODULATION, upper=False),
CONF_MANCHESTER: cv.boolean,
CONF_NUM_PREAMBLE: cv.int_range(min=0, max=7),
CONF_SYNC1: cv.hex_uint8_t,
CONF_SYNC0: cv.hex_uint8_t,
CONF_MAGN_TARGET: cv.enum(MAGN_TARGET, upper=False),
CONF_MAX_LNA_GAIN: cv.enum(MAX_LNA_GAIN, upper=False),
CONF_MAX_DVGA_GAIN: cv.enum(MAX_DVGA_GAIN, upper=False),
CONF_CARRIER_SENSE_ABS_THR: cv.int_range(min=-8, max=7),
CONF_CARRIER_SENSE_REL_THR: cv.enum(CARRIER_SENSE_REL_THR, upper=False),
CONF_LNA_PRIORITY: cv.boolean,
CONF_FILTER_LENGTH_FSK_MSK: cv.enum(FILTER_LENGTH_FSK_MSK, upper=False),
CONF_FILTER_LENGTH_ASK_OOK: cv.enum(FILTER_LENGTH_ASK_OOK, upper=False),
CONF_FREEZE: cv.enum(FREEZE, upper=False),
CONF_WAIT_TIME: cv.enum(WAIT_TIME, upper=False),
CONF_HYST_LEVEL: cv.enum(HYST_LEVEL, upper=False),
CONF_PACKET_MODE: cv.boolean,
CONF_PACKET_LENGTH: cv.uint8_t,
CONF_CRC_ENABLE: cv.boolean,
CONF_WHITENING: cv.boolean,
cv.Optional(CONF_OUTPUT_POWER, default=10): cv.float_range(min=-30.0, max=11.0),
cv.Optional(CONF_RX_ATTENUATION, default="0dB"): cv.enum(
RX_ATTENUATION, upper=False
),
cv.Optional(CONF_DC_BLOCKING_FILTER, default=True): cv.boolean,
cv.Optional(CONF_FREQUENCY, default="433.92MHz"): cv.All(
cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)
),
cv.Optional(CONF_IF_FREQUENCY, default="153kHz"): cv.All(
cv.frequency, cv.float_range(min=25000, max=788000)
),
cv.Optional(CONF_FILTER_BANDWIDTH, default="203kHz"): cv.All(
cv.frequency, cv.float_range(min=58000, max=812000)
),
cv.Optional(CONF_CHANNEL, default=0): cv.uint8_t,
cv.Optional(CONF_CHANNEL_SPACING, default="200kHz"): cv.All(
cv.frequency, cv.float_range(min=25000, max=405000)
),
cv.Optional(CONF_FSK_DEVIATION): cv.All(
cv.frequency, cv.float_range(min=1500, max=381000)
),
cv.Optional(CONF_MSK_DEVIATION): cv.int_range(min=1, max=8),
cv.Optional(CONF_SYMBOL_RATE, default=5000): cv.float_range(min=600, max=500000),
cv.Optional(CONF_SYNC_MODE, default="16/16"): cv.enum(SYNC_MODE, upper=False),
cv.Optional(CONF_CARRIER_SENSE_ABOVE_THRESHOLD, default=False): cv.boolean,
cv.Optional(CONF_MODULATION_TYPE, default="ASK/OOK"): cv.enum(
MODULATION, upper=False
),
cv.Optional(CONF_MANCHESTER, default=False): cv.boolean,
cv.Optional(CONF_NUM_PREAMBLE, default=2): cv.int_range(min=0, max=7),
cv.Optional(CONF_SYNC1, default=0xD3): cv.hex_uint8_t,
cv.Optional(CONF_SYNC0, default=0x91): cv.hex_uint8_t,
cv.Optional(CONF_MAGN_TARGET, default="42dB"): cv.enum(MAGN_TARGET, upper=False),
cv.Optional(CONF_MAX_LNA_GAIN, default="Default"): cv.enum(
MAX_LNA_GAIN, upper=False
),
cv.Optional(CONF_MAX_DVGA_GAIN, default="-3"): cv.enum(MAX_DVGA_GAIN, upper=False),
cv.Optional(CONF_CARRIER_SENSE_ABS_THR): cv.int_range(min=-8, max=7),
cv.Optional(CONF_CARRIER_SENSE_REL_THR): cv.enum(
CARRIER_SENSE_REL_THR, upper=False
),
cv.Optional(CONF_LNA_PRIORITY, default=False): cv.boolean,
cv.Optional(CONF_FILTER_LENGTH_FSK_MSK): cv.enum(
FILTER_LENGTH_FSK_MSK, upper=False
),
cv.Optional(CONF_FILTER_LENGTH_ASK_OOK): cv.enum(
FILTER_LENGTH_ASK_OOK, upper=False
),
cv.Optional(CONF_FREEZE): cv.enum(FREEZE, upper=False),
cv.Optional(CONF_WAIT_TIME, default="32"): cv.enum(WAIT_TIME, upper=False),
cv.Optional(CONF_HYST_LEVEL): cv.enum(HYST_LEVEL, upper=False),
cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean,
cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t,
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_WHITENING, default=False): cv.boolean,
}
@@ -217,7 +239,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
}
)
.extend({cv.Optional(key): validator for key, validator in CONFIG_MAP.items()})
.extend(CONFIG_MAP)
.extend(cv.COMPONENT_SCHEMA)
.extend(spi.spi_device_schema(cs_pin_required=True)),
_validate_packet_mode,
@@ -229,7 +251,8 @@ async def to_code(config):
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
for key in CONFIG_MAP:
for opt in CONFIG_MAP:
key = opt.schema
if key in config:
cg.add(getattr(var, f"set_{key}")(config[key]))

View File

@@ -98,25 +98,8 @@ CC1101Component::CC1101Component() {
this->state_.LENGTH_CONFIG = 2;
this->state_.FS_AUTOCAL = 1;
// Default Settings
this->set_frequency(433920000);
this->set_if_frequency(153000);
this->set_filter_bandwidth(203000);
this->set_channel(0);
this->set_channel_spacing(200000);
this->set_symbol_rate(5000);
this->set_sync_mode(SyncMode::SYNC_MODE_NONE);
this->set_carrier_sense_above_threshold(true);
this->set_modulation_type(Modulation::MODULATION_ASK_OOK);
this->set_magn_target(MagnTarget::MAGN_TARGET_42DB);
this->set_max_lna_gain(MaxLnaGain::MAX_LNA_GAIN_DEFAULT);
this->set_max_dvga_gain(MaxDvgaGain::MAX_DVGA_GAIN_MINUS_3);
this->set_lna_priority(false);
this->set_wait_time(WaitTime::WAIT_TIME_32_SAMPLES);
// CRITICAL: Initialize PA Table to avoid transmitting 0 power (Silence)
memset(this->pa_table_, 0, sizeof(this->pa_table_));
this->set_output_power(10.0f);
}
void CC1101Component::setup() {

View File

@@ -2,6 +2,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/macros.h"
#include <strings.h>
namespace esphome::climate {
@@ -190,24 +191,30 @@ ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
}
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) {
return this->set_fan_mode(custom_fan_mode, strlen(custom_fan_mode));
}
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
return this->set_fan_mode(fan_mode.data(), fan_mode.size());
}
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode, size_t len) {
// Check if it's a standard enum mode first
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) {
if (strncasecmp(custom_fan_mode, mode_entry.str, len) == 0 && mode_entry.str[len] == '\0') {
return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
}
}
// Find the matching pointer from parent climate device
if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) {
if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode, len)) {
this->custom_fan_mode_ = mode_ptr;
this->fan_mode_.reset();
return *this;
}
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode);
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %.*s", this->parent_->get_name().c_str(), (int) len, custom_fan_mode);
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) {
if (fan_mode.has_value()) {
this->set_fan_mode(fan_mode.value());
@@ -222,24 +229,30 @@ ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
}
ClimateCall &ClimateCall::set_preset(const char *custom_preset) {
return this->set_preset(custom_preset, strlen(custom_preset));
}
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
return this->set_preset(preset.data(), preset.size());
}
ClimateCall &ClimateCall::set_preset(const char *custom_preset, size_t len) {
// Check if it's a standard enum preset first
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
if (str_equals_case_insensitive(custom_preset, preset_entry.str)) {
if (strncasecmp(custom_preset, preset_entry.str, len) == 0 && preset_entry.str[len] == '\0') {
return this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
}
}
// Find the matching pointer from parent climate device
if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) {
if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset, len)) {
this->custom_preset_ = preset_ptr;
this->preset_.reset();
return *this;
}
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset);
ESP_LOGW(TAG, "'%s' - Unrecognized preset %.*s", this->parent_->get_name().c_str(), (int) len, custom_preset);
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) {
if (preset.has_value()) {
this->set_preset(preset.value());
@@ -688,11 +701,19 @@ bool Climate::set_custom_preset_(const char *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);
return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
}
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode, size_t len) {
return this->get_traits().find_custom_fan_mode_(custom_fan_mode, len);
}
const char *Climate::find_custom_preset_(const char *custom_preset) {
return this->get_traits().find_custom_preset_(custom_preset);
return this->find_custom_preset_(custom_preset, strlen(custom_preset));
}
const char *Climate::find_custom_preset_(const char *custom_preset, size_t len) {
return this->get_traits().find_custom_preset_(custom_preset, len);
}
void Climate::dump_traits_(const char *tag) {

View File

@@ -78,6 +78,8 @@ class ClimateCall {
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 custom fan mode of the climate device (zero-copy API path).
ClimateCall &set_fan_mode(const char *custom_fan_mode, size_t len);
/// Set the swing mode of the climate device.
ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
/// Set the swing mode of the climate device.
@@ -94,6 +96,8 @@ class ClimateCall {
ClimateCall &set_preset(optional<std::string> preset);
/// Set the custom preset of the climate device.
ClimateCall &set_preset(const char *custom_preset);
/// Set the custom preset of the climate device (zero-copy API path).
ClimateCall &set_preset(const char *custom_preset, size_t len);
void perform();
@@ -290,9 +294,11 @@ class Climate : public EntityBase {
/// 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);
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len);
/// Find and return the matching custom preset pointer from traits, or nullptr if not found.
const char *find_custom_preset_(const char *custom_preset);
const char *find_custom_preset_(const char *custom_preset, size_t len);
/** Get the default traits of this climate device.
*
@@ -320,8 +326,8 @@ class Climate : public EntityBase {
void dump_traits_(const char *tag);
CallbackManager<void(Climate &)> state_callback_{};
CallbackManager<void(ClimateCall &)> control_callback_{};
LazyCallbackManager<void(Climate &)> state_callback_{};
LazyCallbackManager<void(ClimateCall &)> control_callback_{};
ESPPreferenceObject rtc_;
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
float visual_min_temperature_override_{NAN};

View File

@@ -20,18 +20,22 @@ using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimateP
// Lightweight linear search for small vectors (1-20 items) of const char* pointers
// Avoids std::find template overhead
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
inline bool vector_contains(const std::vector<const char *> &vec, const char *value, size_t len) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
if (strncmp(item, value, len) == 0 && item[len] == '\0')
return true;
}
return false;
}
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
return vector_contains(vec, value, strlen(value));
}
// 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) {
inline const char *vector_find(const std::vector<const char *> &vec, const char *value, size_t len) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
if (strncmp(item, value, len) == 0 && item[len] == '\0')
return item;
}
return nullptr;
@@ -257,13 +261,19 @@ 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);
return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
}
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len) const {
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len);
}
/// 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);
return this->find_custom_preset_(custom_preset, strlen(custom_preset));
}
const char *find_custom_preset_(const char *custom_preset, size_t len) const {
return vector_find(this->supported_custom_presets_, custom_preset, len);
}
uint32_t feature_flags_{0};

View File

@@ -152,7 +152,7 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
optional<CoverRestoreState> restore_state_();
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;
};

View File

@@ -22,7 +22,7 @@ class DateTimeBase : public EntityBase {
#endif
protected:
CallbackManager<void()> state_callback_;
LazyCallbackManager<void()> state_callback_;
#ifdef USE_TIME
time::RealTimeClock *rtc_;

View File

@@ -8,17 +8,20 @@ namespace dht {
static const char *const TAG = "dht";
void DHT::setup() {
this->pin_->digital_write(true);
this->pin_->setup();
this->pin_->digital_write(true);
this->t_pin_->digital_write(true);
this->t_pin_->setup();
#ifdef USE_ESP32
this->t_pin_->pin_mode(this->t_pin_->get_flags() | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN);
#endif
this->t_pin_->digital_write(true);
}
void DHT::dump_config() {
ESP_LOGCONFIG(TAG, "DHT:");
LOG_PIN(" Pin: ", this->pin_);
LOG_PIN(" Pin: ", this->t_pin_);
ESP_LOGCONFIG(TAG, " %sModel: %s", this->is_auto_detect_ ? "Auto-detected " : "",
this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent");
ESP_LOGCONFIG(TAG, " Internal pull-up: %s", ONOFF(this->pin_->get_flags() & gpio::FLAG_PULLUP));
ESP_LOGCONFIG(TAG, " Internal pull-up: %s", ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP));
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
@@ -72,21 +75,15 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
int8_t i = 0;
uint8_t data[5] = {0, 0, 0, 0, 0};
this->pin_->digital_write(false);
this->pin_->pin_mode(gpio::FLAG_OUTPUT);
this->pin_->digital_write(false);
#ifndef USE_ESP32
this->pin_.pin_mode(gpio::FLAG_OUTPUT);
#endif
this->pin_.digital_write(false);
if (this->model_ == DHT_MODEL_DHT11) {
delayMicroseconds(18000);
} else if (this->model_ == DHT_MODEL_SI7021) {
#ifdef USE_ESP8266
delayMicroseconds(500);
this->pin_->digital_write(true);
delayMicroseconds(40);
#else
delayMicroseconds(400);
this->pin_->digital_write(true);
#endif
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
delayMicroseconds(2000);
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
@@ -94,7 +91,12 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
} else {
delayMicroseconds(800);
}
this->pin_->pin_mode(this->pin_->get_flags());
#ifdef USE_ESP32
this->pin_.digital_write(true);
#else
this->pin_.pin_mode(this->t_pin_->get_flags());
#endif
{
InterruptLock lock;
@@ -110,7 +112,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
uint32_t start_time = micros();
// Wait for rising edge
while (!this->pin_->digital_read()) {
while (!this->pin_.digital_read()) {
if (micros() - start_time > 90) {
if (i < 0) {
error_code = 1; // line didn't clear
@@ -127,7 +129,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
uint32_t end_time = start_time;
// Wait for falling edge
while (this->pin_->digital_read()) {
while (this->pin_.digital_read()) {
end_time = micros();
if (end_time - start_time > 90) {
if (i < 0) {

View File

@@ -38,7 +38,10 @@ class DHT : public PollingComponent {
*/
void set_dht_model(DHTModel model);
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
void set_pin(InternalGPIOPin *pin) {
this->t_pin_ = pin;
this->pin_ = pin->to_isr();
}
void set_model(DHTModel model) { model_ = model; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
@@ -54,7 +57,8 @@ class DHT : public PollingComponent {
protected:
bool read_sensor_(float *temperature, float *humidity, bool report_errors);
InternalGPIOPin *pin_;
InternalGPIOPin *t_pin_;
ISRInternalGPIOPin pin_;
DHTModel model_{DHT_MODEL_AUTO_DETECT};
bool is_auto_detect_{false};
sensor::Sensor *temperature_sensor_{nullptr};

View File

@@ -54,6 +54,7 @@ bool MenuItemSelect::select_next() {
if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_next(true).perform();
this->on_value_();
changed = true;
}
@@ -65,6 +66,7 @@ bool MenuItemSelect::select_prev() {
if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_previous(true).perform();
this->on_value_();
changed = true;
}

View File

@@ -130,10 +130,10 @@ class ESP32Preferences : public ESPPreferences {
// go through vector from back to front (makes erase easier/more efficient)
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
const auto &save = s_pending_save[i];
ESP_LOGVV(TAG, "Checking if NVS data %" PRIu32 " has changed", save.key);
if (this->is_changed(this->nvs_handle, save)) {
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
if (err != 0) {
@@ -166,10 +166,9 @@ class ESP32Preferences : public ESPPreferences {
return failed == 0;
}
bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) {
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, to_save.key);
protected:
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
size_t actual_len;
esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {

View File

@@ -308,13 +308,21 @@ bool ESP32BLE::ble_setup_() {
bool ESP32BLE::ble_dismantle_() {
esp_err_t err = esp_bluedroid_disable();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
return false;
// ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine
if (err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
return false;
}
ESP_LOGD(TAG, "Already disabled");
}
err = esp_bluedroid_deinit();
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err);
return false;
// ESP_ERR_INVALID_STATE means Bluedroid is already deinitialized, which is fine
if (err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err);
return false;
}
ESP_LOGD(TAG, "Already deinitialized");
}
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID

View File

@@ -212,17 +212,23 @@ extern ESP32BLE *global_ble;
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
public:
bool check(const Ts &...x) override { return global_ble->is_active(); }
bool check(const Ts &...x) override { return global_ble != nullptr && global_ble->is_active(); }
};
template<typename... Ts> class BLEEnableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_ble->enable(); }
void play(const Ts &...x) override {
if (global_ble != nullptr)
global_ble->enable();
}
};
template<typename... Ts> class BLEDisableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_ble->disable(); }
void play(const Ts &...x) override {
if (global_ble != nullptr)
global_ble->disable();
}
};
} // namespace esphome::esp32_ble

View File

@@ -143,9 +143,8 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
return this->as_128bit() == uuid.as_128bit();
}
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
std::string ESPBTUUID::to_string() const {
char buf[40]; // Enough for 128-bit UUID with dashes
char *pos = buf;
void ESPBTUUID::to_str(std::span<char, UUID_STR_LEN> output) const {
char *pos = output.data();
switch (this->uuid_.len) {
case ESP_UUID_LEN_16:
@@ -156,7 +155,7 @@ std::string ESPBTUUID::to_string() const {
*pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 4) & 0x0F);
*pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 & 0x0F);
*pos = '\0';
return std::string(buf);
return;
case ESP_UUID_LEN_32:
*pos++ = '0';
@@ -165,7 +164,7 @@ std::string ESPBTUUID::to_string() const {
*pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid32 >> shift) & 0x0F);
}
*pos = '\0';
return std::string(buf);
return;
default:
case ESP_UUID_LEN_128:
@@ -179,9 +178,13 @@ std::string ESPBTUUID::to_string() const {
}
}
*pos = '\0';
return std::string(buf);
return;
}
return "";
}
std::string ESPBTUUID::to_string() const {
char buf[UUID_STR_LEN];
this->to_str(buf);
return std::string(buf);
}
} // namespace esphome::esp32_ble

View File

@@ -7,11 +7,15 @@
#ifdef USE_ESP32
#ifdef USE_ESP32_BLE_UUID
#include <span>
#include <string>
#include <esp_bt_defs.h>
namespace esphome::esp32_ble {
/// Buffer size for UUID string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\0"
static constexpr size_t UUID_STR_LEN = 37;
class ESPBTUUID {
public:
ESPBTUUID();
@@ -37,6 +41,7 @@ class ESPBTUUID {
esp_bt_uuid_t get_uuid() const;
std::string to_string() const;
void to_str(std::span<char, UUID_STR_LEN> output) const;
protected:
esp_bt_uuid_t uuid_;

View File

@@ -50,8 +50,12 @@ void BLECharacteristic::parse_descriptors() {
desc->handle = result.handle;
desc->characteristic = this;
this->descriptors.push_back(desc);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[espbt::UUID_STR_LEN];
desc->uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(),
this->service->client->address_str(), desc->uuid.to_string().c_str(), desc->handle);
this->service->client->address_str(), uuid_buf, desc->handle);
#endif
offset++;
}
}

View File

@@ -411,12 +411,15 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
} else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) {
#ifdef USE_ESP32_BLE_DEVICE
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
for (auto &svc : this->services_) {
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_,
svc->uuid.to_string().c_str());
char uuid_buf[espbt::UUID_STR_LEN];
svc->uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_,
svc->start_handle, svc->end_handle);
}
#endif
#endif
}
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);

View File

@@ -64,9 +64,12 @@ void BLEService::parse_characteristics() {
characteristic->handle = result.char_handle;
characteristic->service = this;
this->characteristics.push_back(characteristic);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[espbt::UUID_STR_LEN];
characteristic->uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(),
this->client->address_str(), characteristic->uuid.to_string().c_str(), characteristic->handle,
characteristic->properties);
this->client->address_str(), uuid_buf, characteristic->handle, characteristic->properties);
#endif
offset++;
}
}

View File

@@ -109,7 +109,11 @@ void BLECharacteristic::do_create(BLEService *service) {
esp_attr_control_t control;
control.auto_rsp = ESP_GATT_RSP_BY_APP;
ESP_LOGV(TAG, "Creating characteristic - %s", this->uuid_.to_string().c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
this->uuid_.to_str(uuid_buf);
ESP_LOGV(TAG, "Creating characteristic - %s", uuid_buf);
#endif
esp_bt_uuid_t uuid = this->uuid_.get_uuid();
esp_err_t err = esp_ble_gatts_add_char(service->get_handle(), &uuid, static_cast<esp_gatt_perm_t>(this->permissions_),

View File

@@ -34,7 +34,11 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
esp_attr_control_t control;
control.auto_rsp = ESP_GATT_AUTO_RSP;
ESP_LOGV(TAG, "Creating descriptor - %s", this->uuid_.to_string().c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
this->uuid_.to_str(uuid_buf);
ESP_LOGV(TAG, "Creating descriptor - %s", uuid_buf);
#endif
esp_bt_uuid_t uuid = this->uuid_.get_uuid();
esp_err_t err = esp_ble_gatts_add_char_descr(this->characteristic_->get_service()->get_handle(), &uuid,
this->permissions_, &this->value_, &control);

View File

@@ -106,7 +106,11 @@ void BLEServer::restart_advertising_() {
}
BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) {
ESP_LOGV(TAG, "Creating BLE service - %s", uuid.to_string().c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "Creating BLE service - %s", uuid_buf);
#endif
// Calculate the inst_id for the service
uint8_t inst_id = 0;
for (; inst_id < 0xFF; inst_id++) {
@@ -115,7 +119,9 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n
}
}
if (inst_id == 0xFF) {
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", uuid.to_string().c_str());
char warn_uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(warn_uuid_buf);
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", warn_uuid_buf);
return nullptr;
}
BLEService *service = // NOLINT(cppcoreguidelines-owning-memory)
@@ -128,7 +134,11 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n
}
void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid.to_string().c_str(), inst_id);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid_buf, inst_id);
#endif
for (auto it = this->services_.begin(); it != this->services_.end(); ++it) {
if (it->uuid == uuid && it->inst_id == inst_id) {
it->service->do_delete();
@@ -137,7 +147,9 @@ void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
return;
}
}
ESP_LOGW(TAG, "BLE service %s %d does not exist", uuid.to_string().c_str(), inst_id);
char warn_uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(warn_uuid_buf);
ESP_LOGW(TAG, "BLE service %s %d does not exist", warn_uuid_buf, inst_id);
}
BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) {

View File

@@ -5,7 +5,7 @@ import logging
from esphome import automation
import esphome.codegen as cg
from esphome.components import esp32_ble
from esphome.components import esp32_ble, ota
from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import (
IDF_MAX_CONNECTIONS,
@@ -328,7 +328,7 @@ async def to_code(config):
# Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now
# configured in esp32_ble component based on max_connections setting
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
ota.request_ota_state_listeners() # To be notified when an OTA update starts
cg.add_define("USE_ESP32_BLE_CLIENT")
CORE.add_job(_add_ble_features)

View File

@@ -71,21 +71,24 @@ void ESP32BLETracker::setup() {
global_esp32_ble_tracker = this;
#ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback(
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
this->stop_scan();
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->disconnect();
}
#endif
}
});
#ifdef USE_OTA_STATE_LISTENER
ota::get_global_ota_callback()->add_global_state_listener(this);
#endif
}
#ifdef USE_OTA_STATE_LISTENER
void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
this->stop_scan();
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->disconnect();
}
#endif
}
}
#endif
void ESP32BLETracker::loop() {
if (!this->parent_->is_active()) {
this->ble_was_disabled_ = true;
@@ -185,7 +188,10 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_();
void ESP32BLETracker::stop_scan_() {
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
// If scanner is already idle, there's nothing to stop - this is not an error
if (this->scanner_state_ != ScannerState::IDLE) {
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
}
return;
}
// Reset timeout state machine when stopping scan
@@ -435,24 +441,31 @@ void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_);
}
for (auto &uuid : this->service_uuids_) {
ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str());
char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " Service UUID: %s", uuid_buf);
}
for (auto &data : this->manufacturer_datas_) {
auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data);
if (ibeacon.has_value()) {
ESP_LOGVV(TAG, " Manufacturer iBeacon:");
ESP_LOGVV(TAG, " UUID: %s", ibeacon.value().get_uuid().to_string().c_str());
char uuid_buf[esp32_ble::UUID_STR_LEN];
ibeacon.value().get_uuid().to_str(uuid_buf);
ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major());
ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor());
ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power());
} else {
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", data.uuid.to_string().c_str(),
format_hex_pretty(data.data).c_str());
char uuid_buf[esp32_ble::UUID_STR_LEN];
data.uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", uuid_buf, format_hex_pretty(data.data).c_str());
}
}
for (auto &data : this->service_datas_) {
ESP_LOGVV(TAG, " Service data:");
ESP_LOGVV(TAG, " UUID: %s", data.uuid.to_string().c_str());
char uuid_buf[esp32_ble::UUID_STR_LEN];
data.uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str());
}

View File

@@ -22,6 +22,10 @@
#include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/esp32_ble/ble_scan_result.h"
#ifdef USE_OTA_STATE_LISTENER
#include "esphome/components/ota/ota_backend.h"
#endif
namespace esphome::esp32_ble_tracker {
using namespace esp32_ble;
@@ -241,6 +245,9 @@ class ESP32BLETracker : public Component,
public GAPScanEventHandler,
public GATTcEventHandler,
public BLEStatusEventHandler,
#ifdef USE_OTA_STATE_LISTENER
public ota::OTAGlobalStateListener,
#endif
public Parented<ESP32BLE> {
public:
void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; }
@@ -274,6 +281,10 @@ class ESP32BLETracker : public Component,
void gap_scan_event_handler(const BLEScanResult &scan_result) override;
void ble_before_disabled_event_handler() override;
#ifdef USE_OTA_STATE_LISTENER
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override;
#endif
/// Add a listener for scanner state changes
void add_scanner_state_listener(BLEScannerStateListener *listener) {
this->scanner_state_listeners_.push_back(listener);

View File

@@ -11,6 +11,9 @@ namespace esphome {
namespace esp32_camera {
static const char *const TAG = "esp32_camera";
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000;
#endif
/* ---------------- public API (derivated) ---------------- */
void ESP32Camera::setup() {
@@ -204,7 +207,20 @@ void ESP32Camera::loop() {
}
this->current_image_ = std::make_shared<ESP32CameraImage>(fb, this->single_requesters_ | this->stream_requesters_);
ESP_LOGD(TAG, "Got Image: len=%u", fb->len);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, "Got Image: len=%u", fb->len);
#else
// Initialize log time on first frame to ensure accurate interval measurement
if (this->frame_count_ == 0) {
this->last_log_time_ = now;
}
this->frame_count_++;
if (now - this->last_log_time_ >= FRAME_LOG_INTERVAL_MS) {
ESP_LOGD(TAG, "Received %u images in last %us", this->frame_count_, FRAME_LOG_INTERVAL_MS / 1000);
this->last_log_time_ = now;
this->frame_count_ = 0;
}
#endif
for (auto *listener : this->listeners_) {
listener->on_camera_image(this->current_image_);
}

View File

@@ -213,6 +213,10 @@ class ESP32Camera : public camera::Camera {
uint32_t last_idle_request_{0};
uint32_t last_update_{0};
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
uint32_t last_log_time_{0};
uint16_t frame_count_{0};
#endif
#ifdef USE_I2C
i2c::InternalI2CBus *i2c_bus_{nullptr};
#endif // USE_I2C

View File

@@ -41,10 +41,6 @@ static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32
#endif // USE_OTA_PASSWORD
void ESPHomeOTAComponent::setup() {
#ifdef USE_OTA_STATE_CALLBACK
ota::register_ota_platform(this);
#endif
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->server_ == nullptr) {
this->log_socket_error_(LOG_STR("creation"));
@@ -297,8 +293,8 @@ void ESPHomeOTAComponent::handle_data_() {
// accidentally trigger the update process.
this->log_start_(LOG_STR("update"));
this->status_set_warning();
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_STARTED, 0.0f, 0);
#endif
// This will block for a few seconds as it locks flash
@@ -357,8 +353,8 @@ void ESPHomeOTAComponent::handle_data_() {
last_progress = now;
float percentage = (total * 100.0f) / ota_size;
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
#endif
// feed watchdog and give other tasks a chance to run
this->yield_and_feed_watchdog_();
@@ -387,8 +383,8 @@ void ESPHomeOTAComponent::handle_data_() {
delay(10);
ESP_LOGI(TAG, "Update complete");
this->status_clear_warning();
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, 0);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_COMPLETED, 100.0f, 0);
#endif
delay(100); // NOLINT
App.safe_reboot();
@@ -402,8 +398,8 @@ error:
}
this->status_momentary_error("err", 5000);
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#endif
}

View File

@@ -50,7 +50,7 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback);
protected:
CallbackManager<void(const std::string &event_type)> event_callback_;
LazyCallbackManager<void(const std::string &event_type)> event_callback_;
FixedVector<const char *> types_;
private:

View File

@@ -19,22 +19,28 @@ const LogString *fan_direction_to_string(FanDirection direction) {
}
}
FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { return this->set_preset_mode(preset_mode.c_str()); }
FanCall &FanCall::set_preset_mode(const std::string &preset_mode) {
return this->set_preset_mode(preset_mode.data(), preset_mode.size());
}
FanCall &FanCall::set_preset_mode(const char *preset_mode) {
if (preset_mode == nullptr || strlen(preset_mode) == 0) {
return this->set_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0);
}
FanCall &FanCall::set_preset_mode(const char *preset_mode, size_t len) {
if (preset_mode == nullptr || len == 0) {
this->preset_mode_ = nullptr;
return *this;
}
// Find and validate pointer from traits immediately
auto traits = this->parent_.get_traits();
const char *validated_mode = traits.find_preset_mode(preset_mode);
const char *validated_mode = traits.find_preset_mode(preset_mode, len);
if (validated_mode != nullptr) {
this->preset_mode_ = validated_mode; // Store pointer from traits
} else {
// Preset mode not found in traits - log warning and don't set
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), preset_mode);
ESP_LOGW(TAG, "%s: Preset mode '%.*s' not supported", this->parent_.get_name().c_str(), (int) len, preset_mode);
this->preset_mode_ = nullptr;
}
return *this;
@@ -140,7 +146,13 @@ FanCall Fan::turn_off() { return this->make_call().set_state(false); }
FanCall Fan::toggle() { return this->make_call().set_state(!this->state); }
FanCall Fan::make_call() { return FanCall(*this); }
const char *Fan::find_preset_mode_(const char *preset_mode) { return this->get_traits().find_preset_mode(preset_mode); }
const char *Fan::find_preset_mode_(const char *preset_mode) {
return this->find_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0);
}
const char *Fan::find_preset_mode_(const char *preset_mode, size_t len) {
return this->get_traits().find_preset_mode(preset_mode, len);
}
bool Fan::set_preset_mode_(const char *preset_mode) {
if (preset_mode == nullptr) {

View File

@@ -72,6 +72,7 @@ class FanCall {
optional<FanDirection> get_direction() const { return this->direction_; }
FanCall &set_preset_mode(const std::string &preset_mode);
FanCall &set_preset_mode(const char *preset_mode);
FanCall &set_preset_mode(const char *preset_mode, size_t len);
const char *get_preset_mode() const { return this->preset_mode_; }
bool has_preset_mode() const { return this->preset_mode_ != nullptr; }
@@ -152,8 +153,9 @@ class Fan : public EntityBase {
void clear_preset_mode_();
/// Find and return the matching preset mode pointer from traits, or nullptr if not found.
const char *find_preset_mode_(const char *preset_mode);
const char *find_preset_mode_(const char *preset_mode, size_t len);
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;
FanRestoreMode restore_mode_;

View File

@@ -47,10 +47,13 @@ class FanTraits {
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
/// Find and return the matching preset mode pointer from supported modes, or nullptr if not found.
const char *find_preset_mode(const char *preset_mode) const {
if (preset_mode == nullptr)
return this->find_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0);
}
const char *find_preset_mode(const char *preset_mode, size_t len) const {
if (preset_mode == nullptr || len == 0)
return nullptr;
for (const char *mode : this->preset_modes_) {
if (strcmp(mode, preset_mode) == 0) {
if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') {
return mode; // Return pointer from traits
}
}

View File

@@ -63,7 +63,7 @@ void HM3301Component::update() {
int16_t aqi_value = -1;
if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) {
AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_);
aqi::AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_);
aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value);
}

View File

@@ -3,7 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
#include "aqi_calculator_factory.h"
#include "esphome/components/aqi/aqi_calculator_factory.h"
namespace esphome {
namespace hm3301 {
@@ -19,7 +19,7 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice {
void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; }
void set_aqi_sensor(sensor::Sensor *aqi_sensor) { aqi_sensor_ = aqi_sensor; }
void set_aqi_calculation_type(AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; }
void set_aqi_calculation_type(aqi::AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; }
void setup() override;
void dump_config() override;
@@ -41,8 +41,8 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice {
sensor::Sensor *pm_10_0_sensor_{nullptr};
sensor::Sensor *aqi_sensor_{nullptr};
AQICalculatorType aqi_calc_type_;
AQICalculatorFactory aqi_calculator_factory_ = AQICalculatorFactory();
aqi::AQICalculatorType aqi_calc_type_;
aqi::AQICalculatorFactory aqi_calculator_factory_ = aqi::AQICalculatorFactory();
bool validate_checksum_(const uint8_t *data);
uint16_t get_sensor_value_(const uint8_t *data, uint8_t i);

View File

@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import i2c, sensor
from esphome.components.aqi import AQI_CALCULATION_TYPE, CONF_AQI, CONF_CALCULATION_TYPE
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
@@ -16,23 +17,16 @@ from esphome.const import (
)
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["aqi"]
CODEOWNERS = ["@freekode"]
hm3301_ns = cg.esphome_ns.namespace("hm3301")
HM3301Component = hm3301_ns.class_(
"HM3301Component", cg.PollingComponent, i2c.I2CDevice
)
AQICalculatorType = hm3301_ns.enum("AQICalculatorType")
CONF_AQI = "aqi"
CONF_CALCULATION_TYPE = "calculation_type"
UNIT_INDEX = "index"
AQI_CALCULATION_TYPE = {
"CAQI": AQICalculatorType.CAQI_TYPE,
"AQI": AQICalculatorType.AQI_TYPE,
}
def _validate(config):
if CONF_AQI in config and CONF_PM_2_5 not in config:

View File

@@ -152,14 +152,26 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
}
container->feed_wdt();
container->content_length = esp_http_client_fetch_headers(client);
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
container->set_response_headers(user_data.response_headers);
container->duration_ms = millis() - start;
if (is_success(container->status_code)) {
return container;
int64_t result = esp_http_client_fetch_headers(client);
if (result < 0) {
if (result == -ESP_ERR_HTTP_EAGAIN) {
container->status_code = ESP_ERR_HTTP_EAGAIN;
} else {
this->status_momentary_error("failed", 1000);
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(result));
esp_http_client_cleanup(client);
return nullptr;
}
} else {
container->content_length = result;
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
container->set_response_headers(user_data.response_headers);
container->duration_ms = millis() - start;
if (is_success(container->status_code)) {
return container;
}
}
if (this->follow_redirects_) {
@@ -187,15 +199,26 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
}
container->feed_wdt();
container->content_length = esp_http_client_fetch_headers(client);
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
container->duration_ms = millis() - start;
if (is_success(container->status_code)) {
return container;
result = esp_http_client_fetch_headers(client);
if (result < 0) {
if (result == -ESP_ERR_HTTP_EAGAIN) {
container->status_code = ESP_ERR_HTTP_EAGAIN;
} else {
this->status_momentary_error("failed", 1000);
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(result));
esp_http_client_cleanup(client);
return nullptr;
}
} else {
container->content_length = result;
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
container->duration_ms = millis() - start;
if (is_success(container->status_code)) {
return container;
}
}
num_redirects--;
}

View File

@@ -16,12 +16,6 @@ namespace http_request {
static const char *const TAG = "http_request.ota";
void OtaHttpRequestComponent::setup() {
#ifdef USE_OTA_STATE_CALLBACK
ota::register_ota_platform(this);
#endif
}
void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); };
void OtaHttpRequestComponent::set_md5_url(const std::string &url) {
@@ -48,24 +42,24 @@ void OtaHttpRequestComponent::flash() {
}
ESP_LOGI(TAG, "Starting update");
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_STARTED, 0.0f, 0);
#endif
auto ota_status = this->do_ota_();
switch (ota_status) {
case ota::OTA_RESPONSE_OK:
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, ota_status);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_COMPLETED, 100.0f, ota_status);
#endif
delay(10);
App.safe_reboot();
break;
default:
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_ERROR, 0.0f, ota_status);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_ERROR, 0.0f, ota_status);
#endif
this->md5_computed_.clear(); // will be reset at next attempt
this->md5_expected_.clear(); // will be reset at next attempt
@@ -165,8 +159,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
last_progress = now;
float percentage = container->get_bytes_read() * 100.0f / container->content_length;
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
#endif
}
} // while

View File

@@ -24,7 +24,6 @@ enum OtaHttpRequestError : uint8_t {
class OtaHttpRequestComponent : public ota::OTAComponent, public Parented<HttpRequestComponent> {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }

View File

@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import update
from esphome.components import ota, update
import esphome.config_validation as cv
from esphome.const import CONF_SOURCE
@@ -38,6 +38,6 @@ async def to_code(config):
cg.add(var.set_source_url(config[CONF_SOURCE]))
cg.add_define("USE_OTA_STATE_CALLBACK")
ota.request_ota_state_listeners()
await cg.register_component(var, config)

View File

@@ -20,19 +20,19 @@ static const char *const TAG = "http_request.update";
static const size_t MAX_READ_SIZE = 256;
void HttpRequestUpdate::setup() {
this->ota_parent_->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t err) {
if (state == ota::OTAState::OTA_IN_PROGRESS) {
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = true;
this->update_info_.progress = progress;
this->publish_state();
} else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) {
this->state_ = update::UPDATE_STATE_AVAILABLE;
this->status_set_error(LOG_STR("Failed to install firmware"));
this->publish_state();
}
});
void HttpRequestUpdate::setup() { this->ota_parent_->add_state_listener(this); }
void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) {
if (state == ota::OTAState::OTA_IN_PROGRESS) {
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = true;
this->update_info_.progress = progress;
this->publish_state();
} else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) {
this->state_ = update::UPDATE_STATE_AVAILABLE;
this->status_set_error(LOG_STR("Failed to install firmware"));
this->publish_state();
}
}
void HttpRequestUpdate::update() {

View File

@@ -14,7 +14,7 @@
namespace esphome {
namespace http_request {
class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent {
class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent, public ota::OTAStateListener {
public:
void setup() override;
void update() override;
@@ -29,6 +29,8 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent {
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void on_ota_state(ota::OTAState state, float progress, uint8_t error) override;
protected:
HttpRequestComponent *request_parent_;
OtaHttpRequestComponent *ota_parent_;

View File

@@ -95,35 +95,35 @@ CONF_DOUBLE_BUFFER = "double_buffer"
CONF_MIN_REFRESH_RATE = "min_refresh_rate"
# Map to hub75 library enums (in global namespace)
ShiftDriver = cg.global_ns.enum("ShiftDriver", is_class=True)
Hub75ShiftDriver = cg.global_ns.enum("Hub75ShiftDriver", is_class=True)
SHIFT_DRIVERS = {
"GENERIC": ShiftDriver.GENERIC,
"FM6126A": ShiftDriver.FM6126A,
"ICN2038S": ShiftDriver.ICN2038S,
"FM6124": ShiftDriver.FM6124,
"MBI5124": ShiftDriver.MBI5124,
"DP3246": ShiftDriver.DP3246,
"GENERIC": Hub75ShiftDriver.GENERIC,
"FM6126A": Hub75ShiftDriver.FM6126A,
"ICN2038S": Hub75ShiftDriver.ICN2038S,
"FM6124": Hub75ShiftDriver.FM6124,
"MBI5124": Hub75ShiftDriver.MBI5124,
"DP3246": Hub75ShiftDriver.DP3246,
}
PanelLayout = cg.global_ns.enum("PanelLayout", is_class=True)
Hub75PanelLayout = cg.global_ns.enum("Hub75PanelLayout", is_class=True)
PANEL_LAYOUTS = {
"HORIZONTAL": PanelLayout.HORIZONTAL,
"TOP_LEFT_DOWN": PanelLayout.TOP_LEFT_DOWN,
"TOP_RIGHT_DOWN": PanelLayout.TOP_RIGHT_DOWN,
"BOTTOM_LEFT_UP": PanelLayout.BOTTOM_LEFT_UP,
"BOTTOM_RIGHT_UP": PanelLayout.BOTTOM_RIGHT_UP,
"TOP_LEFT_DOWN_ZIGZAG": PanelLayout.TOP_LEFT_DOWN_ZIGZAG,
"TOP_RIGHT_DOWN_ZIGZAG": PanelLayout.TOP_RIGHT_DOWN_ZIGZAG,
"BOTTOM_LEFT_UP_ZIGZAG": PanelLayout.BOTTOM_LEFT_UP_ZIGZAG,
"BOTTOM_RIGHT_UP_ZIGZAG": PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG,
"HORIZONTAL": Hub75PanelLayout.HORIZONTAL,
"TOP_LEFT_DOWN": Hub75PanelLayout.TOP_LEFT_DOWN,
"TOP_RIGHT_DOWN": Hub75PanelLayout.TOP_RIGHT_DOWN,
"BOTTOM_LEFT_UP": Hub75PanelLayout.BOTTOM_LEFT_UP,
"BOTTOM_RIGHT_UP": Hub75PanelLayout.BOTTOM_RIGHT_UP,
"TOP_LEFT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_LEFT_DOWN_ZIGZAG,
"TOP_RIGHT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_RIGHT_DOWN_ZIGZAG,
"BOTTOM_LEFT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_LEFT_UP_ZIGZAG,
"BOTTOM_RIGHT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG,
}
ScanPattern = cg.global_ns.enum("ScanPattern", is_class=True)
Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True)
SCAN_PATTERNS = {
"STANDARD_TWO_SCAN": ScanPattern.STANDARD_TWO_SCAN,
"FOUR_SCAN_16PX_HIGH": ScanPattern.FOUR_SCAN_16PX_HIGH,
"FOUR_SCAN_32PX_HIGH": ScanPattern.FOUR_SCAN_32PX_HIGH,
"FOUR_SCAN_64PX_HIGH": ScanPattern.FOUR_SCAN_64PX_HIGH,
"STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN,
"FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH,
"FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH,
"FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH,
}
Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
@@ -531,7 +531,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None:
add_idf_component(
name="esphome/esp-hub75",
ref="0.1.6",
ref="0.1.7",
)
# Set compile-time configuration via defines

View File

@@ -120,10 +120,10 @@ class LibreTinyPreferences : public ESPPreferences {
// go through vector from back to front (makes erase easier/more efficient)
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
const auto &save = s_pending_save[i];
ESP_LOGVV(TAG, "Checking if FDB data %" PRIu32 " has changed", save.key);
if (this->is_changed(&this->db, save)) {
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
if (this->is_changed_(&this->db, save, key_str)) {
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
fdb_blob_make(&this->blob, save.data.get(), save.len);
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
@@ -150,10 +150,8 @@ class LibreTinyPreferences : public ESPPreferences {
return failed == 0;
}
bool is_changed(const fdb_kvdb_t db, const NVSData &to_save) {
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, to_save.key);
protected:
bool is_changed_(fdb_kvdb_t db, const NVSData &to_save, const char *key_str) {
struct fdb_kv kv;
fdb_kv_t kvp = fdb_kv_get_obj(db, key_str, &kv);
if (kvp == nullptr) {

View File

@@ -174,7 +174,7 @@ class Lock : public EntityBase {
*/
virtual void control(const LockCall &call) = 0;
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
Deduplicator<LockState> publish_dedup_;
ESPPreferenceObject rtc_;
};

View File

@@ -118,11 +118,11 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
// Platform-specific: does write_msg_ add its own newline?
// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, LibreTiny)
// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, RP2040, LibreTiny)
// Allows single write call with newline included for efficiency
// true: write_msg_ adds newline itself via puts()/println() (other platforms)
// Newline should NOT be added to buffer
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_LIBRETINY)
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
static constexpr bool WRITE_MSG_ADDS_NEWLINE = false;
#else
static constexpr bool WRITE_MSG_ADDS_NEWLINE = true;

View File

@@ -27,7 +27,10 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); }
void HOT Logger::write_msg_(const char *msg, size_t len) {
// Single write with newline already in buffer (added by caller)
this->hw_serial_->write(msg, len);
}
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {

View File

@@ -157,7 +157,7 @@ class MediaPlayer : public EntityBase {
virtual void control(const MediaPlayerCall &call) = 0;
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
};
} // namespace media_player

View File

@@ -7,7 +7,7 @@ from urllib.parse import urljoin
from esphome import automation, external_files, git
from esphome.automation import register_action, register_condition
import esphome.codegen as cg
from esphome.components import esp32, microphone, socket
from esphome.components import esp32, microphone, ota, socket
import esphome.config_validation as cv
from esphome.const import (
CONF_FILE,
@@ -452,7 +452,7 @@ async def to_code(config):
cg.add(var.set_microphone_source(mic_source))
cg.add_define("USE_MICRO_WAKE_WORD")
cg.add_define("USE_OTA_STATE_CALLBACK")
ota.request_ota_state_listeners()
esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1")

View File

@@ -119,18 +119,21 @@ void MicroWakeWord::setup() {
}
});
#ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback(
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
this->suspend_task_();
} else if (state == ota::OTA_ERROR) {
this->resume_task_();
}
});
#ifdef USE_OTA_STATE_LISTENER
ota::get_global_ota_callback()->add_global_state_listener(this);
#endif
}
#ifdef USE_OTA_STATE_LISTENER
void MicroWakeWord::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
this->suspend_task_();
} else if (state == ota::OTA_ERROR) {
this->resume_task_();
}
}
#endif
void MicroWakeWord::inference_task(void *params) {
MicroWakeWord *this_mww = (MicroWakeWord *) params;

View File

@@ -9,8 +9,13 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/ring_buffer.h"
#ifdef USE_OTA_STATE_LISTENER
#include "esphome/components/ota/ota_backend.h"
#endif
#include <freertos/event_groups.h>
#include <frontend.h>
@@ -26,13 +31,22 @@ enum State {
STOPPED,
};
class MicroWakeWord : public Component {
class MicroWakeWord : public Component
#ifdef USE_OTA_STATE_LISTENER
,
public ota::OTAGlobalStateListener
#endif
{
public:
void setup() override;
void loop() override;
float get_setup_priority() const override;
void dump_config() override;
#ifdef USE_OTA_STATE_LISTENER
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override;
#endif
void start();
void stop();

View File

@@ -9,6 +9,7 @@ from esphome.const import (
CONF_GAIN_FACTOR,
CONF_ID,
CONF_MICROPHONE,
CONF_ON_DATA,
CONF_TRIGGER_ID,
)
from esphome.core import CORE
@@ -19,8 +20,6 @@ CODEOWNERS = ["@jesserockz", "@kahrendt"]
IS_PLATFORM_COMPONENT = True
CONF_ON_DATA = "on_data"
microphone_ns = cg.esphome_ns.namespace("microphone")
Microphone = microphone_ns.class_("Microphone")

View File

@@ -83,6 +83,7 @@ void MMC5603Component::dump_config() {
ESP_LOGE(TAG, "The ID registers don't match - Is this really an MMC5603?");
}
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG, " Auto set/reset: %s", ONOFF(this->auto_set_reset_));
LOG_SENSOR(" ", "X Axis", this->x_sensor_);
LOG_SENSOR(" ", "Y Axis", this->y_sensor_);
@@ -93,7 +94,8 @@ void MMC5603Component::dump_config() {
float MMC5603Component::get_setup_priority() const { return setup_priority::DATA; }
void MMC5603Component::update() {
if (!this->write_byte(MMC56X3_CTRL0_REG, 0x01)) {
uint8_t ctrl0 = (this->auto_set_reset_) ? 0x21 : 0x01;
if (!this->write_byte(MMC56X3_CTRL0_REG, ctrl0)) {
this->status_set_warning();
return;
}

View File

@@ -25,6 +25,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice {
void set_y_sensor(sensor::Sensor *y_sensor) { y_sensor_ = y_sensor; }
void set_z_sensor(sensor::Sensor *z_sensor) { z_sensor_ = z_sensor; }
void set_heading_sensor(sensor::Sensor *heading_sensor) { heading_sensor_ = heading_sensor; }
void set_auto_set_reset(bool auto_set_reset) { auto_set_reset_ = auto_set_reset; }
protected:
MMC5603Datarate datarate_;
@@ -32,6 +33,7 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice {
sensor::Sensor *y_sensor_{nullptr};
sensor::Sensor *z_sensor_{nullptr};
sensor::Sensor *heading_sensor_{nullptr};
bool auto_set_reset_{true};
enum ErrorCode {
NONE = 0,
COMMUNICATION_FAILED,

View File

@@ -16,6 +16,8 @@ from esphome.const import (
UNIT_MICROTESLA,
)
CONF_AUTO_SET_RESET = "auto_set_reset"
DEPENDENCIES = ["i2c"]
mmc5603_ns = cg.esphome_ns.namespace("mmc5603")
@@ -54,6 +56,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema,
cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema,
cv.Optional(CONF_HEADING): heading_schema,
cv.Optional(CONF_AUTO_SET_RESET, default=True): cv.boolean,
}
)
.extend(cv.polling_component_schema("60s"))
@@ -88,3 +91,5 @@ async def to_code(config):
if CONF_HEADING in config:
sens = await sensor.new_sensor(config[CONF_HEADING])
cg.add(var.set_heading_sensor(sens))
if CONF_AUTO_SET_RESET in config:
cg.add(var.set_auto_set_reset(config[CONF_AUTO_SET_RESET]))

View File

@@ -154,7 +154,15 @@ bool MQTTComponent::send_discovery_() {
device_info[MQTT_DEVICE_MANUFACTURER] =
model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
#else
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time_ref() + ")";
static const char ver_fmt[] PROGMEM = ESPHOME_VERSION " (config hash 0x%08" PRIx32 ")";
#ifdef USE_ESP8266
char fmt_buf[sizeof(ver_fmt)];
strcpy_P(fmt_buf, ver_fmt);
const char *fmt = fmt_buf;
#else
const char *fmt = ver_fmt;
#endif
device_info[MQTT_DEVICE_SW_VERSION] = str_sprintf(fmt, App.get_config_hash());
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
#if defined(USE_ESP8266) || defined(USE_ESP32)
device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif";

View File

@@ -49,7 +49,7 @@ class Number : public EntityBase {
*/
virtual void control(float value) = 0;
CallbackManager<void(float)> state_callback_;
LazyCallbackManager<void(float)> state_callback_;
};
} // namespace esphome::number

View File

@@ -13,6 +13,8 @@ from esphome.const import (
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
OTA_STATE_LISTENER_KEY = "ota_state_listener"
CODEOWNERS = ["@esphome/core"]
AUTO_LOAD = ["md5", "safe_mode"]
@@ -86,6 +88,7 @@ BASE_OTA_SCHEMA = cv.Schema(
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
async def to_code(config):
cg.add_define("USE_OTA")
CORE.add_job(final_step)
if CORE.is_rp2040 and CORE.using_arduino:
cg.add_library("Updater", None)
@@ -119,7 +122,24 @@ async def ota_to_code(var, config):
await automation.build_automation(trigger, [(cg.uint8, "x")], conf)
use_state_callback = True
if use_state_callback:
cg.add_define("USE_OTA_STATE_CALLBACK")
request_ota_state_listeners()
def request_ota_state_listeners() -> None:
"""Request that OTA state listeners be compiled in.
Components that need to be notified about OTA state changes (start, progress,
complete, error) should call this function during their code generation.
This enables the add_state_listener() API on OTAComponent.
"""
CORE.data[OTA_STATE_LISTENER_KEY] = True
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step():
"""Final code generation step to configure optional OTA features."""
if CORE.data.get(OTA_STATE_LISTENER_KEY, False):
cg.add_define("USE_OTA_STATE_LISTENER")
FILTER_SOURCE_FILES = filter_source_files_from_platform(

View File

@@ -1,5 +1,5 @@
#pragma once
#ifdef USE_OTA_STATE_CALLBACK
#ifdef USE_OTA_STATE_LISTENER
#include "ota_backend.h"
#include "esphome/core/automation.h"
@@ -7,70 +7,64 @@
namespace esphome {
namespace ota {
class OTAStateChangeTrigger : public Trigger<OTAState> {
class OTAStateChangeTrigger final : public Trigger<OTAState>, public OTAStateListener {
public:
explicit OTAStateChangeTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (!parent->is_failed()) {
trigger(state);
}
});
explicit OTAStateChangeTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); }
void on_ota_state(OTAState state, float progress, uint8_t error) override {
if (!this->parent_->is_failed()) {
this->trigger(state);
}
}
protected:
OTAComponent *parent_;
};
class OTAStartTrigger : public Trigger<> {
template<OTAState State> class OTAStateTrigger final : public Trigger<>, public OTAStateListener {
public:
explicit OTAStartTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == OTA_STARTED && !parent->is_failed()) {
trigger();
}
});
explicit OTAStateTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); }
void on_ota_state(OTAState state, float progress, uint8_t error) override {
if (state == State && !this->parent_->is_failed()) {
this->trigger();
}
}
protected:
OTAComponent *parent_;
};
class OTAProgressTrigger : public Trigger<float> {
using OTAStartTrigger = OTAStateTrigger<OTA_STARTED>;
using OTAEndTrigger = OTAStateTrigger<OTA_COMPLETED>;
using OTAAbortTrigger = OTAStateTrigger<OTA_ABORT>;
class OTAProgressTrigger final : public Trigger<float>, public OTAStateListener {
public:
explicit OTAProgressTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == OTA_IN_PROGRESS && !parent->is_failed()) {
trigger(progress);
}
});
explicit OTAProgressTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); }
void on_ota_state(OTAState state, float progress, uint8_t error) override {
if (state == OTA_IN_PROGRESS && !this->parent_->is_failed()) {
this->trigger(progress);
}
}
protected:
OTAComponent *parent_;
};
class OTAEndTrigger : public Trigger<> {
class OTAErrorTrigger final : public Trigger<uint8_t>, public OTAStateListener {
public:
explicit OTAEndTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == OTA_COMPLETED && !parent->is_failed()) {
trigger();
}
});
}
};
explicit OTAErrorTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); }
class OTAAbortTrigger : public Trigger<> {
public:
explicit OTAAbortTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == OTA_ABORT && !parent->is_failed()) {
trigger();
}
});
void on_ota_state(OTAState state, float progress, uint8_t error) override {
if (state == OTA_ERROR && !this->parent_->is_failed()) {
this->trigger(error);
}
}
};
class OTAErrorTrigger : public Trigger<uint8_t> {
public:
explicit OTAErrorTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == OTA_ERROR && !parent->is_failed()) {
trigger(error);
}
});
}
protected:
OTAComponent *parent_;
};
} // namespace ota

View File

@@ -3,7 +3,7 @@
namespace esphome {
namespace ota {
#ifdef USE_OTA_STATE_CALLBACK
#ifdef USE_OTA_STATE_LISTENER
OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
OTAGlobalCallback *get_global_ota_callback() {
@@ -13,7 +13,12 @@ OTAGlobalCallback *get_global_ota_callback() {
return global_ota_callback;
}
void register_ota_platform(OTAComponent *ota_caller) { get_global_ota_callback()->register_ota(ota_caller); }
void OTAComponent::notify_state_(OTAState state, float progress, uint8_t error) {
for (auto *listener : this->state_listeners_) {
listener->on_ota_state(state, progress, error);
}
get_global_ota_callback()->notify_ota_state(state, progress, error, this);
}
#endif
} // namespace ota

View File

@@ -4,8 +4,8 @@
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#ifdef USE_OTA_STATE_CALLBACK
#include "esphome/core/automation.h"
#ifdef USE_OTA_STATE_LISTENER
#include <vector>
#endif
namespace esphome {
@@ -60,62 +60,75 @@ class OTABackend {
virtual bool supports_compression() = 0;
};
class OTAComponent : public Component {
#ifdef USE_OTA_STATE_CALLBACK
/** Listener interface for OTA state changes.
*
* Components can implement this interface to receive OTA state updates
* without the overhead of std::function callbacks.
*/
class OTAStateListener {
public:
void add_on_state_callback(std::function<void(ota::OTAState, float, uint8_t)> &&callback) {
this->state_callback_.add(std::move(callback));
}
virtual ~OTAStateListener() = default;
virtual void on_ota_state(OTAState state, float progress, uint8_t error) = 0;
};
class OTAComponent : public Component {
#ifdef USE_OTA_STATE_LISTENER
public:
void add_state_listener(OTAStateListener *listener) { this->state_listeners_.push_back(listener); }
protected:
/** Extended callback manager with deferred call support.
void notify_state_(OTAState state, float progress, uint8_t error);
/** Notify state with deferral to main loop (for thread safety).
*
* This adds a call_deferred() method for thread-safe execution from other tasks.
* This should be used by OTA implementations that run in separate tasks
* (like web_server OTA) to ensure listeners execute in the main loop.
*/
class StateCallbackManager : public CallbackManager<void(OTAState, float, uint8_t)> {
public:
StateCallbackManager(OTAComponent *component) : component_(component) {}
void notify_state_deferred_(OTAState state, float progress, uint8_t error) {
this->defer([this, state, progress, error]() { this->notify_state_(state, progress, error); });
}
/** Call callbacks with deferral to main loop (for thread safety).
*
* This should be used by OTA implementations that run in separate tasks
* (like web_server OTA) to ensure callbacks execute in the main loop.
*/
void call_deferred(ota::OTAState state, float progress, uint8_t error) {
component_->defer([this, state, progress, error]() { this->call(state, progress, error); });
}
private:
OTAComponent *component_;
};
StateCallbackManager state_callback_{this};
std::vector<OTAStateListener *> state_listeners_;
#endif
};
#ifdef USE_OTA_STATE_CALLBACK
#ifdef USE_OTA_STATE_LISTENER
/** Listener interface for global OTA state changes (includes OTA component pointer).
*
* Used by OTAGlobalCallback to aggregate state from multiple OTA components.
*/
class OTAGlobalStateListener {
public:
virtual ~OTAGlobalStateListener() = default;
virtual void on_ota_global_state(OTAState state, float progress, uint8_t error, OTAComponent *component) = 0;
};
/** Global callback that aggregates OTA state from all OTA components.
*
* OTA components call notify_ota_state() directly with their pointer,
* which forwards the event to all registered global listeners.
*/
class OTAGlobalCallback {
public:
void register_ota(OTAComponent *ota_caller) {
ota_caller->add_on_state_callback([this, ota_caller](OTAState state, float progress, uint8_t error) {
this->state_callback_.call(state, progress, error, ota_caller);
});
}
void add_on_state_callback(std::function<void(OTAState, float, uint8_t, OTAComponent *)> &&callback) {
this->state_callback_.add(std::move(callback));
void add_global_state_listener(OTAGlobalStateListener *listener) { this->global_listeners_.push_back(listener); }
void notify_ota_state(OTAState state, float progress, uint8_t error, OTAComponent *component) {
for (auto *listener : this->global_listeners_) {
listener->on_ota_global_state(state, progress, error, component);
}
}
protected:
CallbackManager<void(OTAState, float, uint8_t, OTAComponent *)> state_callback_{};
std::vector<OTAGlobalStateListener *> global_listeners_;
};
OTAGlobalCallback *get_global_ota_callback();
void register_ota_platform(OTAComponent *ota_caller);
// OTA implementations should use:
// - state_callback_.call() when already in main loop (e.g., esphome OTA)
// - state_callback_.call_deferred() when in separate task (e.g., web_server OTA)
// This ensures proper callback execution in all contexts.
// - notify_state_() when already in main loop (e.g., esphome OTA)
// - notify_state_deferred_() when in separate task (e.g., web_server OTA)
// This ensures proper listener execution in all contexts.
#endif
std::unique_ptr<ota::OTABackend> make_ota_backend();

View File

@@ -18,6 +18,8 @@ static const uint16_t PMS_CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automaticall
static const uint16_t PMS_CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode
static const uint16_t PMS_CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode
void PMSX003Component::setup() {}
void PMSX003Component::dump_config() {
ESP_LOGCONFIG(TAG, "PMSX003:");
LOG_SENSOR(" ", "PM1.0STD", this->pm_1_0_std_sensor_);
@@ -39,21 +41,36 @@ void PMSX003Component::dump_config() {
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
if (this->update_interval_ <= PMS_STABILISING_MS) {
ESP_LOGCONFIG(TAG, " Mode: active continuous (sensor default)");
} else {
ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles");
}
this->check_uart_settings(9600);
}
void PMSX003Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
// Initialize sensor mode on first loop
if (this->initialised_ == 0) {
if (this->update_interval_ > PMS_STABILISING_MS) {
// Long update interval: use passive mode with sleep/wake cycles
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_PASSIVE);
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP);
} else {
// Short/zero update interval: use active continuous mode
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_ACTIVE);
}
this->initialised_ = 1;
}
// If we update less often than it takes the device to stabilise, spin the fan down
// rather than running it constantly. It does take some time to stabilise, so we
// need to keep track of what state we're in.
if (this->update_interval_ > PMS_STABILISING_MS) {
if (this->initialised_ == 0) {
this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_PASSIVE);
this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP);
this->initialised_ = 1;
}
switch (this->state_) {
case PMSX003_STATE_IDLE:
// Power on the sensor now so it'll be ready when we hit the update time
@@ -248,6 +265,13 @@ void PMSX003Component::parse_data_() {
if (this->pm_particles_25um_sensor_ != nullptr)
this->pm_particles_25um_sensor_->publish_state(pm_particles_25um);
// Calculate and publish AQI if sensor is configured
if (this->aqi_sensor_ != nullptr) {
aqi::AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_);
int32_t aqi_value = calculator->get_aqi(pm_2_5_concentration, pm_10_0_concentration);
this->aqi_sensor_->publish_state(aqi_value);
}
if (this->type_ == PMSX003_TYPE_5003T) {
ESP_LOGD(TAG,
"Got PM0.3 Particles: %u Count/0.1L, PM0.5 Particles: %u Count/0.1L, PM1.0 Particles: %u Count/0.1L, "

View File

@@ -4,6 +4,7 @@
#include "esphome/core/helpers.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/aqi/aqi_calculator_factory.h"
namespace esphome {
namespace pmsx003 {
@@ -31,6 +32,7 @@ enum PMSX003State {
class PMSX003Component : public uart::UARTDevice, public Component {
public:
PMSX003Component() = default;
void setup() override;
void dump_config() override;
void loop() override;
@@ -72,6 +74,10 @@ class PMSX003Component : public uart::UARTDevice, public Component {
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void set_aqi_sensor(sensor::Sensor *aqi_sensor) { aqi_sensor_ = aqi_sensor; }
void set_aqi_calculation_type(aqi::AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; }
protected:
optional<bool> check_byte_();
void parse_data_();
@@ -115,6 +121,12 @@ class PMSX003Component : public uart::UARTDevice, public Component {
// Temperature and Humidity
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
// AQI
sensor::Sensor *aqi_sensor_{nullptr};
aqi::AQICalculatorType aqi_calc_type_;
aqi::AQICalculatorFactory aqi_calculator_factory_ = aqi::AQICalculatorFactory();
};
} // namespace pmsx003

View File

@@ -1,5 +1,6 @@
import esphome.codegen as cg
from esphome.components import sensor, uart
from esphome.components.aqi import AQI_CALCULATION_TYPE, CONF_AQI, CONF_CALCULATION_TYPE
import esphome.config_validation as cv
from esphome.const import (
CONF_FORMALDEHYDE,
@@ -20,6 +21,7 @@ from esphome.const import (
CONF_TEMPERATURE,
CONF_TYPE,
CONF_UPDATE_INTERVAL,
DEVICE_CLASS_AQI,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM1,
DEVICE_CLASS_PM10,
@@ -35,11 +37,13 @@ from esphome.const import (
CODEOWNERS = ["@ximex"]
DEPENDENCIES = ["uart"]
AUTO_LOAD = ["aqi"]
pmsx003_ns = cg.esphome_ns.namespace("pmsx003")
PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Component)
PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor)
UNIT_INDEX = "index"
TYPE_PMSX003 = "PMSX003"
TYPE_PMS5003T = "PMS5003T"
TYPE_PMS5003ST = "PMS5003ST"
@@ -77,6 +81,10 @@ def validate_pmsx003_sensors(value):
for key, types in SENSORS_TO_TYPE.items():
if key in value and value[CONF_TYPE] not in types:
raise cv.Invalid(f"{value[CONF_TYPE]} does not have {key} sensor!")
if CONF_AQI in value and CONF_PM_2_5 not in value:
raise cv.Invalid("AQI computation requires PM 2.5 sensor")
if CONF_AQI in value and CONF_PM_10_0 not in value:
raise cv.Invalid("AQI computation requires PM 10 sensor")
return value
@@ -192,6 +200,19 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AQI): sensor.sensor_schema(
unit_of_measurement=UNIT_INDEX,
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Required(CONF_CALCULATION_TYPE): cv.enum(
AQI_CALCULATION_TYPE, upper=True
),
}
),
cv.Optional(CONF_UPDATE_INTERVAL, default="0s"): validate_update_interval,
}
)
@@ -278,4 +299,9 @@ async def to_code(config):
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity_sensor(sens))
if CONF_AQI in config:
sens = await sensor.new_sensor(config[CONF_AQI])
cg.add(var.set_aqi_sensor(sens))
cg.add(var.set_aqi_calculation_type(config[CONF_AQI][CONF_CALCULATION_TYPE]))
cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))

View File

@@ -111,7 +111,7 @@ class Select : public EntityBase {
}
}
CallbackManager<void(size_t)> state_callback_;
LazyCallbackManager<void(size_t)> state_callback_;
};
} // namespace esphome::select

View File

@@ -1,4 +1,5 @@
#include "sen5x.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -154,10 +155,10 @@ void SEN5XComponent::setup() {
if (this->voc_sensor_ && this->store_baseline_) {
uint32_t combined_serial =
encode_uint24(this->serial_number_[0], this->serial_number_[1], this->serial_number_[2]);
// Hash with compilation time and serial number
// Hash with config hash, version, and serial number
// This ensures the baseline storage is cleared after OTA
// Serial numbers are unique to each sensor, so mulitple sensors can be used without conflict
uint32_t hash = fnv1_hash(App.get_compilation_time_ref() + std::to_string(combined_serial));
// Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), std::to_string(combined_serial));
this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true);
if (this->pref_.load(&this->voc_baselines_storage_)) {

View File

@@ -4,17 +4,28 @@ import esphome.codegen as cg
from esphome.components import i2c, sensirion_common, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ALGORITHM_TUNING,
CONF_GAIN_FACTOR,
CONF_GATING_MAX_DURATION_MINUTES,
CONF_HUMIDITY,
CONF_ID,
CONF_INDEX_OFFSET,
CONF_LEARNING_TIME_GAIN_HOURS,
CONF_LEARNING_TIME_OFFSET_HOURS,
CONF_NORMALIZED_OFFSET_SLOPE,
CONF_NOX,
CONF_OFFSET,
CONF_PM_1_0,
CONF_PM_2_5,
CONF_PM_4_0,
CONF_PM_10_0,
CONF_STD_INITIAL,
CONF_STORE_BASELINE,
CONF_TEMPERATURE,
CONF_TEMPERATURE_COMPENSATION,
CONF_TIME_CONSTANT,
CONF_VOC,
CONF_VOC_BASELINE,
DEVICE_CLASS_AQI,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM1,
@@ -42,18 +53,7 @@ SEN5XComponent = sen5x_ns.class_(
RhtAccelerationMode = sen5x_ns.enum("RhtAccelerationMode")
CONF_ACCELERATION_MODE = "acceleration_mode"
CONF_ALGORITHM_TUNING = "algorithm_tuning"
CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval"
CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes"
CONF_INDEX_OFFSET = "index_offset"
CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours"
CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope"
CONF_NOX = "nox"
CONF_STD_INITIAL = "std_initial"
CONF_TIME_CONSTANT = "time_constant"
CONF_VOC = "voc"
CONF_VOC_BASELINE = "voc_baseline"
# Actions

View File

@@ -76,9 +76,7 @@ StateClass Sensor::get_state_class() {
void Sensor::publish_state(float state) {
this->raw_state = state;
if (this->raw_callback_) {
this->raw_callback_->call(state);
}
this->raw_callback_.call(state);
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
@@ -91,10 +89,7 @@ void Sensor::publish_state(float state) {
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) { this->callback_.add(std::move(callback)); }
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
if (!this->raw_callback_) {
this->raw_callback_ = make_unique<CallbackManager<void(float)>>();
}
this->raw_callback_->add(std::move(callback));
this->raw_callback_.add(std::move(callback));
}
void Sensor::add_filter(Filter *filter) {

View File

@@ -125,8 +125,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
void internal_send_state_to_frontend(float state);
protected:
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
CallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
LazyCallbackManager<void(float)> raw_callback_; ///< Storage for raw state callbacks.
LazyCallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
Filter *filter_list_{nullptr}; ///< Store all active filters.

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