Compare commits

..
Author SHA1 Message Date
J. Nick Koston daeab6987f Merge branch 'dev' into esp32-variant-pin-spec 2026-10-23 19:05:01 -10:00
J. Nick Koston acb89a8676 [duco] Use the shared sntp_time id in the component test (#21548) 2026-10-23 19:03:42 -10:00
Keith BurzinskiandClaude Opus 5.5 08d231991b [core] Tighten SBOM source detection for local installs (#21529)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 22:55:48 -05:00
253db1e0d5 [hitachi_ac344] Add quiet and vertical swing mode (#18229)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-23 17:50:48 -10:00
J. Nick Koston bb07d04c90 [tests] Drive the clock in the UDP OTA no-progress test (#21192) 2026-10-23 17:41:33 -10:00
3c1e53628f [hitachi_ac344] Add mildew-proof switch (#18230)
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@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-23 17:32:27 -10:00
d515e8b2e3 [sim800l] Add on_dtmf_received trigger (#21495)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-23 17:31:36 -10:00
Keith BurzinskiandClaude Opus 5.5 49d4b83dd2 [core] Add esphome.project.sbom for SBOM supplier and contact (#21455)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 22:29:53 -05:00
Jonathan Swoboda e04dbc1881 [tests] Exercise unused options on core entity actions (#21532) 2026-10-23 22:54:18 -04:00
Jonathan Swoboda 4c62713eb2 [alarm_control_panel] Reject a code on the pending and triggered actions (#21539) 2026-10-23 22:46:33 -04:00
J. Nick Koston b3633fe53c [text_sensor] Publish PROGMEM strings without copying them at the call site (#21536) 2026-10-23 16:40:25 -10:00
b6fdccfc65 [mqtt] Honor publish_nan_as_none for climate target states (#20598)
Co-authored-by: Javier Carrasco Reyes <javier.carrasco@brightstarlottery.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-23 16:37:22 -10:00
Jonathan Swoboda 1a11424475 [tests] Cover remaining untested triggers and lvgl update actions (#21530) 2026-10-23 22:32:32 -04:00
J. Nick Koston f35395b584 [mqtt] Rebuild subscription topics when needed instead of storing them (#20762) 2026-10-24 02:12:06 +00:00
Jonathan Swoboda 6d110fd981 [lvgl] Skip the creation handler when updating widgets without update options (#21535) 2026-10-23 22:03:52 -04:00
J. Nick Koston 7c69d1b63f [gpio_expander] Move eight expanders onto the shared interrupt and loop base (#21469) 2026-10-23 15:59:18 -10:00
J. Nick Koston 1546ff022c [gpio_expander] Add a shared component base for cached expanders with an interrupt pin (#21468) 2026-10-23 15:59:18 -10:00
mrtoy-meandClaude Opus 5.5 c2911a51bd [tas58xx] Add an activate switch (#21458)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:19:12 -10:00
J. Nick Koston db16d86ee2 [mqtt] Reject subscribed topics longer than the ESP8266 and LibreTiny MQTT client accepts (#20768) 2026-10-23 15:16:12 -10:00
b401a8bf17 [toshiba] Add support for RAS-25J2KVSG-ND (#21413)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-23 14:49:19 -10:00
minimicro34 051978212c [esp32_ble] Ignore unhandled BLE key GAP events (#21457) 2026-10-23 14:01:16 -10:00
Keith BurzinskiandClaude Opus 5.5 971035d6d1 [core] Write esp-idf-sbom manifests into ESP-IDF builds (#21454)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 18:53:10 -05:00
b90e1e43a3 [tuya] fan optimistic mode (#18525)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-23 13:52:15 -10:00
Jonathan Swoboda fc6accb8d9 [core] Declare component defines missing from defines.h so clang-tidy parses the code behind them (#21521) 2026-10-23 19:45:14 -04:00
Jonathan Swoboda ade01b2c71 [midea] Clamp the follow-me temperature before rounding (#21524) 2026-10-23 19:13:33 -04:00
Jonathan Swoboda b415f86847 [lvgl] Declare the log callback's strchr result as a pointer (#21523) 2026-10-23 13:11:00 -10:00
Jonathan Swoboda 8fa9932033 [ota] Name the protected partition table method per convention and mark the cache statics (#21525) 2026-10-23 13:10:26 -10:00
Jonathan Swoboda c464ef0cc4 [safe_mode] Avoid a duplicated branch when partition access is enabled (#21526) 2026-10-23 13:09:52 -10:00
J. Nick Koston 6f091e4cf2 [light] Use the shared LED strip helpers in five addressable drivers (#21447) 2026-10-23 13:05:11 -10:00
J. Nick Koston 4a9409e299 [light] Add shared helpers for addressable LED strip drivers (#21445) 2026-10-23 13:05:11 -10:00
J. Nick Koston 53357c92f8 [light] Keep strobe and color wipe colors in flash tables (#20222) 2026-10-23 13:04:50 -10:00
J. Nick Koston ed882741bc [remote_base] Let BitTimings vary the mark per bit and send the space first (#21511) 2026-10-23 13:03:05 -10:00
Jonathan Swoboda f994fbde04 [esphome] Reject the esphome OTA platform on targets without an OTA backend (#21515) 2026-10-23 18:51:12 -04:00
Jonathan Swoboda a72adc7573 [libretiny_pwm] Reject LN882H at config time (#21498) 2026-10-23 18:23:35 -04:00
Jonathan Swoboda 55a62993c8 [ezo_pmp] Guard entity pointer uses so the hub builds without sensors (#21516) 2026-10-23 11:56:03 -10:00
Jonathan Swoboda 31a43f13d7 [ecocomfort2] Only compile the fan when a fan platform is configured (#21517) 2026-10-23 11:55:42 -10:00
3c06d41c6a [modem] Add PPPoS networking over a UART cellular modem on ESP32 (#6721)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Rapsssito <contact@rodrigomartin.dev>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-23 21:38:24 +00:00
Jonathan SwobodaandJ. Nick Koston 3be29eab35 [tests] Cover untested entity platforms, automations and platform-gated code (#21506)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-23 17:14:21 -04:00
J. Nick Koston 7a5ace9c21 Merge branch 'dev' into esp32-variant-pin-spec 2026-10-23 10:55:57 -10:00
J. Nick Koston 4db3f0407d [esp32] Note that the pin range check cannot be bypassed 2026-10-22 22:36:49 -10:00
J. Nick Koston 7e226093a5 [esp32] Make the pin spec a plain slotted class to keep import cheap 2026-10-22 22:20:08 -10:00
J. Nick Koston 3956a5b7ad [esp32] Add a data driven pin spec for the variant GPIO validators 2026-10-22 22:06:07 -10:00
248 changed files with 8996 additions and 1097 deletions

No files matched your search

+2 -1
View File
@@ -209,7 +209,8 @@ file does, and it is the authority when they disagree. The most useful starting
registered with `automation.ApplyBytes` (see Actions below), which keeps a constant payload in flash;
`TEMPLATABLE_BYTES` is for a `play()` that does more with the bytes.
* **Fixed sets of names** (status words, model names): a `PROGMEM_STRING_TABLE` (`esphome/core/progmem.h`),
logged with `get_log_str` and `LOG_STR_ARG`, copied out with `ESPHOME_strncpy_P` before `publish_state`.
logged with `get_log_str` and `LOG_STR_ARG`, published on a text sensor with
`publish_state(Table::get_progmem_str(idx, fallback))`, which copies it out of flash on ESP8266.
A `dump_config` helper that picks a name returns `const LogString *` from `LOG_STR(...)`.
* **CRCs and bit tricks** come from `esphome/core/helpers.h` (`crc8`, `crc16`, `xor_bytes`, `sum16_bytes`,
`reverse_bits`, `encode_uint16`, `encode_value_le`, `sign_extend<BITS>`, `bcd_to_uint8`, `assign_bits`); pick
+1
View File
@@ -436,6 +436,7 @@ esphome/components/modbus_controller/sensor/* @martgras
esphome/components/modbus_controller/switch/* @martgras
esphome/components/modbus_controller/text_sensor/* @martgras
esphome/components/modbus_server/* @exciton
esphome/components/modem/* @oarcher
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
esphome/components/mopeka_pro_check/* @spbrogan
esphome/components/mopeka_std_check/* @Fabian-Schmidt
+2
View File
@@ -84,3 +84,5 @@ FROM base-${BUILD_TYPE} AS final
# Copy esphome and install
COPY . /esphome
RUN uv pip install --no-cache-dir -e /esphome
# The tree has no .git; lets SBOM manifests describe it as the release it was built from
ENV ESPHOME_IS_DOCKER_IMAGE=true
+1
View File
@@ -49,6 +49,7 @@ from esphome.cpp_generator import ( # noqa: F401
static_const_array,
static_function,
templatable,
unique_global_id,
variable,
with_local_variable,
)
@@ -91,17 +91,19 @@ def alarm_control_panel_schema(
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(AlarmControlPanel),
}
)
ALARM_CONTROL_PANEL_CODE_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(AlarmControlPanel),
cv.Optional(CONF_CODE): cv.templatable(cv.string),
}
)
ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(AlarmControlPanel),
}
)
ALARM_CONTROL_PANEL_CONDITION_SCHEMA = ALARM_CONTROL_PANEL_ACTION_SCHEMA
_CALLBACK_AUTOMATIONS = (
@@ -194,7 +196,7 @@ for _name, _arm in (
):
automation.register_apply_action(
_name,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
ALARM_CONTROL_PANEL_CODE_ACTION_SCHEMA,
automation.ApplyCall(_arm),
automation.ApplyField(CONF_CODE, "set_code", cg.std_string),
call="make_call",
@@ -293,14 +293,9 @@ void BekenSPILEDStripLightOutput::set_led_params(uint8_t bit0, uint8_t bit1, uin
}
void BekenSPILEDStripLightOutput::write_state(light::LightState *state) {
// protect from refreshing too often
uint32_t now = micros();
if (this->max_refresh_rate_.has_value() && *this->max_refresh_rate_ != 0 &&
(now - this->last_refresh_) < *this->max_refresh_rate_) {
// try again next loop iteration, so that this change won't get lost
this->schedule_show();
if (this->throttle_show_(now, this->last_refresh_, this->max_refresh_rate_))
return;
}
this->last_refresh_ = now;
this->mark_shown_();
@@ -369,17 +364,6 @@ void BekenSPILEDStripLightOutput::write_state(light::LightState *state) {
this->status_clear_warning();
}
light::ESPColorView BekenSPILEDStripLightOutput::get_view_internal(int32_t index) const {
const light::ChannelColors &colors = this->channel_colors_;
uint8_t *led = this->buf_ + (index * colors.bytes_per_led());
return {led + colors.r,
led + colors.g,
led + colors.b,
colors.has_white() ? led + colors.w : nullptr,
&this->effect_data_[index],
&this->correction_};
}
void BekenSPILEDStripLightOutput::dump_config() {
ESP_LOGCONFIG(TAG,
"Beken SPI LED Strip:\n"
@@ -390,7 +374,7 @@ void BekenSPILEDStripLightOutput::dump_config() {
" Channel colors: %s\n"
" Max refresh rate: %" PRIu32 "\n"
" Number of LEDs: %u",
this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_.value_or(0), this->num_leds_);
this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_, this->num_leds_);
}
float BekenSPILEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -18,15 +18,7 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
float get_setup_priority() const override;
int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
if (this->channel_colors_.has_white()) {
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
} else {
traits.set_supported_color_modes({light::ColorMode::RGB});
}
return traits;
}
light::LightTraits get_traits() override { return this->channel_colors_.traits(); }
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; }
@@ -38,14 +30,16 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
void set_led_params(uint8_t bit0, uint8_t bit1, uint32_t spi_frequency);
void clear_effect_data() override {
for (int i = 0; i < this->size(); i++)
this->effect_data_[i] = 0;
if (this->effect_data_ != nullptr)
memset(this->effect_data_, 0, this->num_leds_);
}
void dump_config() override;
protected:
light::ESPColorView get_view_internal(int32_t index) const override;
light::ESPColorView get_view_internal(int32_t index) const override {
return this->channel_colors_.view(this->buf_, this->effect_data_, index, &this->correction_);
}
size_t get_buffer_size_() const { return this->num_leds_ * this->channel_colors_.bytes_per_led(); }
@@ -62,7 +56,7 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
light::ChannelColors channel_colors_{0, 1, 2, light::ChannelColors::NO_WHITE};
uint32_t last_refresh_{0};
optional<uint32_t> max_refresh_rate_{};
uint32_t max_refresh_rate_{0};
};
} // namespace esphome::beken_spi_led_strip
+3 -2
View File
@@ -24,7 +24,9 @@ climate::ClimateTraits ClimateIR::traits() {
return traits;
}
void ClimateIR::setup() {
void ClimateIR::setup() { this->setup_(this->restore_state_()); }
void ClimateIR::setup_(optional<climate::ClimateDeviceRestoreState> restore) {
if (this->sensor_) {
this->sensor_->add_on_state_callback([this](float state) {
this->current_temperature = state;
@@ -43,7 +45,6 @@ void ClimateIR::setup() {
}
// restore set points
auto restore = this->restore_state_();
if (restore.has_value()) {
restore->apply(this);
} else {
@@ -60,6 +60,10 @@ class ClimateIR : public Component,
/// Transmit via IR the state of this climate controller.
virtual void transmit_state() = 0;
/// setup() with the restored state already loaded, for subclasses that read it first. Load it only once:
/// every restore_state_() call takes a new preference slot on some platforms.
void setup_(optional<climate::ClimateDeviceRestoreState> restore);
// Dummy implement on_receive so implementation is optional for inheritors
bool on_receive(remote_base::RemoteReceiveData data) override { return false; };
@@ -1,5 +1,5 @@
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#if defined(USE_ESP32) && defined(USE_FAN)
#include "ecocomfort2_fan.h"
#include "esphome/core/log.h"
@@ -138,4 +138,4 @@ void Ecocomfort2Fan::on_status() {
} // namespace esphome::ecocomfort2
#endif // USE_ESP32
#endif // USE_ESP32 && USE_FAN
@@ -1,7 +1,7 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#if defined(USE_ESP32) && defined(USE_FAN)
#include "esphome/core/component.h"
#include "esphome/components/fan/fan.h"
@@ -23,4 +23,4 @@ class Ecocomfort2Fan : public fan::Fan, public Ecocomfort2Client, public Compone
} // namespace esphome::ecocomfort2
#endif // USE_ESP32
#endif // USE_ESP32 && USE_FAN
+2
View File
@@ -56,6 +56,7 @@ from esphome.const import (
from esphome.core import CORE, EsphomeError, HexInt
from esphome.core.config import BOARD_MAX_LENGTH
from esphome.coroutine import CoroPriority, coroutine_with_priority
from esphome.espidf import sbom
from esphome.espidf.component import IDFComponent, generate_idf_components
import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed
@@ -3993,6 +3994,7 @@ def copy_files():
CORE.relative_build_path("version.txt"),
__version__,
)
sbom.write_manifests()
# Remote extra build files are fetched into the shared download cache in
# one parallel batch (conditional requests skip unchanged files), then
+121 -3
View File
@@ -1,13 +1,37 @@
"""Pin checks shared by the ESP32 variant validators."""
from collections.abc import Iterable, Mapping, Set as AbstractSet
import logging
from logging import Logger
from types import MappingProxyType
from typing import Any
from esphome.components.const import CONF_HOLD_STATE
from esphome.const import CONF_NUMBER
import esphome.config_validation as cv
from esphome.const import (
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
)
from esphome.pins import check_strapping_pin
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
_NO_PINS: Mapping[int, str] = MappingProxyType({})
_INPUT_ONLY_REJECTED_MODES = (
(CONF_OUTPUT, "does not support output pin mode."),
(CONF_PULLUP, "does not support pullups."),
(CONF_PULLDOWN, "does not support pulldowns."),
)
def check_usb_jtag_pin(num: int, usb_jtag_pins: set[int], logger: Logger) -> None:
def check_usb_jtag_pin(
num: int, usb_jtag_pins: AbstractSet[int], logger: Logger
) -> None:
if num in usb_jtag_pins:
logger.warning(
"GPIO%d is used by the USB-Serial-JTAG interface."
@@ -17,10 +41,104 @@ def check_usb_jtag_pin(num: int, usb_jtag_pins: set[int], logger: Logger) -> Non
def check_usb_jtag_hold(
conf: dict[str, Any], usb_jtag_pins: set[int], logger: Logger
conf: dict[str, Any], usb_jtag_pins: AbstractSet[int], logger: Logger
) -> None:
num = conf[CONF_NUMBER]
if conf.get(CONF_HOLD_STATE) and num in usb_jtag_pins:
logger.warning(
"GPIO%d cannot hold at low level during wakeup from deep sleep.", num
)
def spi_psram_errors(variant: str, functions: Mapping[int, str]) -> dict[int, str]:
"""Error messages for pins taken by the SPI flash/PSRAM interface."""
return {
num: f"This pin cannot be used on {variant}s and is already used by the"
f" SPI/PSRAM interface (function: {function})"
for num, function in functions.items()
}
def unusable_errors(variant: str, pins: Iterable[int]) -> dict[int, str]:
"""Error messages for pins that are missing from the GPIO matrix."""
return {num: f"The pin GPIO{num} is not usable on {variant}s." for num in pins}
class ESP32PinSpec:
"""Pin facts of one ESP32 variant, with the two validators built from them.
`error_pins` fail pin validation, which `ignore_pin_validation_error` bypasses;
`fatal_pins` and pins above `max_pin` fail both validators, so the option cannot bypass them.
`input_only_label` is inserted after "GPIOn" in the input-only errors.
A plain class, not a dataclass: building a dataclass costs ~0.5 ms of import.
"""
__slots__ = (
"error_pins",
"fatal_pins",
"input_only_label",
"input_only_pins",
"max_pin",
"no_hold_pins",
"strapping_pins",
"usb_jtag_pins",
"warning_pins",
)
def __init__(
self,
*,
max_pin: int,
strapping_pins: frozenset[int],
usb_jtag_pins: frozenset[int] = frozenset(),
error_pins: Mapping[int, str] = _NO_PINS,
fatal_pins: Mapping[int, str] = _NO_PINS,
warning_pins: Mapping[int, str] = _NO_PINS,
input_only_pins: frozenset[int] = frozenset(),
input_only_label: str = "",
no_hold_pins: frozenset[int] = frozenset(),
) -> None:
self.max_pin = max_pin
self.strapping_pins = strapping_pins
self.usb_jtag_pins = usb_jtag_pins
self.error_pins = error_pins
self.fatal_pins = fatal_pins
self.warning_pins = warning_pins
self.input_only_pins = input_only_pins
self.input_only_label = input_only_label
self.no_hold_pins = no_hold_pins
def _check_pin_exists(self, num: int) -> None:
if num < 0 or num > self.max_pin:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-{self.max_pin})")
if (message := self.fatal_pins.get(num)) is not None:
raise cv.Invalid(message)
def validate_gpio_pin(self, value: int) -> int:
self._check_pin_exists(value)
if (message := self.error_pins.get(value)) is not None:
raise cv.Invalid(message)
if (message := self.warning_pins.get(value)) is not None:
_LOGGER.warning("%s", message)
check_usb_jtag_pin(value, self.usb_jtag_pins, _LOGGER)
return value
def validate_supports(self, value: ConfigType) -> ConfigType:
num = value[CONF_NUMBER]
self._check_pin_exists(num)
hold = value.get(CONF_HOLD_STATE)
if num in self.input_only_pins:
prefix = f"GPIO{num}{self.input_only_label}"
mode = value[CONF_MODE]
for key, problem in _INPUT_ONLY_REJECTED_MODES:
if mode[key]:
raise cv.Invalid(f"{prefix} {problem}", [CONF_MODE, key])
if hold:
raise cv.Invalid(
f"{prefix} is input-only and cannot be held.", [CONF_HOLD_STATE]
)
if hold and num in self.no_hold_pins:
raise cv.Invalid(f"GPIO{num} has no hold function.", [CONF_HOLD_STATE])
check_usb_jtag_hold(value, self.usb_jtag_pins, _LOGGER)
check_strapping_pin(value, self.strapping_pins, _LOGGER)
return value
+1
View File
@@ -667,6 +667,7 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
}
// Ignore these GAP events as they are not relevant for our use case
case ESP_GAP_BLE_KEY_EVT:
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: // BLE 5.0 PHY update complete
case ESP_GAP_BLE_CHANNEL_SELECT_ALGORITHM_EVT: // BLE 5.0 channel selection algorithm
@@ -157,14 +157,9 @@ void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bi
}
void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
// protect from refreshing too often
uint32_t now = micros();
auto rate = this->max_refresh_rate_.value_or(0);
if (rate != 0 && (now - this->last_refresh_) < rate) {
// try again next loop iteration, so that this change won't get lost
this->schedule_show();
if (this->throttle_show_(now, this->last_refresh_, this->max_refresh_rate_))
return;
}
this->last_refresh_ = now;
this->mark_shown_();
@@ -217,17 +212,6 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
}
}
light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index) const {
const light::ChannelColors &colors = this->channel_colors_;
uint8_t *led = this->buf_ + (index * colors.bytes_per_led());
return {led + colors.r,
led + colors.g,
led + colors.b,
colors.has_white() ? led + colors.w : nullptr,
&this->effect_data_[index],
&this->correction_};
}
void ESP32RMTLEDStripLightOutput::dump_config() {
ESP_LOGCONFIG(TAG,
"ESP32 RMT LED Strip:\n"
@@ -239,7 +223,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() {
" Channel colors: %s\n"
" Max refresh rate: %" PRIu32 "\n"
" Number of LEDs: %u",
this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_.value_or(0), this->num_leds_);
this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_, this->num_leds_);
}
float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -29,15 +29,7 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
float get_setup_priority() const override;
int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
if (this->channel_colors_.has_white()) {
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
} else {
traits.set_supported_color_modes({light::ColorMode::RGB});
}
return traits;
}
light::LightTraits get_traits() override { return this->channel_colors_.traits(); }
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_inverted(bool inverted) { this->invert_out_ = inverted; }
@@ -55,14 +47,16 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; }
void clear_effect_data() override {
for (int i = 0; i < this->size(); i++)
this->effect_data_[i] = 0;
if (this->effect_data_ != nullptr)
memset(this->effect_data_, 0, this->num_leds_);
}
void dump_config() override;
protected:
light::ESPColorView get_view_internal(int32_t index) const override;
light::ESPColorView get_view_internal(int32_t index) const override {
return this->channel_colors_.view(this->buf_, this->effect_data_, index, &this->correction_);
}
size_t get_buffer_size_() const { return this->num_leds_ * this->channel_colors_.bytes_per_led(); }
@@ -86,7 +80,7 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
light::ChannelColors channel_colors_{0, 1, 2, light::ChannelColors::NO_WHITE};
uint32_t last_refresh_{0};
optional<uint32_t> max_refresh_rate_{};
uint32_t max_refresh_rate_{0};
};
} // namespace esphome::esp32_rmt_led_strip
@@ -30,6 +30,13 @@ from esphome.const import (
CONF_SAFE_MODE,
CONF_VERSION,
CONF_WEB_SERVER,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_HOST,
PLATFORM_LN882X,
PLATFORM_RP2,
PLATFORM_RTL87XX,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
@@ -276,6 +283,31 @@ def _validate_no_password_with_encryption(config: ConfigType) -> ConfigType:
)
# Platforms with an OTA flash backend (ota_backend_factory.h)
_SUPPORTED_PLATFORMS = [
PLATFORM_ESP8266,
PLATFORM_ESP32,
PLATFORM_RP2,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
PLATFORM_HOST,
]
def _validate_platform(config: ConfigType) -> ConfigType:
if CORE.target_platform in _SUPPORTED_PLATFORMS:
return config
hint = (
f"; use '{CONF_PLATFORM}: zephyr_mcumgr' for OTA updates on nRF52"
if CORE.is_nrf52
else ""
)
raise cv.Invalid(
f"'{CONF_PLATFORM}: {DOMAIN}' is not supported on {CORE.target_platform}{hint}"
)
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for OTA component."""
from esphome.components import socket
@@ -287,6 +319,7 @@ def _consume_ota_sockets(config: ConfigType) -> ConfigType:
CONFIG_SCHEMA = cv.All(
_validate_platform,
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
@@ -1,3 +1,4 @@
from dataclasses import dataclass, field
import logging
from pathlib import Path
from typing import Any
@@ -24,6 +25,33 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "external_components"
@dataclass
class ExternalSource:
"""Origin of the components found in one source's components directory."""
components_dir: Path
url: str | None = None
ref: str | None = None
# Root of the git checkout holding components_dir
repo_dir: Path | None = None
@dataclass
class ExternalComponentsData:
sources: list[ExternalSource] = field(default_factory=list)
def _get_data() -> ExternalComponentsData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = ExternalComponentsData()
return CORE.data[DOMAIN]
def get_sources() -> list[ExternalSource]:
"""Return the sources processed for this configuration."""
return _get_data().sources
CONFIG_SCHEMA = cv.ensure_list(
{
cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA,
@@ -67,6 +95,15 @@ def _process_git_config(config: dict[str, Any], refresh: TimePeriodSeconds) -> P
"Could not find components folder for source. Please check the source contains a 'components' or 'esphome/components' folder"
)
get_sources().append(
ExternalSource(
components_dir=components_dir,
# Ends up in SBOMs that are shared outside
url=git.strip_url_credentials(config[CONF_URL]),
ref=config.get(CONF_REF),
repo_dir=repo_dir,
)
)
return components_dir
@@ -106,6 +143,7 @@ def _process_single_config(config: dict[str, Any]) -> None:
)
elif conf[CONF_TYPE] == TYPE_LOCAL:
components_dir = Path(CORE.relative_config_path(conf[CONF_PATH]))
get_sources().append(ExternalSource(components_dir=components_dir))
else:
raise NotImplementedError
+46
View File
@@ -48,12 +48,16 @@ void EzoPMP::update() {
}
if (this->is_first_read_) {
#ifdef USE_TEXT_SENSOR
this->queue_command_(EZO_PMP_COMMAND_READ_CALIBRATION_STATUS, 0, 0, (bool) this->calibration_status_);
#endif
#ifdef USE_SENSOR
this->queue_command_(EZO_PMP_COMMAND_READ_MAX_FLOW_RATE, 0, 0, (bool) this->max_flow_rate_);
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, (bool) this->total_volume_dosed_);
this->queue_command_(EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED, 0, 0,
(bool) this->absolute_total_volume_dosed_);
#endif
this->queue_command_(EZO_PMP_COMMAND_READ_PAUSE_STATUS, 0, 0, true);
this->is_first_read_ = false;
}
@@ -61,6 +65,7 @@ void EzoPMP::update() {
if (!this->is_waiting_ && this->peek_next_command_() == EZO_PMP_COMMAND_NONE) {
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
#ifdef USE_SENSOR
if (this->is_dosing_flag_) {
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
this->queue_command_(EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED, 0, 0, (bool) this->total_volume_dosed_);
@@ -69,6 +74,7 @@ void EzoPMP::update() {
}
this->queue_command_(EZO_PMP_COMMAND_READ_PUMP_VOLTAGE, 0, 0, (bool) this->pump_voltage_);
#endif
} else {
ESP_LOGV(TAG, "Not Scheduling new Command during update()");
}
@@ -192,51 +198,68 @@ void EzoPMP::read_command_result_() {
if (parsed_third_parameter.has_value())
this->is_dosing_flag_ = parsed_third_parameter.value_or(0) == 1;
#ifdef USE_BINARY_SENSOR
if (this->is_dosing_)
this->is_dosing_->publish_state(this->is_dosing_flag_);
#endif
#ifdef USE_SENSOR
if (parsed_second_parameter.has_value() && this->last_volume_requested_) {
this->last_volume_requested_->publish_state(parsed_second_parameter.value_or(0));
}
#endif
#ifdef USE_TEXT_SENSOR
if (!this->is_dosing_flag_ && !this->is_paused_flag_) {
// If pump is not paused and not dispensing
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE)
this->dosing_mode_->publish_state(DOSING_MODE_NONE);
}
#endif
break;
case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53)
#ifdef USE_SENSOR
if (parsed_first_parameter.has_value() && (bool) this->current_volume_dosed_) {
this->current_volume_dosed_->publish_state(parsed_first_parameter.value_or(0));
}
#endif
break;
case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE: // Constant Flow Rate (page 57)
#ifdef USE_SENSOR
if (parsed_second_parameter.has_value() && this->max_flow_rate_)
this->max_flow_rate_->publish_state(parsed_second_parameter.value_or(0));
#endif
break;
case EZO_PMP_COMMAND_READ_PAUSE_STATUS: // Pause (page 61)
if (parsed_second_parameter.has_value())
this->is_paused_flag_ = parsed_second_parameter.value_or(0) == 1;
#ifdef USE_BINARY_SENSOR
if (this->is_paused_)
this->is_paused_->publish_state(this->is_paused_flag_);
#endif
break;
case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED: // Total Volume Dispensed (page 64)
#ifdef USE_SENSOR
if (parsed_second_parameter.has_value() && this->total_volume_dosed_)
this->total_volume_dosed_->publish_state(parsed_second_parameter.value_or(0));
#endif
break;
case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED: // Total Volume Dispensed (page 64)
#ifdef USE_SENSOR
if (parsed_second_parameter.has_value() && this->absolute_total_volume_dosed_)
this->absolute_total_volume_dosed_->publish_state(parsed_second_parameter.value_or(0));
#endif
break;
case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS: // Calibration (page 65)
#ifdef USE_TEXT_SENSOR
if (parsed_second_parameter.has_value() && this->calibration_status_) {
if (parsed_second_parameter.value_or(0) == 1) {
this->calibration_status_->publish_state("Fixed Volume");
@@ -248,41 +271,56 @@ void EzoPMP::read_command_result_() {
this->calibration_status_->publish_state("Uncalibrated");
}
}
#endif
break;
case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE: // Pump Voltage (page 67)
#ifdef USE_SENSOR
if (parsed_second_parameter.has_value() && this->pump_voltage_)
this->pump_voltage_->publish_state(parsed_second_parameter.value_or(0));
#endif
break;
// Non-Read Commands
case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55)
#ifdef USE_TEXT_SENSOR
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME)
this->dosing_mode_->publish_state(DOSING_MODE_VOLUME);
#endif
break;
case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56)
#ifdef USE_TEXT_SENSOR
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME_OVER_TIME)
this->dosing_mode_->publish_state(DOSING_MODE_VOLUME_OVER_TIME);
#endif
break;
case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57)
#ifdef USE_TEXT_SENSOR
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONSTANT_FLOW_RATE)
this->dosing_mode_->publish_state(DOSING_MODE_CONSTANT_FLOW_RATE);
#endif
break;
case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54)
#ifdef USE_TEXT_SENSOR
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONTINUOUS)
this->dosing_mode_->publish_state(DOSING_MODE_CONTINUOUS);
#endif
break;
case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62)
this->is_paused_flag_ = false;
#ifdef USE_BINARY_SENSOR
if (this->is_paused_)
this->is_paused_->publish_state(this->is_paused_flag_);
#endif
#ifdef USE_TEXT_SENSOR
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE)
this->dosing_mode_->publish_state(DOSING_MODE_NONE);
#endif
break;
case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS:
@@ -489,25 +527,33 @@ void EzoPMP::find() { this->queue_command_(EZO_PMP_COMMAND_FIND, 0, 0, true); }
void EzoPMP::dose_continuously() {
this->queue_command_(EZO_PMP_COMMAND_DOSE_CONTINUOUSLY, 0, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
#ifdef USE_SENSOR
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
#endif
}
void EzoPMP::dose_volume(double volume) {
this->queue_command_(EZO_PMP_COMMAND_DOSE_VOLUME, volume, 0, true);
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
#ifdef USE_SENSOR
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
#endif
}
void EzoPMP::dose_volume_over_time(double volume, int duration) {
this->queue_command_(EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME, volume, duration, true);
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
#ifdef USE_SENSOR
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
#endif
}
void EzoPMP::dose_with_constant_flow_rate(double volume, int duration) {
this->queue_command_(EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE, volume, duration, true);
this->queue_command_(EZO_PMP_COMMAND_READ_DOSING, 0, 0, true);
#ifdef USE_SENSOR
this->queue_command_(EZO_PMP_COMMAND_READ_SINGLE_REPORT, 0, 0, (bool) this->current_volume_dosed_);
#endif
}
void EzoPMP::set_calibration_volume(double volume) {
@@ -22,3 +22,9 @@ def validate_interrupt_pin(value: ConfigType) -> ConfigType:
f"Remove the '{CONF_INTERRUPT_PIN}' to fall back to polling."
)
return value
# Schema of a CachedGpioExpanderComponent; to_code sets the pin with cg.add_pin().
INTERRUPT_EXPANDER_SCHEMA = cv.COMPONENT_SCHEMA.extend(
{cv.Optional(CONF_INTERRUPT_PIN): validate_interrupt_pin}
)
@@ -0,0 +1,29 @@
#include "cached_gpio_component.h"
namespace esphome::gpio_expander {
// Not a member of the template: GCC 10 (ESP8266) ignores section attributes on template instantiations.
static void IRAM_ATTR interrupt_pin_isr(Component *arg) { arg->enable_loop_soon_any_context(); }
bool InterruptExpanderComponent::attach_interrupt_pin_() {
// Polling mode enables the loop in pin_mode() for the first input; interrupt mode from the ISR.
this->disable_loop();
if (this->interrupt_pin_ == nullptr)
return false;
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&interrupt_pin_isr, static_cast<Component *>(this),
gpio::INTERRUPT_FALLING_EDGE);
// An INT line already low at attach gives no falling edge, so read the inputs now
if (!this->interrupt_pin_->digital_read())
this->enable_loop();
return true;
}
// Input changes that straddle the read leave INT asserted without a new falling edge, so keep looping
// until the line is released or the expander would keep stale state forever.
void InterruptExpanderComponent::disable_loop_when_released_() {
if (this->interrupt_pin_->digital_read())
this->disable_loop();
}
} // namespace esphome::gpio_expander
@@ -0,0 +1,50 @@
#pragma once
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
namespace esphome::gpio_expander {
/// Component side of a cached GPIO expander with an optional active-low INT line. Not a template, so the
/// ISR and the shared code are emitted once for every expander.
class InterruptExpanderComponent : public Component {
public:
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
float get_setup_priority() const override { return setup_priority::IO; }
protected:
/// Attach the falling-edge ISR when there is an interrupt pin, then disable the loop until an input
/// needs it. Returns whether the interrupt pin is in use.
bool attach_interrupt_pin_();
/// The loop runs again after the INT line falls; stop it once the line is released (HIGH).
/// Only called with an interrupt pin.
void disable_loop_when_released_();
/// Call from pin_mode() for an input pin: without an interrupt pin the loop polls instead.
void enable_polling_loop_() {
if (this->interrupt_pin_ == nullptr)
this->enable_loop();
}
InternalGPIOPin *interrupt_pin_{nullptr};
};
/// Base for expanders that cache reads per loop. With an interrupt pin the cache stays valid until INT fires.
template<typename T, uint16_t N>
class CachedGpioExpanderComponent : public InterruptExpanderComponent, public CachedGpioExpander<T, N> {
public:
void loop() override {
this->reset_pin_cache_();
if (this->interrupt_pin_ != nullptr)
this->disable_loop_when_released_();
}
protected:
/// Call at the end of setup().
void setup_interrupt_pin_() {
if (this->attach_interrupt_pin_())
this->set_invalidate_on_read_(false);
}
};
} // namespace esphome::gpio_expander
@@ -1,6 +1,8 @@
#include "hitachi_ac344.h"
#include "esphome/core/helpers.h"
#include <cstring>
namespace esphome::hitachi_ac344 {
ESPHOME_LOG_TAG(TAG, "climate.hitachi_ac344");
@@ -101,21 +103,7 @@ void HitachiClimate::set_fan_(uint8_t speed) {
remote_state_[29] = 0x01;
}
void HitachiClimate::set_swing_v_toggle_(bool on) {
uint8_t button = get_button_(); // Get the current button value.
if (on) {
button = HITACHI_AC344_BUTTON_SWINGV; // Set the button to SwingV.
} else if (button == HITACHI_AC344_BUTTON_SWINGV) { // Asked to unset it
// It was set previous, so use Power as a default
button = HITACHI_AC344_BUTTON_POWER;
}
set_button_(button);
}
bool HitachiClimate::get_swing_v_toggle_() { return get_button_() == HITACHI_AC344_BUTTON_SWINGV; }
void HitachiClimate::set_swing_v_(bool on) {
set_swing_v_toggle_(on); // Set the button value.
assign_bits(this->remote_state_[HITACHI_AC344_SWINGV_BYTE], 1 << HITACHI_AC344_SWINGV_OFFSET, on);
}
@@ -129,18 +117,59 @@ void HitachiClimate::set_swing_h_(uint8_t position) {
return;
}
set_bits(&remote_state_[HITACHI_AC344_SWINGH_BYTE], HITACHI_AC344_SWINGH_OFFSET, HITACHI_AC344_SWINGH_SIZE, position);
set_button_(HITACHI_AC344_BUTTON_SWINGH);
}
uint8_t HitachiClimate::get_swing_h_() {
return GETBITS8(remote_state_[HITACHI_AC344_SWINGH_BYTE], HITACHI_AC344_SWINGH_OFFSET, HITACHI_AC344_SWINGH_SIZE);
}
uint8_t HitachiClimate::get_button_() { return remote_state_[HITACHI_AC344_BUTTON_BYTE]; }
void HitachiClimate::set_button_(uint8_t button) { remote_state_[HITACHI_AC344_BUTTON_BYTE] = button; }
void HitachiClimate::transmit_state() {
static bool swing_vertical(climate::ClimateSwingMode mode) {
return mode == climate::CLIMATE_SWING_VERTICAL || mode == climate::CLIMATE_SWING_BOTH;
}
bool HitachiClimate::get_mildew_proof() const {
return GETBIT8(this->remote_state_[HITACHI_AC344_MILDEWPROOF_BYTE], HITACHI_AC344_MILDEWPROOF_OFFSET);
}
void HitachiClimate::set_mildew_proof(bool on) {
// A repeated write resends the frame, so a missed burst can be retried
assign_bits(this->remote_state_[HITACHI_AC344_MILDEWPROOF_BYTE], 1 << HITACHI_AC344_MILDEWPROOF_OFFSET, on);
this->transmit_state_(HITACHI_AC344_BUTTON_MILDEWPROOF);
this->publish_mildew_proof_();
}
void HitachiClimate::publish_mildew_proof_() {
#ifdef USE_SWITCH
if (this->mildew_proof_switch_ != nullptr)
this->mildew_proof_switch_->publish_state(this->get_mildew_proof());
#endif
}
void HitachiClimate::setup() {
// Seed the frame before the base publishes the restored state; an on_state automation may transmit from that
// callback. Nothing is sent at boot, the restored settings ride along with the next frame.
auto restore = this->restore_state_();
if (restore.has_value()) {
set_swing_v_(swing_vertical(restore->swing_mode));
}
#ifdef USE_SWITCH
// The restored value is the switch state, which an inverted switch flips relative to the frame bit
if (this->mildew_proof_switch_ != nullptr) {
if (auto initial = this->mildew_proof_switch_->get_initial_state_with_restore_mode()) {
assign_bits(this->remote_state_[HITACHI_AC344_MILDEWPROOF_BYTE], 1 << HITACHI_AC344_MILDEWPROOF_OFFSET,
*initial != this->mildew_proof_switch_->is_inverted());
}
}
#endif
this->setup_(restore);
this->publish_mildew_proof_();
}
void HitachiClimate::transmit_state() { this->transmit_state_(HITACHI_AC344_BUTTON_POWER); }
void HitachiClimate::transmit_state_(uint8_t button) {
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
set_mode_(HITACHI_AC344_MODE_COOL);
@@ -167,6 +196,9 @@ void HitachiClimate::transmit_state() {
set_temp_(static_cast<uint8_t>(this->target_temperature));
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
case climate::CLIMATE_FAN_QUIET:
set_fan_(HITACHI_AC344_FAN_MIN);
break;
case climate::CLIMATE_FAN_LOW:
set_fan_(HITACHI_AC344_FAN_LOW);
break;
@@ -182,27 +214,18 @@ void HitachiClimate::transmit_state() {
set_fan_(HITACHI_AC344_FAN_AUTO);
}
switch (this->swing_mode) {
case climate::CLIMATE_SWING_BOTH:
set_swing_v_(true);
set_swing_h_(HITACHI_AC344_SWINGH_AUTO);
break;
case climate::CLIMATE_SWING_VERTICAL:
set_swing_v_(true);
set_swing_h_(HITACHI_AC344_SWINGH_MIDDLE);
break;
case climate::CLIMATE_SWING_HORIZONTAL:
set_swing_v_(false);
set_swing_h_(HITACHI_AC344_SWINGH_AUTO);
break;
case climate::CLIMATE_SWING_OFF:
set_swing_v_(false);
set_swing_h_(HITACHI_AC344_SWINGH_MIDDLE);
break;
}
const bool swing_v = swing_vertical(this->swing_mode);
const bool swing_h =
this->swing_mode == climate::CLIMATE_SWING_HORIZONTAL || this->swing_mode == climate::CLIMATE_SWING_BOTH;
set_swing_h_(swing_h ? HITACHI_AC344_SWINGH_AUTO : HITACHI_AC344_SWINGH_MIDDLE);
// The unit ignores the swing bit and toggles on the swing button, so only a vertical swing change presses it;
// everything else sends the given button
set_button_(swing_v != get_swing_v_() ? HITACHI_AC344_BUTTON_SWINGV : button);
set_swing_v_(swing_v);
// TODO: find change value to set button, now always set to power button
set_button_(HITACHI_AC344_BUTTON_POWER);
// The remote sends this when powering off with mildew proof on
if (this->mode == climate::CLIMATE_MODE_OFF && this->get_mildew_proof())
remote_state_[9] = 0xA5;
invert_byte_pairs(remote_state_ + 3, HITACHI_AC344_STATE_LENGTH - 3);
@@ -275,6 +298,8 @@ bool HitachiClimate::parse_fan_(const uint8_t remote_state[]) {
ESP_LOGV(TAG, "Fan: %02X %02X", remote_state[HITACHI_AC344_FAN_BYTE], fan_mode);
switch (fan_mode) {
case HITACHI_AC344_FAN_MIN:
this->fan_mode = climate::CLIMATE_FAN_QUIET;
break;
case HITACHI_AC344_FAN_LOW:
this->fan_mode = climate::CLIMATE_FAN_LOW;
break;
@@ -298,11 +323,13 @@ bool HitachiClimate::parse_swing_(const uint8_t remote_state[]) {
uint8_t swing_modeh =
GETBITS8(remote_state[HITACHI_AC344_SWINGH_BYTE], HITACHI_AC344_SWINGH_OFFSET, HITACHI_AC344_SWINGH_SIZE);
ESP_LOGV(TAG, "SwingH: %02X %02X", remote_state[HITACHI_AC344_SWINGH_BYTE], swing_modeh);
bool swing_v = GETBIT8(remote_state[HITACHI_AC344_SWINGV_BYTE], HITACHI_AC344_SWINGV_OFFSET);
ESP_LOGV(TAG, "SwingV: %02X %s", remote_state[HITACHI_AC344_SWINGV_BYTE], ONOFF(swing_v));
if ((swing_modeh & 0x3) == 0x3) {
this->swing_mode = climate::CLIMATE_SWING_OFF;
if (swing_modeh != HITACHI_AC344_SWINGH_AUTO) {
this->swing_mode = swing_v ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
} else {
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
this->swing_mode = swing_v ? climate::CLIMATE_SWING_BOTH : climate::CLIMATE_SWING_HORIZONTAL;
}
return true;
@@ -345,9 +372,10 @@ bool HitachiClimate::on_receive(remote_base::RemoteReceiveData data) {
this->parse_fan_(recv_state);
// parse swingv
this->parse_swing_(recv_state);
// Copy first so an on_state automation transmits from the received frame
memcpy(remote_state_, recv_state, HITACHI_AC344_STATE_LENGTH);
this->publish_state();
for (uint8_t i = 0; i < HITACHI_AC344_STATE_LENGTH; i++)
remote_state_[i] = recv_state[i];
this->publish_mildew_proof_();
return true;
}
@@ -2,6 +2,9 @@
#include "esphome/core/log.h"
#include "esphome/components/climate_ir/climate_ir.h"
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
namespace esphome::hitachi_ac344 {
@@ -79,9 +82,18 @@ class HitachiClimate final : public climate_ir::ClimateIR {
public:
HitachiClimate()
: climate_ir::ClimateIR(HITACHI_AC344_TEMP_MIN, HITACHI_AC344_TEMP_MAX, 1.0F, true, true,
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL}) {}
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_QUIET, climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
bool get_mildew_proof() const;
/// Mildew proof keeps the fan running after power off to dry the unit
void set_mildew_proof(bool on);
#ifdef USE_SWITCH
void set_mildew_proof_switch(switch_::Switch *sw) { this->mildew_proof_switch_ = sw; }
#endif
void setup() override;
protected:
uint8_t remote_state_[HITACHI_AC344_STATE_LENGTH]{0x01, 0x10, 0x00, 0x40, 0x00, 0xFF, 0x00, 0xCC, 0x00, 0x00, 0x00,
@@ -89,8 +101,14 @@ class HitachiClimate final : public climate_ir::ClimateIR {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t previous_temp_{27};
#ifdef USE_SWITCH
switch_::Switch *mildew_proof_switch_{nullptr};
#endif
// Transmit via IR the state of this climate controller.
void transmit_state() override;
// The frame with the button byte the remote sends for this change
void transmit_state_(uint8_t button);
void publish_mildew_proof_();
bool get_power_();
void set_power_(bool on);
uint8_t get_mode_();
@@ -98,13 +116,10 @@ class HitachiClimate final : public climate_ir::ClimateIR {
void set_temp_(uint8_t celsius, bool set_previous = true);
uint8_t get_fan_();
void set_fan_(uint8_t speed);
void set_swing_v_toggle_(bool on);
bool get_swing_v_toggle_();
void set_swing_v_(bool on);
bool get_swing_v_();
void set_swing_h_(uint8_t position);
uint8_t get_swing_h_();
uint8_t get_button_();
void set_button_(uint8_t button);
// Handle received IR Buffer
bool on_receive(remote_base::RemoteReceiveData data) override;
@@ -0,0 +1,30 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from ..climate import HitachiClimate
CONF_HITACHI_AC344_ID = "hitachi_ac344_id"
CONF_MILDEW_PROOF = "mildew_proof"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HITACHI_AC344_ID): cv.use_id(HitachiClimate),
cv.Required(CONF_MILDEW_PROOF): switch.switch_schema(
switch.ApplySwitch,
icon="mdi:shield-check-outline",
entity_category=ENTITY_CATEGORY_CONFIG,
default_restore_mode="RESTORE_DEFAULT_OFF",
),
}
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_HITACHI_AC344_ID])
switches = switch.sub_switches(config, parent=hub)
await switches(
CONF_MILDEW_PROOF, hub.set_mildew_proof_switch, apply="set_mildew_proof(state)"
)
+2 -1
View File
@@ -322,8 +322,9 @@ class I2CDevice {
bool update_register(uint8_t a_register, uint8_t mask, uint8_t value);
protected:
I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance
// Last so a derived class can place its byte-sized members in the padding after it
uint8_t address_{0x00}; ///< store the address of the device on the bus
I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance
};
} // namespace esphome::i2c
@@ -149,17 +149,12 @@ bool I2SClocklessLedStrip::allocate_buffers_() {
RAMAllocator<uint8_t> allocator(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
this->i2s_data_ = allocator.allocate(i2s_data_bytes);
if (this->i2s_data_ != nullptr) {
this->color_data_ = allocator.allocate(this->color_data_bytes_);
// The effect data follows the color data
this->color_data_ = allocator.allocate(this->color_data_bytes_ + this->num_leds_);
if (this->color_data_ != nullptr) {
this->effect_data_ = allocator.allocate(this->num_leds_);
if (this->effect_data_ != nullptr) {
memset(this->i2s_data_, 0, i2s_data_bytes);
memset(this->color_data_, 0, this->color_data_bytes_);
memset(this->effect_data_, 0, this->num_leds_);
return true;
}
allocator.deallocate(this->color_data_, this->color_data_bytes_);
this->color_data_ = nullptr;
memset(this->i2s_data_, 0, i2s_data_bytes);
memset(this->color_data_, 0, this->color_data_bytes_ + this->num_leds_);
return true;
}
allocator.deallocate(this->i2s_data_, i2s_data_bytes);
this->i2s_data_ = nullptr;
@@ -167,16 +162,6 @@ bool I2SClocklessLedStrip::allocate_buffers_() {
return false;
}
light::LightTraits I2SClocklessLedStrip::get_traits() {
auto traits = light::LightTraits();
if (this->channel_colors_.has_white()) {
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
} else {
traits.set_supported_color_modes({light::ColorMode::RGB});
}
return traits;
}
void I2SClocklessLedStrip::write_state(light::LightState *state) {
if (this->is_failed())
return;
@@ -236,19 +221,6 @@ bool IRAM_ATTR HOT I2SClocklessLedStrip::i2s_on_sent_callback(i2s_chan_handle_t
return false;
}
void I2SClocklessLedStrip::clear_effect_data() { memset(this->effect_data_, 0, this->num_leds_); }
light::ESPColorView I2SClocklessLedStrip::get_view_internal(int32_t index) const {
const light::ChannelColors &colors = this->channel_colors_;
uint8_t *led = this->color_data_ + (index * colors.bytes_per_led());
return {led + colors.r,
led + colors.g,
led + colors.b,
colors.has_white() ? led + colors.w : nullptr,
&this->effect_data_[index],
&this->correction_};
}
} // namespace esphome::i2s_clockless_led_strip
#endif
@@ -8,6 +8,7 @@
#include <atomic>
#include <cstdint>
#include <cstring>
#include "esphome/core/component.h"
#include "esphome/core/log.h"
@@ -27,20 +28,27 @@ class I2SClocklessLedStrip final : public light::AddressableLight {
float get_setup_priority() const override;
void setup() override;
light::LightTraits get_traits() override;
light::LightTraits get_traits() override { return this->channel_colors_.traits(); }
int32_t size() const override { return this->num_leds_; }
void clear_effect_data() override;
void clear_effect_data() override {
if (uint8_t *effect = this->effect_data_())
memset(effect, 0, this->num_leds_);
}
void write_state(light::LightState *state) override;
protected:
static bool i2s_on_sent_callback(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx);
bool allocate_buffers_();
light::ESPColorView get_view_internal(int32_t index) const override;
uint8_t *effect_data_() const {
return this->color_data_ == nullptr ? nullptr : this->color_data_ + this->color_data_bytes_;
}
light::ESPColorView get_view_internal(int32_t index) const override {
return this->channel_colors_.view(this->color_data_, this->effect_data_(), index, &this->correction_);
}
const size_t color_data_bytes_;
uint8_t *effect_data_{nullptr};
uint8_t *color_data_{nullptr};
uint8_t *i2s_data_{nullptr};
size_t i2s_data_sent_{0};
+8 -6
View File
@@ -1066,19 +1066,20 @@ bool LD6002BComponent::queue_command_(uint16_t type, const uint8_t *data, uint8_
ESP_LOGW(TAG, "Command data too large: %u", len);
return false;
}
if (this->cmd_queue_.size() >= CMD_QUEUE_SIZE) {
if (this->cmd_count_ >= CMD_QUEUE_SIZE) {
ESP_LOGW(TAG, "Command queue full, dropping command 0x%04X", type);
return false;
}
PendingCommand cmd;
PendingCommand &cmd = this->cmd_queue_[this->cmd_tail_];
cmd.type = type;
cmd.len = len;
if (len > 0 && data != nullptr) {
std::memcpy(cmd.data.data(), data, len);
}
this->cmd_queue_.push(cmd);
this->cmd_tail_ = (this->cmd_tail_ + 1) % CMD_QUEUE_SIZE;
this->cmd_count_++;
this->process_command_queue_();
return true;
}
@@ -1145,11 +1146,12 @@ void LD6002BComponent::process_command_queue_() {
return;
}
if (this->cmd_queue_.empty())
if (this->cmd_count_ == 0)
return;
this->active_command_ = this->cmd_queue_.front();
this->cmd_queue_.pop();
this->active_command_ = this->cmd_queue_[this->cmd_head_];
this->cmd_head_ = (this->cmd_head_ + 1) % CMD_QUEUE_SIZE;
this->cmd_count_--;
this->send_generation_++;
this->retries_left_ = CMD_MAX_RETRIES;
+4 -1
View File
@@ -419,7 +419,10 @@ class LD6002BComponent : public Component, public uart::UARTDevice {
// A reply cannot trail the frame that earned it for longer than this; the field worst case is ~726ms.
static constexpr uint32_t STALE_ACK_MAX_AGE_MS = 1000;
StaticRingBuffer<PendingCommand, CMD_QUEUE_SIZE> cmd_queue_;
std::array<PendingCommand, CMD_QUEUE_SIZE> cmd_queue_{};
uint8_t cmd_head_{0};
uint8_t cmd_tail_{0};
uint8_t cmd_count_{0};
bool command_active_{false};
bool command_sent_{false};
PendingCommand active_command_{};
+12 -8
View File
@@ -1,6 +1,7 @@
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import output
from esphome.components import libretiny, output
from esphome.components.libretiny.const import FAMILY_LN882H
import esphome.config_validation as cv
from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_PIN
from esphome.types import ConfigType
@@ -12,13 +13,16 @@ LibreTinyPWM = libretinypwm_ns.class_("LibreTinyPWM", output.FloatOutput, cg.Com
validate_frequency = cv.All(cv.frequency, cv.float_range(min=0, min_included=False))
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(LibreTinyPWM),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_FREQUENCY, default="1kHz"): validate_frequency,
}
).extend(cv.COMPONENT_SCHEMA)
CONFIG_SCHEMA = cv.All(
libretiny.only_on_family(unsupported=FAMILY_LN882H),
output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(LibreTinyPWM),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_FREQUENCY, default="1kHz"): validate_frequency,
}
).extend(cv.COMPONENT_SCHEMA),
)
async def to_code(config: ConfigType) -> None:
@@ -134,6 +134,13 @@ class AddressableLight : public LightOutput, public Component {
#endif
}
virtual ESPColorView get_view_internal(int32_t index) const = 0;
// Queues the frame for the next loop and returns true until interval_us has passed since last_us
bool throttle_show_(uint32_t now_us, uint32_t last_us, uint32_t interval_us) {
if (now_us - last_us >= interval_us)
return false;
this->schedule_show();
return true;
}
#ifdef USE_LIGHT_POWER_ESTIMATION
friend class AddressableLightState;
@@ -1,8 +1,10 @@
#pragma once
#include <type_traits>
#include <utility>
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/light/light_state.h"
#include "esphome/components/light/addressable_light.h"
@@ -101,17 +103,28 @@ class AddressableRainbowLightEffect : public AddressableLightEffect {
uint16_t width_{50};
};
/// One wipe color. Codegen keeps the colors in a flash table, so it is read with progmem_memcpy.
struct AddressableColorWipeEffectColor {
uint8_t r, g, b, w;
bool random;
size_t num_leds;
uint8_t random_slot; ///< index into the effect's random colors, or NO_RANDOM_SLOT
bool gradient;
uint32_t num_leds;
};
static_assert(std::is_trivially_copyable_v<AddressableColorWipeEffectColor>,
"AddressableColorWipeEffectColor is copied out of flash");
static_assert(sizeof(AddressableColorWipeEffectColor) % sizeof(uint32_t) == 0,
"AddressableColorWipeEffectColor must be whole words for ESP8266 flash reads");
static constexpr uint8_t NO_RANDOM_SLOT = 0xFF;
class AddressableColorWipeEffect : public AddressableLightEffect {
public:
explicit AddressableColorWipeEffect(const char *name) : AddressableLightEffect(name) {}
void set_colors(const std::initializer_list<AddressableColorWipeEffectColor> &colors) { this->colors_ = colors; }
/// Codegen only: point at the generated flash table of colors, which must outlive the effect.
/// Random colors keep their current value in a RAM array, one slot per random entry.
void set_colors(const AddressableColorWipeEffectColor *colors, size_t count, Color *random_colors) {
this->colors_ = {colors, count};
this->random_colors_ = random_colors;
}
void set_add_led_interval(uint32_t add_led_interval) { this->add_led_interval_ = add_led_interval; }
void set_reverse(bool reverse) { this->reverse_ = reverse; }
void apply(AddressableLight &it, const Color &current_color) override {
@@ -124,12 +137,11 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
} else {
it.shift_right(1);
}
const AddressableColorWipeEffectColor &color = this->colors_[this->at_color_];
Color esp_color = Color(color.r, color.g, color.b, color.w);
const AddressableColorWipeEffectColor color = this->entry_(this->at_color_);
Color esp_color = this->color_of_(color);
if (color.gradient) {
size_t next_color_index = (this->at_color_ + 1) % this->colors_.size();
const AddressableColorWipeEffectColor &next_color = this->colors_[next_color_index];
const Color next_esp_color = Color(next_color.r, next_color.g, next_color.b, next_color.w);
const Color next_esp_color = this->color_of_(this->entry_(next_color_index));
uint8_t gradient = 255 * ((float) this->leds_added_ / color.num_leds);
esp_color = esp_color.gradient(next_esp_color, gradient);
}
@@ -141,19 +153,32 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
if (++this->leds_added_ >= color.num_leds) {
this->leds_added_ = 0;
this->at_color_ = (this->at_color_ + 1) % this->colors_.size();
AddressableColorWipeEffectColor &new_color = this->colors_[this->at_color_];
if (new_color.random) {
const AddressableColorWipeEffectColor new_color = this->entry_(this->at_color_);
if (new_color.random_slot != NO_RANDOM_SLOT) {
Color c = Color::random_color();
new_color.r = c.r;
new_color.g = c.g;
new_color.b = c.b;
Color &slot = this->random_colors_[new_color.random_slot];
slot.r = c.r;
slot.g = c.g;
slot.b = c.b;
}
}
it.schedule_show();
}
protected:
FixedVector<AddressableColorWipeEffectColor> colors_;
AddressableColorWipeEffectColor entry_(size_t index) const {
AddressableColorWipeEffectColor entry;
progmem_memcpy(&entry, &this->colors_[index], sizeof(entry));
return entry;
}
Color color_of_(const AddressableColorWipeEffectColor &entry) const {
if (entry.random_slot != NO_RANDOM_SLOT)
return this->random_colors_[entry.random_slot];
return Color(entry.r, entry.g, entry.b, entry.w);
}
ConstVector<AddressableColorWipeEffectColor> colors_;
Color *random_colors_{nullptr};
size_t at_color_{0};
uint32_t last_add_{0};
uint32_t add_led_interval_{};
+34 -8
View File
@@ -1,8 +1,10 @@
#pragma once
#include <type_traits>
#include <utility>
#include "esphome/core/automation.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "light_effect.h"
@@ -154,11 +156,25 @@ class AutomationLightEffect : public LightEffect {
Trigger<> trig_;
};
/// One strobe step. Codegen keeps the steps in a flash table, so it is read with progmem_memcpy.
struct StrobeLightEffectColor {
LightColorValues color;
float state;
float brightness;
float color_brightness;
float red;
float green;
float blue;
float white;
float color_temperature;
float cold_white;
float warm_white;
uint32_t duration;
uint32_t transition_length;
ColorMode color_mode;
};
static_assert(std::is_trivially_copyable_v<StrobeLightEffectColor>, "StrobeLightEffectColor is copied out of flash");
static_assert(sizeof(StrobeLightEffectColor) % sizeof(uint32_t) == 0,
"StrobeLightEffectColor must be whole words for ESP8266 flash reads");
class StrobeLightEffect : public LightEffect {
public:
@@ -166,19 +182,22 @@ class StrobeLightEffect : public LightEffect {
void start() override {
// Place the cycle at the end of the last color, so the first apply() switches straight to the first color
this->at_color_ = this->colors_.size() - 1;
this->last_switch_ = millis() - this->colors_.back().duration;
this->last_switch_ = millis() - this->duration_(this->at_color_);
}
void apply() override {
const uint32_t now = millis();
if (now - this->last_switch_ < this->colors_[this->at_color_].duration)
if (now - this->last_switch_ < this->duration_(this->at_color_))
return;
// Switch to next color
this->at_color_ = (this->at_color_ + 1) % this->colors_.size();
auto color = this->colors_[this->at_color_].color;
StrobeLightEffectColor step;
progmem_memcpy(&step, &this->colors_[this->at_color_], sizeof(step));
LightColorValues color(step.color_mode, step.state, step.brightness, step.color_brightness, step.red, step.green,
step.blue, step.white, step.color_temperature, step.cold_white, step.warm_white);
auto call = this->state_->turn_on();
call.from_light_color_values(this->colors_[this->at_color_].color);
call.from_light_color_values(color);
if (!color.is_on()) {
// Don't turn the light off, otherwise the light effect will be stopped
@@ -187,15 +206,22 @@ class StrobeLightEffect : public LightEffect {
}
call.set_publish(false);
call.set_save(false);
call.set_transition_length_if_supported(this->colors_[this->at_color_].transition_length);
call.set_transition_length_if_supported(step.transition_length);
call.perform();
this->last_switch_ = now;
}
void set_colors(const std::initializer_list<StrobeLightEffectColor> &colors) { this->colors_ = colors; }
/// Codegen only: point at the generated flash table of steps, which must outlive the effect.
void set_colors(const StrobeLightEffectColor *colors, size_t count) { this->colors_ = {colors, count}; }
protected:
FixedVector<StrobeLightEffectColor> colors_;
uint32_t duration_(size_t index) const {
uint32_t duration;
progmem_memcpy(&duration, &this->colors_[index].duration, sizeof(duration));
return duration;
}
ConstVector<StrobeLightEffectColor> colors_;
uint32_t last_switch_{0};
size_t at_color_{0};
};
+27
View File
@@ -2,6 +2,9 @@
#include <cstdint>
#include "esp_color_view.h"
#include "light_traits.h"
namespace esphome::light {
/// Which byte of an addressable LED's data carries each colour.
@@ -36,6 +39,30 @@ struct ChannelColors {
buf[this->bytes_per_led()] = '\0';
return buf;
}
/// Colour modes of a strip with this channel order.
LightTraits traits() const {
LightTraits traits;
if (this->has_white()) {
traits.set_supported_color_modes({ColorMode::RGB_WHITE, ColorMode::WHITE});
} else {
traits.set_supported_color_modes({ColorMode::RGB});
}
return traits;
}
/// View of LED `index` in `buf`; a scratch pixel while `buf` or `effect_data` is unallocated (failed setup()).
ESPColorView view(uint8_t *buf, uint8_t *effect_data, int32_t index, const ESPColorCorrection *correction) const {
static uint8_t scratch[4];
if (buf == nullptr || effect_data == nullptr) {
buf = scratch;
effect_data = nullptr;
index = 0;
}
uint8_t *led = buf + index * this->bytes_per_led();
return {led + this->r, led + this->g, led + this->b, this->has_white() ? led + this->w : nullptr,
effect_data + index, correction};
}
};
} // namespace esphome::light
+64 -26
View File
@@ -29,6 +29,7 @@ from esphome.const import (
CONF_WHITE,
CONF_WIDTH,
)
from esphome.core import CORE
from esphome.cpp_generator import MockObjClass
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
@@ -52,7 +53,6 @@ from .types import (
ColorMode,
FlickerLightEffect,
LambdaLightEffect,
LightColorValues,
LightStateRef,
PulseLightEffect,
RandomLightEffect,
@@ -351,28 +351,26 @@ async def strobe_effect_to_code(config, effect_id):
colors = [
cg.StructInitializer(
StrobeLightEffectColor,
(
"color",
LightColorValues(
color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN),
color[CONF_STATE],
color[CONF_BRIGHTNESS],
color[CONF_COLOR_BRIGHTNESS],
color[CONF_RED],
color[CONF_GREEN],
color[CONF_BLUE],
color[CONF_WHITE],
color.get(CONF_COLOR_TEMPERATURE, 0.0),
color[CONF_COLD_WHITE],
color[CONF_WARM_WHITE],
),
),
("state", color[CONF_STATE]),
("brightness", color[CONF_BRIGHTNESS]),
("color_brightness", color[CONF_COLOR_BRIGHTNESS]),
("red", color[CONF_RED]),
("green", color[CONF_GREEN]),
("blue", color[CONF_BLUE]),
("white", color[CONF_WHITE]),
("color_temperature", color.get(CONF_COLOR_TEMPERATURE, 0.0)),
("cold_white", color[CONF_COLD_WHITE]),
("warm_white", color[CONF_WARM_WHITE]),
("duration", color[CONF_DURATION]),
("transition_length", color[CONF_TRANSITION_LENGTH]),
("color_mode", color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN)),
)
for color in config.get(CONF_COLORS, [])
]
cg.add(var.set_colors(colors))
table = cg.shared_progmem_array(
"light_strobe_colors", StrobeLightEffectColor, cg.ArrayInitializer(*colors)
)
cg.add(var.set_colors(table, len(colors)))
return var
@@ -431,6 +429,16 @@ async def addressable_rainbow_effect_to_code(config, effect_id):
return var
# Matches NO_RANDOM_SLOT in addressable_light_effect.h
NO_RANDOM_SLOT = 0xFF
def _validate_random_slots(colors: list[ConfigType]) -> list[ConfigType]:
if sum(1 for color in colors if color[CONF_RANDOM]) > NO_RANDOM_SLOT:
raise cv.Invalid(f"At most {NO_RANDOM_SLOT} colors can be random")
return colors
ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA = cv.Schema(
{
cv.Optional(CONF_COLOR_BRIGHTNESS): cv.percentage,
@@ -459,7 +467,11 @@ ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA = _with_color_option(
{
cv.Optional(
CONF_COLORS, default=[{CONF_NUM_LEDS: 1, CONF_RANDOM: True}]
): cv.ensure_list(ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA),
): cv.All(
cv.ensure_list(ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA),
cv.Length(min=1),
_validate_random_slots,
),
cv.Optional(
CONF_ADD_LED_INTERVAL, default="0.1s"
): cv.positive_time_period_milliseconds,
@@ -471,21 +483,47 @@ async def addressable_color_wipe_effect_to_code(config, effect_id):
cg.add(var.set_add_led_interval(config[CONF_ADD_LED_INTERVAL]))
cg.add(var.set_reverse(config[CONF_REVERSE]))
colors = []
random_colors = []
for color in config.get(CONF_COLORS, []):
color_brightness = color[CONF_COLOR_BRIGHTNESS]
rgbw = (
int(round(color[CONF_RED] * color_brightness * 255)),
int(round(color[CONF_GREEN] * color_brightness * 255)),
int(round(color[CONF_BLUE] * color_brightness * 255)),
int(round(color[CONF_WHITE] * 255)),
)
random_slot = cg.RawExpression("light::NO_RANDOM_SLOT")
if color[CONF_RANDOM]:
random_slot = len(random_colors)
random_colors.append(rgbw)
colors.append(
cg.StructInitializer(
AddressableColorWipeEffectColor,
("r", int(round(color[CONF_RED] * color_brightness * 255))),
("g", int(round(color[CONF_GREEN] * color_brightness * 255))),
("b", int(round(color[CONF_BLUE] * color_brightness * 255))),
("w", int(round(color[CONF_WHITE] * 255))),
("random", color[CONF_RANDOM]),
("num_leds", color[CONF_NUM_LEDS]),
("r", rgbw[0]),
("g", rgbw[1]),
("b", rgbw[2]),
("w", rgbw[3]),
("random_slot", random_slot),
("gradient", color[CONF_GRADIENT]),
("num_leds", color[CONF_NUM_LEDS]),
)
)
cg.add(var.set_colors(colors))
table = cg.shared_progmem_array(
"light_color_wipe_colors",
AddressableColorWipeEffectColor,
cg.ArrayInitializer(*colors),
)
random_storage = cg.nullptr
if random_colors:
# One RAM slot per random entry; it starts at the configured color, as before
id_ = cg.unique_global_id(f"{effect_id.id}_random_colors", Color)
values = cg.ArrayInitializer(
*(Color(r, g, b, w) for r, g, b, w in random_colors)
)
cg.add_global(cg.RawStatement(f"static {Color} {id_}[] = {values};"))
random_storage = cg.MockObj(id_, ".")
CORE.register_variable(id_, random_storage)
cg.add(var.set_colors(table, len(colors), random_storage))
return var
@@ -141,32 +141,13 @@ void LN882xSPILEDStripLightOutput::dump_config() {
float LN882xSPILEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
light::LightTraits LN882xSPILEDStripLightOutput::get_traits() {
auto traits = light::LightTraits();
if (this->channel_colors_.has_white()) {
traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
} else {
traits.set_supported_color_modes({light::ColorMode::RGB});
}
return traits;
}
void LN882xSPILEDStripLightOutput::clear_effect_data() {
if (this->effect_data_ != nullptr)
memset(this->effect_data_, 0, this->num_leds_);
}
void LN882xSPILEDStripLightOutput::write_state(light::LightState *state) {
if (this->is_failed())
return;
// protect from refreshing too often
const uint32_t now = micros();
if (this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < this->max_refresh_rate_) {
// try again next loop iteration, so that this change won't get lost
this->schedule_show();
if (this->throttle_show_(now, this->last_refresh_, this->max_refresh_rate_))
return;
}
if (spi_dma_busy()) {
// Busy sending the last frame, try again later.
@@ -191,20 +172,6 @@ void LN882xSPILEDStripLightOutput::write_state(light::LightState *state) {
spi_dma_transmit(this->dma_data_, this->dma_data_bytes_);
}
light::ESPColorView LN882xSPILEDStripLightOutput::get_view_internal(int32_t index) const {
const light::ChannelColors &colors = this->channel_colors_;
// After a failed setup() effects and partitions still write pixels, so they get a scratch pixel.
static uint8_t scratch[5];
const bool ready = this->color_data_ != nullptr;
uint8_t *led = ready ? this->color_data_ + (index * colors.bytes_per_led()) : scratch;
return {led + colors.r,
led + colors.g,
led + colors.b,
colors.has_white() ? led + colors.w : nullptr,
ready ? &this->effect_data_[index] : scratch + 4,
&this->correction_};
}
} // namespace esphome::ln882x_spi_led_strip
#endif // USE_LN882X
@@ -23,16 +23,21 @@ class LN882xSPILEDStripLightOutput final : public light::AddressableLight {
void dump_config() override;
float get_setup_priority() const override;
light::LightTraits get_traits() override;
light::LightTraits get_traits() override { return this->channel_colors_.traits(); }
int32_t size() const override { return this->num_leds_; }
void clear_effect_data() override;
void clear_effect_data() override {
if (this->effect_data_ != nullptr)
memset(this->effect_data_, 0, this->num_leds_);
}
void write_state(light::LightState *state) override;
/// Set a maximum refresh rate in µs as some lights do not like being updated too often.
void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; }
protected:
light::ESPColorView get_view_internal(int32_t index) const override;
light::ESPColorView get_view_internal(int32_t index) const override {
return this->channel_colors_.view(this->color_data_, this->effect_data_, index, &this->correction_);
}
const size_t color_data_bytes_;
const size_t dma_data_bytes_;
+1 -1
View File
@@ -887,7 +887,7 @@ void LvglComponent::setup() {
lv_timer_set_period(this->refr_timer_, this->refr_timer_period_);
#if LV_USE_LOG
lv_log_register_print_cb([](lv_log_level_t level, const char *buf) {
auto next = strchr(buf, ')');
auto *next = strchr(buf, ')');
if (next != nullptr)
buf = next + 1;
while (isspace(*buf))
+6 -1
View File
@@ -210,10 +210,15 @@ class WidgetType:
async def update_to_code(self, w: "Widget", config: dict) -> None:
"""
Update a widget. Defaults to calling to_code, but can be overridden
Update a widget. Defaults to calling to_code when the widget declares
update options; a widget with an empty modify_schema has nothing
widget-specific to update because the shared update path already
applies the common properties. Can be overridden.
:param w: The widget to update
:param config: The configuration for the update
"""
if not self.modify_schema.schema:
return
await self.to_code(w, config)
async def obj_creator(self, parent: MockObj, config: dict):
+1 -2
View File
@@ -20,10 +20,9 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(MCP23016),
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
.extend(i2c.i2c_device_schema(0x20))
)
+2 -21
View File
@@ -25,25 +25,9 @@ void MCP23016::setup() {
// all pins input
this->write_reg_(MCP23016_IODIR1, 0xFFFF);
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&MCP23016::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
this->disable_loop();
this->setup_interrupt_pin_();
}
void IRAM_ATTR MCP23016::gpio_intr(MCP23016 *arg) { arg->enable_loop_soon_any_context(); }
void MCP23016::loop() {
// Invalidate cache at the start of each loop
this->reset_pin_cache_();
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
// stale state forever; keep looping until the line is released so we self-heal.
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
this->disable_loop();
}
}
bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_mask_); }
bool MCP23016::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
@@ -51,14 +35,11 @@ void MCP23016::digital_write_hw(uint8_t pin, bool value) { this->update_reg_(pin
void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
this->update_reg_(pin, true, MCP23016_IODIR1);
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
this->enable_polling_loop_();
} else if (flags == gpio::FLAG_OUTPUT) {
this->update_reg_(pin, false, MCP23016_IODIR1);
}
}
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
bool MCP23016::read_reg_(uint8_t reg, uint16_t *value) {
if (this->is_failed())
return false;
+2 -9
View File
@@ -3,7 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/gpio_expander/cached_gpio_component.h"
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
namespace esphome::mcp23016 {
@@ -25,20 +25,14 @@ enum MCP23016GPIORegisters {
MCP23016_IOCON1 = 0x0B,
};
class MCP23016 final : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander<uint16_t, 16> {
class MCP23016 final : public gpio_expander::CachedGpioExpanderComponent<uint16_t, 16>, public i2c::I2CDevice {
public:
MCP23016() = default;
void setup() override;
void loop() override;
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void gpio_intr(MCP23016 *arg);
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
@@ -54,7 +48,6 @@ class MCP23016 final : public Component, public i2c::I2CDevice, public gpio_expa
uint16_t olat_{0x0000};
// Cache for input values (16-bit combined for both banks)
uint16_t input_mask_{0x0000};
InternalGPIOPin *interrupt_pin_{nullptr};
};
using MCP23016GPIOPin = gpio_expander::ExpanderGPIOPin<MCP23016>;
@@ -32,15 +32,11 @@ void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) {
} else if (flags == gpio::FLAG_OUTPUT) {
this->update_reg(pin, false, iodir);
}
// When interrupt_pin is configured, auto-enable CHANGE interrupt for input pins
// so the chip's INT output fires on any input state change
if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) {
this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE);
}
// Enable polling loop for input pins (not needed for interrupt-driven mode
// where the ISR handles re-enabling loop)
if (this->interrupt_pin_ == nullptr && (flags & gpio::FLAG_INPUT)) {
this->enable_loop();
if (flags & gpio::FLAG_INPUT) {
// With an interrupt pin, INT fires on any change of an input
if (this->interrupt_pin_ != nullptr)
this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE);
this->enable_polling_loop_();
}
}
@@ -43,15 +43,11 @@ void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) {
} else if (flags == gpio::FLAG_OUTPUT) {
this->update_reg(pin, false, iodir);
}
// When interrupt_pin is configured, auto-enable CHANGE interrupt for input pins
// so the chip's INT output fires on any input state change
if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) {
this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE);
}
// Enable polling loop for input pins (not needed for interrupt-driven mode
// where the ISR handles re-enabling loop)
if (this->interrupt_pin_ == nullptr && (flags & gpio::FLAG_INPUT)) {
this->enable_loop();
if (flags & gpio::FLAG_INPUT) {
// With an interrupt pin, INT fires on any change of an input
if (this->interrupt_pin_ != nullptr)
this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE);
this->enable_polling_loop_();
}
}
+1 -2
View File
@@ -32,9 +32,8 @@ MCP23XXX_INTERRUPT_MODES = MCP23XXXInterruptMode.options(
MCP23XXX_CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean,
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
}
).extend(cv.COMPONENT_SCHEMA)
).extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
@coroutine
@@ -4,8 +4,6 @@
namespace esphome::mcp23xxx_base {
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg) { arg->enable_loop_soon_any_context(); }
template<uint8_t N> void MCP23XXXGPIOPin<N>::setup() {
this->pin_mode(flags_);
// When interrupt_pin is configured, pin_mode() already auto-enables CHANGE
@@ -1,6 +1,6 @@
#pragma once
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/gpio_expander/cached_gpio_component.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
@@ -8,47 +8,15 @@ namespace esphome::mcp23xxx_base {
enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING };
// Not a template member: GCC 10 (ESP8266) ignores section attributes on template instantiations, leaving the
// ISR in flash.
void mcp23xxx_gpio_intr(Component *arg);
template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander<uint8_t, N> {
template<uint8_t N> class MCP23XXXBase : public gpio_expander::CachedGpioExpanderComponent<uint8_t, N> {
public:
virtual void pin_mode(uint8_t pin, gpio::Flags flags);
virtual void pin_interrupt_mode(uint8_t pin, MCP23XXXInterruptMode interrupt_mode);
void set_open_drain_ints(const bool value) { this->open_drain_ints_ = value; }
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
InternalGPIOPin *get_interrupt_pin() const { return this->interrupt_pin_; }
float get_setup_priority() const override { return setup_priority::IO; }
void loop() override {
this->reset_pin_cache_();
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
// stale state forever; keep looping until the line is released so we self-heal.
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
this->disable_loop();
}
}
protected:
// No need to clear latched interrupts before attaching the ISR — if INT is
// already low the ISR fires immediately, loop runs, cache invalidates, and
// the GPIO read clears the latch. One harmless extra read at most.
void setup_interrupt_pin_() {
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&mcp23xxx_gpio_intr, static_cast<Component *>(this),
gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
// Disable loop until an input pin is configured via pin_mode()
// For interrupt-driven mode, loop is re-enabled by the ISR
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
this->disable_loop();
}
// read a given register
virtual bool read_reg(uint8_t reg, uint8_t *value) = 0;
// write a value to a given register
@@ -57,7 +25,6 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) = 0;
bool open_drain_ints_;
InternalGPIOPin *interrupt_pin_{nullptr};
};
template<uint8_t N> class MCP23XXXGPIOPin final : public GPIOPin {
+2 -3
View File
@@ -173,9 +173,8 @@ void AirConditioner::do_follow_me(float temperature, bool use_fahrenheit, bool b
return;
}
// Round and convert temperature to long, then clamp and convert it to uint8_t
uint8_t temp_uint8 =
static_cast<uint8_t>(esphome::clamp<long>(std::lroundf(temperature), 0L, static_cast<long>(UINT8_MAX)));
// Clamp to the uint8_t range before rounding so lroundf never sees an out-of-range value
uint8_t temp_uint8 = static_cast<uint8_t>(std::lroundf(esphome::clamp(temperature, 0.0f, 255.0f)));
char temp_symbol = use_fahrenheit ? 'F' : 'C';
ESP_LOGD(Constants::TAG, "Follow me action called with temperature: %.5f °%c, rounded to: %u °%c", temperature,
+232
View File
@@ -0,0 +1,232 @@
import voluptuous as vol
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components.const import CONF_RTS_PIN
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
from esphome.components.network import add_use_address, require_ipv4
import esphome.config_validation as cv
from esphome.const import (
CONF_BAUD_RATE,
CONF_COMMAND,
CONF_ENABLE_ON_BOOT,
CONF_ID,
CONF_MODEL,
CONF_ON_CONNECT,
CONF_ON_DISCONNECT,
CONF_REBOOT_TIMEOUT,
CONF_RX_BUFFER_SIZE,
CONF_RX_PIN,
CONF_TX_BUFFER_SIZE,
CONF_TX_PIN,
CONF_USE_ADDRESS,
)
from esphome.core import CORE, ID, coroutine_with_priority
from esphome.coroutine import CoroPriority
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType, TemplateArgsType
# Static so `api:` finds network before its dependency check; a config-dependent
# AUTO_LOAD resolves after that check and fails a cellular-only node
AUTO_LOAD = ["network"]
CODEOWNERS = ["@oarcher"]
CONFLICTS_WITH = ["captive_portal", "ethernet"]
DEPENDENCIES = ["esp32"]
DOMAIN = "modem"
CONF_PIN_CODE = "pin_code"
CONF_APN = "apn"
CONF_CTS_PIN = "cts_pin"
CONF_INIT_AT = "init_at"
CONF_ON_SYNC = "on_sync"
CONF_DTE_BUFFER_SIZE = "dte_buffer_size"
CONF_MODEM_ID = "modem_id"
esp_modem_ns = cg.global_ns.namespace("esp_modem")
# Model name to the esp_modem factory that drives it
MODEM_MODELS = {
"BG96": esp_modem_ns.create_BG96_dce,
"SIM800": esp_modem_ns.create_SIM800_dce,
"SIM7000": esp_modem_ns.create_SIM7000_dce,
"SIM7070": esp_modem_ns.create_SIM7070_dce,
"SIM7080": esp_modem_ns.create_SIM7000_dce,
"SIM7600": esp_modem_ns.create_SIM7600_dce,
"SIM7670": esp_modem_ns.create_SIM7600_dce,
"GENERIC": esp_modem_ns.create_generic_dce,
}
modem_ns = cg.esphome_ns.namespace("modem")
ModemComponent = modem_ns.class_("ModemComponent", cg.Component)
ModemState = modem_ns.enum("ModemState", is_class=True)
def _on_state(conf_key: str, state: str) -> automation.CallbackAutomation:
return automation.CallbackAutomation(
conf_key,
"add_on_state_callback",
params=[(ModemState, "state")],
forward=[],
when=f"state == modem::ModemState::{state}",
)
def _default_use_address(config: ConfigType) -> ConfigType:
# The host tools resolve the device through this, like the other network components
config.setdefault(CONF_USE_ADDRESS, f"{CORE.name}.local")
return config
_CALLBACK_AUTOMATIONS = (
_on_state(CONF_ON_CONNECT, "MODEM_STATE_CONNECTED"),
_on_state(CONF_ON_DISCONNECT, "MODEM_STATE_DISCONNECTING"),
_on_state(CONF_ON_SYNC, "MODEM_STATE_SYNCED"),
)
# AT_COMMAND_SIZE - 1 in modem_component.h; the buffer keeps one byte for the terminator
MAX_AT_COMMAND_LENGTH = 127
# A SIM PIN is 4 to 8 digits; anything else would spend one of the SIM's attempts. \Z, not $, so a
# trailing newline is refused too
_validate_pin_code = cv.All(
cv.string_strict,
vol.Match(r"^[0-9]{4,8}\Z", msg="pin_code must be 4 to 8 digits"),
)
# The IDF UART driver needs more than its 128 byte FIFO; the setters take uint16_t
_UART_BUFFER_SIZE = cv.int_range(min=129, max=65535)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ModemComponent),
cv.Required(CONF_TX_PIN): pins.internal_gpio_output_pin_schema,
cv.Required(CONF_RX_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_RTS_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_CTS_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_BAUD_RATE): cv.positive_int,
cv.Required(CONF_MODEL): cv.one_of(*MODEM_MODELS, upper=True),
cv.Required(CONF_APN): cv.string,
cv.Optional(CONF_PIN_CODE): cv.sensitive(_validate_pin_code),
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
# The command count is stored in a byte
cv.Optional(CONF_INIT_AT): cv.All(
cv.ensure_list(
cv.All(cv.string, cv.ByteLength(max=MAX_AT_COMMAND_LENGTH))
),
cv.Length(max=255),
),
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_TX_BUFFER_SIZE, default=1024): _UART_BUFFER_SIZE,
cv.Optional(CONF_RX_BUFFER_SIZE, default=1024): _UART_BUFFER_SIZE,
# CMUX keeps 128 bytes of the DTE buffer back, so a smaller buffer could not be read
cv.Optional(CONF_DTE_BUFFER_SIZE, default=1024): cv.int_range(
min=256, max=65535
),
cv.Optional(
CONF_REBOOT_TIMEOUT, default="10min"
): cv.positive_time_period_milliseconds,
**{
cv.Optional(a.conf_key): automation.validate_automation({})
for a in _CALLBACK_AUTOMATIONS
},
}
).extend(cv.COMPONENT_SCHEMA),
cv.has_none_or_all_keys(CONF_RTS_PIN, CONF_CTS_PIN),
_default_use_address,
require_ipv4(DOMAIN),
)
MODEM_ID_SCHEMA = automation.maybe_simple_id(
{cv.GenerateID(): cv.use_id(ModemComponent)}
)
SEND_AT_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(ModemComponent),
cv.Required(CONF_COMMAND): cv.templatable(
cv.All(cv.string, cv.ByteLength(max=MAX_AT_COMMAND_LENGTH))
),
},
key=CONF_COMMAND,
)
for _name in ("enable", "disable"):
automation.register_apply_action(
f"modem.{_name}", MODEM_ID_SCHEMA, automation.ApplyCall(f"{_name}()")
)
_send_at_builder = automation.apply_action_builder(
"modem.send_at",
SEND_AT_SCHEMA,
# A constant renders as a literal and takes the const char * overload; a lambda returns std::string
automation.ApplyField(CONF_COMMAND, "send_at", cg.std_string),
)
@automation.register_action(
"modem.send_at", automation.ApplyAction, SEND_AT_SCHEMA, synchronous=True
)
async def send_at_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
cg.add_define("USE_MODEM_SEND_AT")
return await _send_at_builder(config, action_id, template_arg, args)
automation.register_apply_condition(
"modem.connected", MODEM_ID_SCHEMA, "is_connected()"
)
automation.register_apply_condition("modem.enabled", MODEM_ID_SCHEMA, "is_enabled()")
@coroutine_with_priority(CoroPriority.COMMUNICATION)
async def to_code(config: ConfigType) -> None:
add_idf_component(name="espressif/esp_modem", ref="2.1.0")
add_idf_sdkconfig_option("CONFIG_LWIP_PPP_SUPPORT", True)
# LCP echo probes notice a modem that went away without a word, which ends the PPP session
add_idf_sdkconfig_option("CONFIG_LWIP_ENABLE_LCP_ECHO", True)
add_idf_sdkconfig_option("CONFIG_ESP_MODEM_CMUX_DELAY_AFTER_DLCI_SETUP", 500)
add_idf_sdkconfig_option(
"CONFIG_ESP_MODEM_CMUX_USE_SHORT_PAYLOADS_ONLY", True
) # Slower but more reliable
cg.add_define("USE_MODEM")
model = config[CONF_MODEL]
factory = MODEM_MODELS[model]
tx = await cg.gpio_pin_expression(config[CONF_TX_PIN])
rx = await cg.gpio_pin_expression(config[CONF_RX_PIN])
var = cg.new_Pvariable(config[CONF_ID], tx, rx, model, factory, config[CONF_APN])
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
add_use_address(var, config[CONF_USE_ADDRESS])
if (pin_code := config.get(CONF_PIN_CODE)) is not None:
cg.add_define("USE_MODEM_PIN_CODE")
cg.add(var.set_pin_code(pin_code))
if config[CONF_ENABLE_ON_BOOT]:
cg.add(var.enable())
if init_at := config.get(CONF_INIT_AT):
# The commands are literals in flash; the component keeps a pointer to the array
arr = cg.shared_progmem_array(
"modem_init_at", cg.const_char_ptr, cg.ArrayInitializer(*init_at)
)
cg.add(var.set_init_at_commands(arr, len(init_at)))
await cg.add_pin(var.set_rts_pin, config.get(CONF_RTS_PIN))
await cg.add_pin(var.set_cts_pin, config.get(CONF_CTS_PIN))
cg.add_optional(var.set_baud_rate, config.get(CONF_BAUD_RATE))
cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE]))
cg.add(var.set_tx_buffer_size(config[CONF_TX_BUFFER_SIZE]))
cg.add(var.set_dte_buffer_size(config[CONF_DTE_BUFFER_SIZE]))
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
await cg.register_component(var, config)
File diff suppressed because it is too large. Load diff
+282
View File
@@ -0,0 +1,282 @@
#pragma once
#ifdef USE_ESP32
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
#ifdef USE_MODEM_URC
#include "esphome/core/line_reader.h"
#endif
#include "esphome/core/preferences.h"
#include "esphome/core/static_task.h"
#include "esphome/components/network/ip_address.h"
#ifdef USE_MODEM_URC
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#include <cxx_include/esp_modem_api.hpp>
#include <esp_modem_config.h>
#include <atomic>
#include <memory>
#include <string>
#include <utility>
namespace esphome::modem {
enum class ModemState : uint8_t {
MODEM_STATE_DISABLED,
MODEM_STATE_ENABLING,
MODEM_STATE_SYNCED,
MODEM_STATE_CONNECTING,
MODEM_STATE_WAIT_IP,
MODEM_STATE_CONNECTED,
MODEM_STATE_DISCONNECTING,
};
// CONNECTING runs its long commands once; the later passes only poll the attachment
enum class ConnectStep : uint8_t {
CONNECT_STEP_START,
CONNECT_STEP_RADIO_ON,
CONNECT_STEP_ATTACH_REQUESTED,
};
enum class SimResult : uint8_t {
SIM_RESULT_READY,
SIM_RESULT_RETRY, // status not readable yet, or a PIN step is in progress
SIM_RESULT_LOCKED, // nothing more to try: PUK, another lock, no pin_code or the PIN is not sent again
SIM_RESULT_HALT, // stop this attempt; a reboot may still open the SIM, so the reboot timer stays
};
// The step that failed most recently; CONNECTED clears it
enum class ModemFault : uint8_t {
MODEM_FAULT_NONE,
MODEM_FAULT_NO_RESPONSE,
MODEM_FAULT_BAUD,
MODEM_FAULT_INIT_AT,
MODEM_FAULT_SIM_NOT_READY,
MODEM_FAULT_SIM_LOCKED,
MODEM_FAULT_PIN_NOT_SENT,
MODEM_FAULT_PIN_RECORD,
MODEM_FAULT_SETUP,
MODEM_FAULT_NOT_ATTACHED,
MODEM_FAULT_PPP,
MODEM_FAULT_TEARDOWN,
};
#ifdef USE_MODEM_PIN_CODE
enum class PinStep : uint8_t {
PIN_STEP_IDLE,
PIN_STEP_SAVING, // the send is counted and waits for the record to reach flash
PIN_STEP_SENT,
};
// Written by loop() once it tried to save pin_record_; the task leaves the record alone while PENDING
enum class PinSave : uint8_t {
PIN_SAVE_DONE,
PIN_SAVE_PENDING,
PIN_SAVE_FAILED,
};
#endif
// Every esp_modem factory shares this signature, so codegen picks the one for the model
using DceFactory = std::unique_ptr<esp_modem::DCE> (*)(const esp_modem::dce_config *, std::shared_ptr<esp_modem::DTE>,
esp_netif_t *);
class ModemComponent : public Component {
public:
ModemComponent(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, const char *model, DceFactory factory,
const char *apn);
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::WIFI + 1; } // Just before Wi-Fi
void set_reboot_timeout(uint32_t timeout) { this->timeout_ = timeout; }
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
void set_rts_pin(InternalGPIOPin *rts_pin) { this->rts_pin_ = rts_pin; }
void set_cts_pin(InternalGPIOPin *cts_pin) { this->cts_pin_ = cts_pin; }
void set_baud_rate(int baud_rate) { this->baud_rate_ = baud_rate; }
#ifdef USE_MODEM_PIN_CODE
void set_pin_code(const char *pin_code) { this->pin_code_ = pin_code; }
#endif
void set_tx_buffer_size(uint16_t tx_buffer_size) { this->tx_buffer_size_ = tx_buffer_size; }
void set_rx_buffer_size(uint16_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; }
void set_dte_buffer_size(uint16_t dte_buffer_size) { this->dte_buffer_size_ = dte_buffer_size; }
void set_init_at_commands(const char *const *cmds, uint8_t count) {
this->init_at_commands_ = cmds;
this->init_at_count_ = count;
}
#ifdef USE_MODEM_URC
void set_urc_text_sensor(text_sensor::TextSensor *urc_text_sensor) { this->urc_text_sensor_ = urc_text_sensor; }
#endif
template<typename F> void add_on_state_callback(F &&callback) {
this->on_state_callback_.add(std::forward<F>(callback));
}
bool is_connected() const { return this->component_state_.load() == ModemState::MODEM_STATE_CONNECTED; }
bool is_disabled() const {
return this->component_state_.load() == ModemState::MODEM_STATE_DISABLED &&
this->target_state_.load() == ModemState::MODEM_STATE_DISABLED;
}
bool is_enabled() const { return !this->is_disabled(); }
void enable();
void disable();
#ifdef USE_MODEM_SEND_AT
/// Queue an AT command for the modem task, which sends it at its next pass and logs the result and response.
/// Returns false when a command is still queued or the command is too long.
bool send_at(const char *cmd, uint32_t timeout = COMMAND_TIMEOUT_MS);
bool send_at(const std::string &cmd, uint32_t timeout = COMMAND_TIMEOUT_MS);
#endif
network::IPAddresses get_ip_addresses();
const char *get_use_address() const { return this->use_address_; }
protected:
static constexpr uint32_t COMMAND_TIMEOUT_MS = 1000;
static constexpr uint32_t RETRY_DELAY_MS = 1000;
static constexpr size_t URC_LINE_SIZE = 128;
static constexpr size_t AT_COMMAND_SIZE = 128;
// esp_modem's calls block until the modem answers or their own timeouts run out, up to a minute for a
// mode change, so the state machine runs on its own task and leaves the main loop alone
static void task_entry(void *arg);
void run_task_();
void wake_task_();
ModemState compute_next_state_();
ModemState handle_state_enabling_();
ModemState handle_state_synced_();
ModemState handle_state_connecting_();
ModemState handle_state_wait_ip_();
ModemState handle_state_disconnecting_();
/// Pause the task before the next pass, then go to state
uint32_t next_probe_delay_();
ModemState retry_(ModemState state, uint32_t delay_ms = RETRY_DELAY_MS);
void transition_to_(ModemState next_state);
// What the task left for loop() to do
enum Pending : uint32_t {
PENDING_STATUS = 1 << 0,
PENDING_SAVE_BAUD = 1 << 1,
#ifdef USE_MODEM_PIN_CODE
PENDING_SAVE_PIN = 1 << 2,
#endif
PENDING_CONNECT_TIMEOUT = 1 << 3, // arm or cancel the reboot timer for the current state
PENDING_ON_SYNC = 1 << 4, // loop() wakes the task once on_sync ran
PENDING_ON_CONNECT = 1 << 5,
PENDING_ON_DISCONNECT = 1 << 6,
};
// Main loop only, so arming and cancelling cannot interleave
void arm_connect_timeout_();
void cancel_connect_timeout_();
void fail_setup_(const LogString *reason);
void set_fault_(ModemFault fault);
void set_teardown_fault_();
void clear_config_fault_(ModemFault fault);
void hand_over_(uint32_t pending);
/// Send an AT command from the task and log the result; blocks for up to the timeout
esp_modem::command_result at_(const char *cmd, uint32_t timeout = COMMAND_TIMEOUT_MS);
void publish_status_();
void not_responding_(int baud);
void dump_connect_params_();
SimResult prepare_sim_();
#ifdef USE_MODEM_PIN_CODE
SimResult send_pin_();
void save_pin_record_();
#endif
InternalGPIOPin *tx_pin_;
InternalGPIOPin *rx_pin_;
InternalGPIOPin *rts_pin_{nullptr};
InternalGPIOPin *cts_pin_{nullptr};
const char *model_;
DceFactory dce_factory_;
const char *apn_;
const char *const *init_at_commands_{nullptr}; // static array from code generation
#ifdef USE_MODEM_URC
text_sensor::TextSensor *urc_text_sensor_{nullptr};
const char *urc_line_{nullptr}; // the line urc_pending_ hands to loop(), inside urc_reader_
#endif
std::unique_ptr<esp_modem::DCE> dce_;
std::shared_ptr<esp_modem::DTE> dte_;
esp_netif_t *ppp_netif_{nullptr};
ESPPreferenceObject pref_;
LazyCallbackManager<void(ModemState)> on_state_callback_;
int baud_rate_{0}; // 0 = use the esp_modem default
int current_baud_rate_{0};
int restored_baud_{0};
StaticTask task_;
uint32_t timeout_{0};
uint32_t step_delay_ms_{0};
uint32_t state_started_ms_{0}; // when the IP wait or the PIN unlock wait began
std::atomic<uint32_t> pending_{0};
#ifdef USE_MODEM_PIN_CODE
const char *pin_code_{nullptr};
ESPPreferenceObject pin_pref_;
#endif
// Kept in flash so reboots cannot keep spending the SIM's attempts on a PIN it never confirmed
struct PinRecord {
uint32_t hash; // pin_code the record is about
uint8_t sends; // AT+CPIN= sent without the SIM reporting READY since
};
#ifdef USE_MODEM_PIN_CODE
PinRecord pin_record_{}; // the task writes it, loop() saves it while pin_save_state_ is PENDING
#endif
#ifdef USE_MODEM_SEND_AT
uint32_t at_timeout_{COMMAND_TIMEOUT_MS};
#endif
uint16_t tx_buffer_size_{1024};
uint16_t rx_buffer_size_{1024};
uint16_t dte_buffer_size_{1024};
// The task owns component_state_; the main loop sets target_state_ and reads both
std::atomic<ModemState> component_state_{ModemState::MODEM_STATE_DISABLED};
std::atomic<ModemState> target_state_{ModemState::MODEM_STATE_DISABLED};
// Written by the task only, read by loop()
std::atomic<ModemFault> fault_{ModemFault::MODEM_FAULT_NONE};
// A failed baud_rate or init_at step, shown once fault_ is clear until the step passes again
std::atomic<ModemFault> config_fault_{ModemFault::MODEM_FAULT_NONE};
// std::atomic<uint8_t> flags: GCC on Xtensa calls out of line for std::atomic<bool>
// Written by the IP event task, read by the modem task
std::atomic<uint8_t> got_ip_{0};
std::atomic<uint8_t> ppp_error_{0}; // last NETIF_PPP_STATUS error code, 0 = none
// Doublings of the silent-modem probe delay; enable() and an answer reset it
std::atomic<uint8_t> probe_backoff_{0};
// Set by loop() once on_sync ran; the task waits for it, not for any notification
std::atomic<uint8_t> sync_acked_{0};
#ifdef USE_MODEM_SEND_AT
// Set by send_at() on the main loop once at_command_ is written, cleared by the task after sending it
std::atomic<uint8_t> at_pending_{0};
#endif
#ifdef USE_MODEM_URC
// Set by the UART task with urc_line_; until loop() has published it, new lines are dropped
std::atomic<uint8_t> urc_pending_{0};
#endif
#ifdef USE_MODEM_PIN_CODE
std::atomic<PinSave> pin_save_state_{PinSave::PIN_SAVE_DONE};
PinStep pin_step_{PinStep::PIN_STEP_IDLE};
bool pin_record_unsaved_{false}; // loop() only; the last PIN record save failed
#endif
ConnectStep connect_step_{ConnectStep::CONNECT_STEP_START};
uint8_t init_at_count_{0};
uint8_t uart_port_{UART_NUM_MAX};
uint8_t attach_polls_{0};
bool connect_timeout_armed_{false};
bool modem_synced_{false}; // a modem that never answered has nothing to tear down
bool resync_{false}; // the attachment read failed, so CONNECTING syncs again first
#ifdef USE_MODEM_SEND_AT
char at_command_[AT_COMMAND_SIZE];
#endif
#ifdef USE_MODEM_URC
// Fed on the UART task
LineReader<URC_LINE_SIZE> urc_reader_;
#endif
private:
// Points at a string literal from code generation, never at dynamic storage
const char *use_address_{""};
};
extern ModemComponent *global_modem_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome::modem
#endif
+27
View File
@@ -0,0 +1,27 @@
import esphome.codegen as cg
from esphome.components import text_sensor
from esphome.components.esp32 import add_idf_sdkconfig_option
import esphome.config_validation as cv
from esphome.types import ConfigType
from . import CONF_MODEM_ID, ModemComponent
DEPENDENCIES = ["modem"]
CONF_URC = "urc"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_MODEM_ID): cv.use_id(ModemComponent),
cv.Optional(CONF_URC): text_sensor.text_sensor_schema(),
}
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_MODEM_ID])
texts = text_sensor.sub_text_sensors(config)
if await texts(CONF_URC, hub.set_urc_text_sensor) is not None:
cg.add_define("USE_MODEM_URC")
cg.add_define("USE_LINE_READER")
add_idf_sdkconfig_option("CONFIG_ESP_MODEM_URC_HANDLER", True)
+233 -27
View File
@@ -1,3 +1,5 @@
from collections.abc import Iterator
from esphome import automation
import esphome.codegen as cg
from esphome.components import logger, network, socket
@@ -10,6 +12,7 @@ from esphome.components.esp32 import (
request_tls,
require_certificate_bundle,
)
from esphome.config import iter_ids
from esphome.config_helpers import (
filter_source_files_from_defines,
filter_source_files_from_platform,
@@ -26,6 +29,8 @@ from esphome.const import (
CONF_CLIENT_ID,
CONF_COMMAND_RETAIN,
CONF_COMMAND_TOPIC,
CONF_DEVICE_ID,
CONF_DEVICES,
CONF_DISCOVER_IP,
CONF_DISCOVERY,
CONF_DISCOVERY_OBJECT_ID_GENERATOR,
@@ -33,11 +38,17 @@ from esphome.const import (
CONF_DISCOVERY_RETAIN,
CONF_DISCOVERY_UNIQUE_ID_GENERATOR,
CONF_ENABLE_ON_BOOT,
CONF_ESPHOME,
CONF_ID,
CONF_INTERNAL,
CONF_KEEPALIVE,
CONF_LEVEL,
CONF_LOG_TOPIC,
CONF_MODE_COMMAND_TOPIC,
CONF_MQTT,
CONF_MQTT_ID,
CONF_NAME,
CONF_NAME_ADD_MAC_SUFFIX,
CONF_ON_CONNECT,
CONF_ON_DISCONNECT,
CONF_ON_JSON_MESSAGE,
@@ -46,6 +57,7 @@ from esphome.const import (
CONF_PAYLOAD,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_PLATFORM,
CONF_PORT,
CONF_PUBLISH_NAN_AS_NONE,
CONF_QOS,
@@ -55,6 +67,9 @@ from esphome.const import (
CONF_SKIP_CERT_CN_CHECK,
CONF_STATE_TOPIC,
CONF_SUBSCRIBE_QOS,
CONF_TARGET_TEMPERATURE_COMMAND_TOPIC,
CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC,
CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC,
CONF_TOPIC,
CONF_TOPIC_PREFIX,
CONF_USE_ABBREVIATIONS,
@@ -69,6 +84,9 @@ from esphome.const import (
PlatformFramework,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObjClass
import esphome.final_validate as fv
from esphome.helpers import sanitize, snake_case
from esphome.types import ConfigType
DEPENDENCIES = ["network"]
@@ -94,10 +112,12 @@ DEFAULT_WS_PATH = "/mqtt"
# Max lengths for stack-based topic building.
# These values are used in cv.Length() validators below to ensure the C++ code
# in mqtt_component.cpp can safely use fixed-size stack buffers without overflow.
# If you change these, update the corresponding constants in mqtt_component.cpp.
# can safely use fixed-size stack buffers without overflow. If you change these,
# update the corresponding constants in mqtt_client.h and mqtt_component.h.
TOPIC_PREFIX_MAX_LEN = 64 # Default is device name, typically short
DISCOVERY_PREFIX_MAX_LEN = 64 # Default is "homeassistant" (13 chars)
# Size of the stack buffer topics are built into (static_assert in mqtt_client.cpp)
MQTT_DEFAULT_TOPIC_MAX_LEN = 248
def validate_message_just_topic(value):
@@ -127,34 +147,91 @@ MQTT_MESSAGE_SCHEMA = cv.Any(
)
mqtt_ns = cg.esphome_ns.namespace("mqtt")
def _topic_source(topic: str) -> cg.MockObj:
# On ESP8266 a topic that fits the buffer stays in flash and is copied out when needed
if CORE.is_esp8266 and len(topic.encode()) < MQTT_DEFAULT_TOPIC_MAX_LEN:
return mqtt_ns.progmem_topic(cg.FlashStringLiteral(topic))
return mqtt_ns.static_topic(topic)
MQTTMessage = mqtt_ns.struct("MQTTMessage")
MQTTClientDisconnectReason = mqtt_ns.enum("MQTTClientDisconnectReason")
MQTTClientComponent = mqtt_ns.class_("MQTTClientComponent", cg.Component)
MQTTPublishJsonAction = mqtt_ns.class_("MQTTPublishJsonAction", automation.Action)
MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component)
MQTTAlarmControlPanelComponent = mqtt_ns.class_(
"MQTTAlarmControlPanelComponent", MQTTComponent
# A topic the firmware builds from a suffix, unless the config key replaces the whole topic
_Topic = tuple[str, str]
# The firmware subscribes to exactly one of these alternatives, chosen by entity traits
_Subscription = tuple[tuple[_Topic, ...], ...]
# The subscriptions each entity MQTT class always makes, keyed by class: component type,
# then the subscriptions
_COMMAND_TOPICS: dict[str, tuple[str, tuple[_Subscription, ...]]] = {}
def _single_topic(suffix: str, custom_key: str) -> _Subscription:
return (((suffix, custom_key),),)
_COMMAND = _single_topic("command", CONF_COMMAND_TOPIC)
def _entity_class(
name: str,
component_type: str | None = None,
subscriptions: tuple[_Subscription, ...] = (_COMMAND,),
) -> MockObjClass:
"""Declare an entity MQTT class; a component_type means it subscribes to command topics."""
cls = mqtt_ns.class_(name, MQTTComponent)
if component_type is not None:
_COMMAND_TOPICS[str(cls)] = (component_type, subscriptions)
return cls
MQTTAlarmControlPanelComponent = _entity_class(
"MQTTAlarmControlPanelComponent", "alarm_control_panel"
)
MQTTBinarySensorComponent = mqtt_ns.class_("MQTTBinarySensorComponent", MQTTComponent)
MQTTClimateComponent = mqtt_ns.class_("MQTTClimateComponent", MQTTComponent)
MQTTCoverComponent = mqtt_ns.class_("MQTTCoverComponent", MQTTComponent)
MQTTFanComponent = mqtt_ns.class_("MQTTFanComponent", MQTTComponent)
MQTTJSONLightComponent = mqtt_ns.class_("MQTTJSONLightComponent", MQTTComponent)
MQTTSensorComponent = mqtt_ns.class_("MQTTSensorComponent", MQTTComponent)
MQTTSwitchComponent = mqtt_ns.class_("MQTTSwitchComponent", MQTTComponent)
MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent)
MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent)
MQTTDateComponent = mqtt_ns.class_("MQTTDateComponent", MQTTComponent)
MQTTTimeComponent = mqtt_ns.class_("MQTTTimeComponent", MQTTComponent)
MQTTDateTimeComponent = mqtt_ns.class_("MQTTDateTimeComponent", MQTTComponent)
MQTTTextComponent = mqtt_ns.class_("MQTTTextComponent", MQTTComponent)
MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent)
MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent)
MQTTLockComponent = mqtt_ns.class_("MQTTLockComponent", MQTTComponent)
MQTTEventComponent = mqtt_ns.class_("MQTTEventComponent", MQTTComponent)
MQTTUpdateComponent = mqtt_ns.class_("MQTTUpdateComponent", MQTTComponent)
MQTTValveComponent = mqtt_ns.class_("MQTTValveComponent", MQTTComponent)
MQTTBinarySensorComponent = _entity_class("MQTTBinarySensorComponent")
MQTTClimateComponent = _entity_class(
"MQTTClimateComponent",
"climate",
(
_single_topic("mode/command", CONF_MODE_COMMAND_TOPIC),
# One target temperature topic, or the _low and _high pair on two-point devices
(
(("target_temperature/command", CONF_TARGET_TEMPERATURE_COMMAND_TOPIC),),
(
(
"target_temperature_low/command",
CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC,
),
(
"target_temperature_high/command",
CONF_TARGET_TEMPERATURE_HIGH_COMMAND_TOPIC,
),
),
),
),
)
MQTTCoverComponent = _entity_class("MQTTCoverComponent", "cover")
MQTTFanComponent = _entity_class("MQTTFanComponent", "fan")
MQTTJSONLightComponent = _entity_class("MQTTJSONLightComponent", "light")
MQTTSensorComponent = _entity_class("MQTTSensorComponent")
MQTTSwitchComponent = _entity_class("MQTTSwitchComponent", "switch")
MQTTTextSensor = _entity_class("MQTTTextSensor")
MQTTNumberComponent = _entity_class("MQTTNumberComponent", "number")
MQTTDateComponent = _entity_class("MQTTDateComponent", "date")
MQTTTimeComponent = _entity_class("MQTTTimeComponent", "time")
MQTTDateTimeComponent = _entity_class("MQTTDateTimeComponent", "datetime")
MQTTTextComponent = _entity_class("MQTTTextComponent", "text")
MQTTSelectComponent = _entity_class("MQTTSelectComponent", "select")
MQTTButtonComponent = _entity_class("MQTTButtonComponent", "button")
MQTTLockComponent = _entity_class("MQTTLockComponent", "lock")
MQTTEventComponent = _entity_class("MQTTEventComponent")
MQTTUpdateComponent = _entity_class("MQTTUpdateComponent", "update")
MQTTValveComponent = _entity_class("MQTTValveComponent", "valve")
MQTTDiscoveryUniqueIdGenerator = mqtt_ns.enum("MQTTDiscoveryUniqueIdGenerator")
MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS = {
@@ -398,6 +475,137 @@ CONFIG_SCHEMA = cv.All(
network.require_ipv4(CONF_MQTT),
)
# AsyncMqttClient, the ESP8266 and LibreTiny backend, drops any inbound message whose
# topic is longer than this many bytes (its default maxTopicLength)
ASYNC_MQTT_CLIENT_MAX_TOPIC_LEN = 128
# Keep in sync with OBJECT_ID_MAX_LEN in esphome/core/entity_base.h
OBJECT_ID_MAX_LEN = 128
# name_add_mac_suffix appends "-" plus 6 hex digits to the name at runtime
MAC_SUFFIX = "-xxxxxx"
def _entity_object_id(
config: ConfigType, mac_suffix: bool, devices: dict[str, str]
) -> str:
"""Build the object id the way EntityBase::write_object_id_to() does."""
# The name falls back the way EntityBase::configure_entity_() does
name = config.get(CONF_NAME) or ""
if not name:
if (device := config.get(CONF_DEVICE_ID)) is not None:
name = devices[device.id]
elif mac_suffix:
# Only the friendly name gains the MAC suffix, and an empty one stays empty
name = f"{CORE.friendly_name}{MAC_SUFFIX}" if CORE.friendly_name else ""
else:
name = CORE.friendly_name or CORE.name
# The firmware sanitizes each UTF-8 byte to one character and caps the result
raw = name.encode()[: OBJECT_ID_MAX_LEN - 1]
return sanitize(snake_case(raw.decode("ascii", "replace")))
def _entity_topic(
config: ConfigType, default_topic: str, suffix: str, custom_key: str
) -> str | None:
"""Return the topic an entity subscribes to, or None when only known at runtime."""
custom = config.get(custom_key)
# An empty specialized topic falls back to the default (MQTT_COMPONENT_CUSTOM_TOPIC_)
# while an empty command_topic stays empty
if custom == "" and custom_key != CONF_COMMAND_TOPIC:
custom = None
if custom is not None:
# A lambda topic is only known at runtime
return custom if isinstance(custom, str) else None
return f"{default_topic}/{suffix}" if default_topic else None
def _entity_subscriptions(
config: ConfigType, topic_prefix: str, mac_suffix: bool, devices: dict[str, str]
) -> Iterator[tuple[tuple[str, ...], ...]]:
"""Yield the subscriptions an entity's MQTT component always makes, as known topics."""
component_type, subscriptions = _COMMAND_TOPICS[str(config[CONF_MQTT_ID].type)]
# Mirrors MQTTComponent::compute_is_internal_(): internal components never subscribe
if CONF_STATE_TOPIC in config:
internal = config[CONF_STATE_TOPIC] == ""
elif CONF_COMMAND_TOPIC in config:
internal = config[CONF_COMMAND_TOPIC] == ""
else:
internal = not topic_prefix or config.get(CONF_INTERNAL, False)
if internal:
return
default_topic = ""
if topic_prefix:
object_id = _entity_object_id(config, mac_suffix, devices)
default_topic = f"{topic_prefix}/{component_type}/{object_id}"
for subscription in subscriptions:
yield tuple(
tuple(
topic
for suffix, custom_key in alternative
if (topic := _entity_topic(config, default_topic, suffix, custom_key))
is not None
)
for alternative in subscription
)
def _subscriptions(config: ConfigType) -> Iterator[tuple[tuple[str, ...], ...]]:
"""Yield every subscription known at build time, as alternatives of topics."""
full_config = fv.full_config.get()
esphome_config = full_config[CONF_ESPHOME]
mac_suffix = esphome_config.get(CONF_NAME_ADD_MAC_SUFFIX, False)
devices = {
device[CONF_ID].id: device[CONF_NAME]
for device in esphome_config.get(CONF_DEVICES, [])
}
topic_prefix = config[CONF_TOPIC_PREFIX]
if mac_suffix and topic_prefix == CORE.name:
topic_prefix += MAC_SUFFIX
# Entities can sit at any depth (a platform can declare its own switches), so find them by mqtt_id
for id_, path in iter_ids(full_config):
if path[-1] != CONF_MQTT_ID or str(id_.type) not in _COMMAND_TOPICS:
continue
entity = full_config
for key in path[:-1]:
entity = entity[key]
yield from _entity_subscriptions(entity, topic_prefix, mac_suffix, devices)
for key in (CONF_ON_MESSAGE, CONF_ON_JSON_MESSAGE):
for trigger in config.get(key, []):
yield ((trigger[CONF_TOPIC],),)
for domain in ("sensor", "text_sensor"):
for entity in full_config.get(domain) or []:
if entity[CONF_PLATFORM] == "mqtt_subscribe":
yield ((entity[CONF_TOPIC],),)
def _too_long(topic: str) -> bool:
# A wildcard filter matches topics of any length, so only literals are checked
if "+" in topic or "#" in topic:
return False
return len(topic.encode()) > ASYNC_MQTT_CLIENT_MAX_TOPIC_LEN
def _validate_topic_lengths(config: ConfigType) -> ConfigType:
"""Reject topics that the ESP8266 and LibreTiny MQTT client would silently drop."""
if not (CORE.is_esp8266 or CORE.is_libretiny):
return config
for subscription in _subscriptions(config):
# The config cannot tell which alternative the firmware picks, so only a
# subscription that is too long whichever way is rejected
too_long = [
next((topic for topic in alternative if _too_long(topic)), None)
for alternative in subscription
]
if all(too_long):
topic = too_long[0]
raise cv.Invalid(
f"MQTT topic '{topic}' is {len(topic.encode())} bytes; the MQTT client "
f"on this platform drops topics longer than {ASYNC_MQTT_CLIENT_MAX_TOPIC_LEN}"
)
return config
FINAL_VALIDATE_SCHEMA = _validate_topic_lengths
def exp_mqtt_message(config):
if config is None:
@@ -566,9 +774,7 @@ async def to_code(config):
((CONF_PAYLOAD, cg.std_string, automation.string_ref_literal),),
),
)
cg.add(
var.subscribe(cg.progmem_string(conf[CONF_TOPIC]), callback, conf[CONF_QOS])
)
cg.add(var.subscribe(_topic_source(conf[CONF_TOPIC]), callback, conf[CONF_QOS]))
for conf in config.get(CONF_ON_JSON_MESSAGE, []):
callback = await automation.build_trigger_callback(
@@ -579,7 +785,7 @@ async def to_code(config):
)
cg.add(
var.subscribe_json(
cg.progmem_string(conf[CONF_TOPIC]), callback, conf[CONF_QOS]
_topic_source(conf[CONF_TOPIC]), callback, conf[CONF_QOS]
)
)
@@ -50,7 +50,7 @@ static bool apply_command(AlarmControlPanelCall &call, const char *state) {
void MQTTAlarmControlPanelComponent::setup() {
this->alarm_control_panel_->add_on_state_callback(
[this](AlarmControlPanelState /*state*/) { this->publish_state(); });
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
auto call = this->alarm_control_panel_->make_call();
if (!payload.empty() && payload[0] == '{') {
// JSON payload: {"state": "DISARM", "code": "1234"}
+1 -1
View File
@@ -15,7 +15,7 @@ using namespace esphome::button;
MQTTButtonComponent::MQTTButtonComponent(button::Button *button) : button_(button) {}
void MQTTButtonComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
if (payload == "PRESS") {
this->button_->press();
} else {
+67 -12
View File
@@ -98,12 +98,16 @@ void MQTTClientComponent::setup() {
if (this->is_discovery_ip_enabled()) {
this->subscribe(
"esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); },
2);
static_topic("esphome/discover"),
[this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
// Format topic on stack - subscribe() copies it
char ping_topic[ESPHOME_TOPIC_BUFFER_SIZE];
format_esphome_topic(ping_topic, "ping");
static_assert(sizeof("esphome/ping/") + ESPHOME_DEVICE_NAME_MAX_LEN <= MQTT_DEFAULT_TOPIC_MAX_LEN);
MQTTTopicSource ping_topic{[](const void *, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) {
size_t len = buf_append_printf(buf.data(), buf.size(), 0, "esphome/ping/%s",
App.get_name().c_str());
return StringRef(buf.data(), len);
},
nullptr};
this->subscribe(
ping_topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
}
@@ -479,6 +483,12 @@ bool MQTTClientComponent::subscribe_(const char *topic, uint8_t qos) {
}
return ret != 0;
}
// Out of line so the topic buffer is not part of loop()'s stack frame
__attribute__((noinline)) bool MQTTClientComponent::subscribe_topic_(const MQTTTopicSource &topic, uint8_t qos) {
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->subscribe_(topic.build(buf).c_str(), qos);
}
void MQTTClientComponent::resubscribe_subscription_(MQTTSubscription *sub) {
if (sub->subscribed)
return;
@@ -487,7 +497,7 @@ void MQTTClientComponent::resubscribe_subscription_(MQTTSubscription *sub) {
bool do_resub = sub->resubscribe_timeout == 0 || now - sub->resubscribe_timeout > 1000;
if (do_resub) {
sub->subscribed = this->subscribe_(sub->topic.c_str(), sub->qos);
sub->subscribed = this->subscribe_topic_(sub->topic, sub->qos);
sub->resubscribe_timeout = now;
}
}
@@ -510,20 +520,57 @@ void MQTTClientComponent::handle_crash_log_request_(const std::string &topic, co
}
#endif
void MQTTClientComponent::add_subscription_(std::string &&topic, mqtt_subscription_callback_t callback, bool boxed,
static StringRef static_topic_builder(const void *context, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN>) {
return StringRef(static_cast<const char *>(context));
}
// Same as static_topic_builder, but a distinct function so its address marks the topics to free
static StringRef copied_topic_builder(const void *context, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN>) {
return StringRef(static_cast<const char *>(context));
}
MQTTTopicSource static_topic(const char *topic) { return {static_topic_builder, topic}; }
static_assert(MQTT_DEFAULT_TOPIC_MAX_LEN == 248, "update MQTT_DEFAULT_TOPIC_MAX_LEN in mqtt/__init__.py");
#ifdef USE_ESP8266
// mqtt/__init__.py picks this source only for topics that fit
static StringRef progmem_topic_builder(const void *context, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) {
size_t len = buf_append_str_p(buf.data(), buf.size(), 0, static_cast<const char *>(context));
return StringRef(buf.data(), len);
}
MQTTTopicSource progmem_topic(ProgmemStr topic) {
return {progmem_topic_builder, reinterpret_cast<const char *>(topic)};
}
#endif
MQTTTopicSource MQTTClientComponent::copy_topic(StringRef topic) {
char *copy = new char[topic.size() + 1]; // NOLINT(cppcoreguidelines-owning-memory)
memcpy(copy, topic.c_str(), topic.size());
copy[topic.size()] = '\0';
return {copied_topic_builder, copy};
}
void MQTTClientComponent::add_subscription_(MQTTTopicSource topic, mqtt_subscription_callback_t callback, bool boxed,
uint8_t qos) {
auto &subscription = this->subscriptions_.emplace_back(MQTTSubscription{
.topic = std::move(topic),
.topic = topic,
.callback = callback,
.resubscribe_timeout = 0,
.qos = qos,
.subscribed = false,
.boxed = boxed,
.removed = false,
.callback = callback,
.resubscribe_timeout = 0,
});
this->resubscribe_subscription_(&subscription);
}
// Out of line so the topic buffer is not part of unsubscribe()'s stack frame
__attribute__((noinline)) static bool topic_equals(const MQTTTopicSource &source, const std::string &topic) {
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return source.build(buf) == topic;
}
void MQTTClientComponent::unsubscribe(const std::string &topic) {
bool ret = this->mqtt_backend_.unsubscribe(topic.c_str());
yield();
@@ -536,7 +583,7 @@ void MQTTClientComponent::unsubscribe(const std::string &topic) {
}
for (auto &subscription : this->subscriptions_) {
if (subscription.topic == topic)
if (topic_equals(subscription.topic, topic))
subscription.removed = true;
}
// A callback may be running; the outermost dispatch erases them once it returns.
@@ -553,6 +600,8 @@ void MQTTClientComponent::erase_removed_subscriptions_() {
if (it->removed) {
if (it->boxed)
it->callback.free_boxed();
if (it->topic.builder == copied_topic_builder)
delete[] static_cast<const char *>(it->topic.context); // NOLINT(cppcoreguidelines-owning-memory)
it = this->subscriptions_.erase(it);
} else {
++it;
@@ -629,6 +678,12 @@ void MQTTClientComponent::disable() {
this->on_shutdown();
}
// Out of line so the topic buffer is off the stack while the matched callback runs
__attribute__((noinline)) static bool topic_matches(const char *message, const MQTTTopicSource &topic) {
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return topic_match(message, topic.build(buf).c_str());
}
void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) {
#ifdef USE_ESP8266
// IMPORTANT: This defer is REQUIRED to prevent stack overflow crashes on ESP8266.
@@ -662,7 +717,7 @@ void MQTTClientComponent::on_message(const std::string &topic, const std::string
const size_t count = this->subscriptions_.size();
for (size_t i = 0; i < count; i++) {
const auto &subscription = this->subscriptions_[i];
if (!subscription.removed && topic_match(topic.c_str(), subscription.topic.c_str()))
if (!subscription.removed && topic_matches(topic.c_str(), subscription.topic))
subscription.callback.call(topic, payload);
}
this->dispatching_ = was_dispatching;
+66 -13
View File
@@ -8,8 +8,11 @@
#include "esphome/components/network/ip_address.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
@@ -22,6 +25,7 @@
#endif
#include "lwip/ip_addr.h"
#include <span>
#include <vector>
// The crash log replay needs the logger, which mqtt does not depend on
@@ -31,6 +35,18 @@
namespace esphome::mqtt {
// Max lengths for stack-based topic building.
// These limits are enforced at Python config validation time in mqtt/__init__.py
// using cv.Length() validators for topic_prefix.
// This ensures the stack buffers are always large enough.
static constexpr size_t MQTT_COMPONENT_TYPE_MAX_LEN = 20;
static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
// Stack buffer size - safe because all inputs are length-validated at config time
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
static constexpr size_t MQTT_DEFAULT_TOPIC_MAX_LEN =
MQTT_TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
/** Callback for MQTT events.
*/
using mqtt_on_connect_callback_t = std::function<MQTTBackend::on_connect_callback_t>;
@@ -46,15 +62,33 @@ using mqtt_json_callback_t ESPDEPRECATED(
"subscribe_json() takes any callable; pass the lambda directly. Removed in 2027.5.0",
"2026.11.0") = std::function<void(const std::string &, JsonObject)>;
/// Returns a subscription's topic, written into buf or referring to storage that outlives the subscription.
/// Never a null c_str(); an empty topic is "".
using mqtt_topic_builder_t = StringRef (*)(const void *context, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf);
/// Where a subscription's topic comes from. The topic is rebuilt whenever it is needed, so nothing is stored.
struct MQTTTopicSource {
mqtt_topic_builder_t builder;
const void *context;
StringRef build(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const { return this->builder(this->context, buf); }
};
/// A topic string that outlives the subscription, such as a literal; it is referenced, not copied.
MQTTTopicSource static_topic(const char *topic);
#ifdef USE_ESP8266
/// A topic in flash, copied into the buffer when needed, so at most MQTT_DEFAULT_TOPIC_MAX_LEN - 1 characters.
MQTTTopicSource progmem_topic(ProgmemStr topic);
#endif
/// internal struct for MQTT subscriptions.
struct MQTTSubscription {
std::string topic;
MQTTTopicSource topic;
mqtt_subscription_callback_t callback;
uint32_t resubscribe_timeout;
uint8_t qos;
bool subscribed;
bool boxed; // made by create_boxed(), freed on unsubscribe
bool removed; // unsubscribed while a callback ran, erased once dispatch returns
mqtt_subscription_callback_t callback;
uint32_t resubscribe_timeout;
};
/// internal struct for MQTT credentials.
@@ -183,32 +217,44 @@ class MQTTClientComponent final : public Component {
* A callback that fits in a pointer (capture at most `this`) is stored inline; a larger one is
* boxed on the heap and freed by unsubscribe().
*
* @param topic Where the topic comes from; it is rebuilt when needed instead of stored.
* @param callback The callback function.
* @param qos The QoS of this subscription.
*/
template<typename F> void subscribe(MQTTTopicSource topic, F &&callback, uint8_t qos = 0) {
constexpr bool boxed = !mqtt_subscription_callback_t::fits_inline<F>();
this->add_subscription_(topic,
boxed ? mqtt_subscription_callback_t::create_boxed(std::forward<F>(callback))
: mqtt_subscription_callback_t::create(std::forward<F>(callback)),
boxed, qos);
}
/** Subscribe to an MQTT topic and call callback when a message is received.
*
* The topic is copied to the heap and freed by unsubscribe().
*
* @param topic The topic. Wildcards are currently not supported.
* @param callback The callback function.
* @param qos The QoS of this subscription.
*/
template<typename F> void subscribe(std::string topic, F &&callback, uint8_t qos = 0) {
constexpr bool boxed = !mqtt_subscription_callback_t::fits_inline<F>();
this->add_subscription_(std::move(topic),
boxed ? mqtt_subscription_callback_t::create_boxed(std::forward<F>(callback))
: mqtt_subscription_callback_t::create(std::forward<F>(callback)),
boxed, qos);
template<typename F> void subscribe(const std::string &topic, F &&callback, uint8_t qos = 0) {
this->subscribe(copy_topic(StringRef(topic)), std::forward<F>(callback), qos);
}
/** Subscribe to a MQTT topic and automatically parse JSON payload.
*
* If an invalid JSON payload is received, the callback will not be called.
*
* @param topic The topic. Wildcards are currently not supported.
* @param topic An MQTTTopicSource, or a string that is copied to the heap.
* @param callback The callback with a parsed JsonObject that will be called when a message with matching topic is
* received.
* @param qos The QoS of this subscription.
*/
template<typename F> void subscribe_json(std::string topic, F &&callback, uint8_t qos = 0) {
template<typename T, typename F> void subscribe_json(T &&topic, F &&callback, uint8_t qos = 0) {
static_assert(std::is_invocable_v<const std::decay_t<F> &, const std::string &, JsonObject>,
"subscribe_json() needs a callback that does not mutate its captures");
this->subscribe(
std::move(topic),
std::forward<T>(topic),
[cb = std::forward<F>(callback)](const std::string &topic, const std::string &payload) {
call_json(cb, topic, payload);
},
@@ -301,6 +347,12 @@ class MQTTClientComponent final : public Component {
void set_wait_for_connection(bool wait_for_connection) { this->wait_for_connection_ = wait_for_connection; }
protected:
// For the lambda command topic, which is copied once
friend class MQTTComponent;
/// A heap copy of topic, owned by the one subscription it is passed to and freed when that is erased.
static MQTTTopicSource copy_topic(StringRef topic);
void send_device_info_();
/// Reconnect to the MQTT broker if not already connected.
@@ -317,6 +369,7 @@ class MQTTClientComponent final : public Component {
void recalculate_availability_();
bool subscribe_(const char *topic, uint8_t qos);
bool subscribe_topic_(const MQTTTopicSource &topic, uint8_t qos);
void resubscribe_subscription_(MQTTSubscription *sub);
template<typename F> static void call_json(const F &callback, const std::string &topic, const std::string &payload) {
JsonDocument doc = json::parse_json(payload);
@@ -328,7 +381,7 @@ class MQTTClientComponent final : public Component {
#ifdef USE_MQTT_CRASH_LOG
void handle_crash_log_request_(const std::string &topic, const std::string &payload);
#endif
void add_subscription_(std::string &&topic, mqtt_subscription_callback_t callback, bool boxed, uint8_t qos);
void add_subscription_(MQTTTopicSource topic, mqtt_subscription_callback_t callback, bool boxed, uint8_t qos);
void resubscribe_subscriptions_();
MQTTCredentials credentials_;
+31 -29
View File
@@ -209,7 +209,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
}
void MQTTClimateComponent::setup() {
auto traits = this->device_->get_traits();
this->subscribe(this->get_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->get_mode_command_topic_source(), [this](const std::string &topic, const std::string &payload) {
auto call = this->device_->make_call();
call.set_mode(payload);
call.perform();
@@ -217,7 +217,7 @@ void MQTTClimateComponent::setup() {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
this->subscribe(this->get_target_temperature_low_command_topic(),
this->subscribe(this->get_target_temperature_low_command_topic_source(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_number<float>(payload);
if (!val.has_value()) {
@@ -228,7 +228,7 @@ void MQTTClimateComponent::setup() {
call.set_target_temperature_low(*val);
call.perform();
});
this->subscribe(this->get_target_temperature_high_command_topic(),
this->subscribe(this->get_target_temperature_high_command_topic_source(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_number<float>(payload);
if (!val.has_value()) {
@@ -240,7 +240,7 @@ void MQTTClimateComponent::setup() {
call.perform();
});
} else {
this->subscribe(this->get_target_temperature_command_topic(),
this->subscribe(this->get_target_temperature_command_topic_source(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_number<float>(payload);
if (!val.has_value()) {
@@ -254,7 +254,7 @@ void MQTTClimateComponent::setup() {
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
this->subscribe(this->get_target_humidity_command_topic(),
this->subscribe(this->get_target_humidity_command_topic_source(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_number<float>(payload);
if (!val.has_value()) {
@@ -268,27 +268,30 @@ void MQTTClimateComponent::setup() {
}
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
this->subscribe(this->get_preset_command_topic(), [this](const std::string &topic, const std::string &payload) {
auto call = this->device_->make_call();
call.set_preset(payload);
call.perform();
});
this->subscribe(this->get_preset_command_topic_source(),
[this](const std::string &topic, const std::string &payload) {
auto call = this->device_->make_call();
call.set_preset(payload);
call.perform();
});
}
if (traits.get_supports_fan_modes()) {
this->subscribe(this->get_fan_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
auto call = this->device_->make_call();
call.set_fan_mode(payload);
call.perform();
});
this->subscribe(this->get_fan_mode_command_topic_source(),
[this](const std::string &topic, const std::string &payload) {
auto call = this->device_->make_call();
call.set_fan_mode(payload);
call.perform();
});
}
if (traits.get_supports_swing_modes()) {
this->subscribe(this->get_swing_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
auto call = this->device_->make_call();
call.set_swing_mode(payload);
call.perform();
});
this->subscribe(this->get_swing_mode_command_topic_source(),
[this](const std::string &topic, const std::string &payload) {
auto call = this->device_->make_call();
call.set_swing_mode(payload);
call.perform();
});
}
this->device_->add_on_state_callback([this](Climate & /*unused*/) { this->publish_state_(); });
@@ -318,15 +321,15 @@ bool MQTTClimateComponent::publish_state_() {
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
len = value_accuracy_to_buf(payload, this->device_->target_temperature_low, target_accuracy);
if (!this->publish(this->get_target_temperature_low_state_topic_to(topic_buf), payload, len))
if (!this->publish_float_(this->get_target_temperature_low_state_topic_to(topic_buf),
this->device_->target_temperature_low, target_accuracy))
success = false;
len = value_accuracy_to_buf(payload, this->device_->target_temperature_high, target_accuracy);
if (!this->publish(this->get_target_temperature_high_state_topic_to(topic_buf), payload, len))
if (!this->publish_float_(this->get_target_temperature_high_state_topic_to(topic_buf),
this->device_->target_temperature_high, target_accuracy))
success = false;
} else {
len = value_accuracy_to_buf(payload, this->device_->target_temperature, target_accuracy);
if (!this->publish(this->get_target_temperature_state_topic_to(topic_buf), payload, len))
if (!this->publish_float_(this->get_target_temperature_state_topic_to(topic_buf), this->device_->target_temperature,
target_accuracy))
success = false;
}
@@ -337,9 +340,8 @@ bool MQTTClimateComponent::publish_state_() {
success = false;
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) &&
!std::isnan(this->device_->target_humidity)) {
len = value_accuracy_to_buf(payload, this->device_->target_humidity, 0);
if (!this->publish(this->get_target_humidity_state_topic_to(topic_buf), payload, len))
(!std::isnan(this->device_->target_humidity) || mqtt::global_mqtt_client->is_publish_nan_as_none())) {
if (!this->publish_float_(this->get_target_humidity_state_topic_to(topic_buf), this->device_->target_humidity, 0))
success = false;
}
@@ -2,6 +2,8 @@
#ifdef USE_MQTT
#include <cmath>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -124,6 +126,14 @@ StringRef MQTTComponent::get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPI
return this->get_default_topic_for_to_(buf, "command", 7);
}
MQTTTopicSource MQTTComponent::command_topic_source_() const {
if (this->custom_command_topic_.is_lambda()) {
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return MQTTClientComponent::copy_topic(this->get_command_topic_to_(buf));
}
return this->topic_source_<MQTTComponent, &MQTTComponent::get_command_topic_to_>();
}
std::string MQTTComponent::get_state_topic_() const {
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
StringRef ref = this->get_state_topic_to_(buf);
@@ -170,6 +180,14 @@ bool MQTTComponent::publish(const char *topic, ProgmemStr payload) {
}
#endif
bool MQTTComponent::publish_float_(StringRef topic, float value, int8_t accuracy) {
if (global_mqtt_client->is_publish_nan_as_none() && std::isnan(value))
return this->publish(topic, "None", 4);
char buf[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(buf, value, accuracy);
return this->publish(topic, buf, len);
}
bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) {
return this->publish_json(topic.c_str(), f);
}
+33 -20
View File
@@ -21,23 +21,14 @@ struct SendDiscoveryConfig {
bool command_topic{true}; ///< If the command topic should be included. Default to true.
};
// Max lengths for stack-based topic building.
// These limits are enforced at Python config validation time in mqtt/__init__.py
// using cv.Length() validators for topic_prefix and discovery_prefix.
// This ensures the stack buffers are always large enough.
static constexpr size_t MQTT_COMPONENT_TYPE_MAX_LEN = 20;
static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
// Stack buffer size - safe because all inputs are length-validated at config time
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
static constexpr size_t MQTT_DEFAULT_TOPIC_MAX_LEN =
MQTT_TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
static constexpr size_t MQTT_DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
class MQTTComponent; // Forward declaration
// Validated in Python: cv.Length(max=64), DISCOVERY_PREFIX_MAX_LEN in mqtt/__init__.py
static constexpr size_t MQTT_DISCOVERY_PREFIX_MAX_LEN = 64;
// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
static constexpr size_t MQTT_DISCOVERY_TOPIC_MAX_LEN = MQTT_DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN +
1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1;
class MQTTComponent; // Forward declaration
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
#define LOG_MQTT_COMPONENT(state_topic, command_topic) log_mqtt_component(TAG, this, state_topic, command_topic)
@@ -49,7 +40,9 @@ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, b
static_assert(sizeof(type_str) - 1 <= MQTT_COMPONENT_TYPE_MAX_LEN, \
#class_name "::component_type() exceeds MQTT_COMPONENT_TYPE_MAX_LEN");
// Macro to define custom topic getter/setter with compile-time suffix length verification
// Macro to define custom topic getter/setter with compile-time suffix length verification.
// Set the topic before setup(): subscriptions rebuild it when needed, so a later change only reaches the broker
// after a reconnect.
#define MQTT_COMPONENT_CUSTOM_TOPIC_(name, type) \
static_assert(sizeof(#name "/" #type) - 1 <= MQTT_SUFFIX_MAX_LEN, \
"topic suffix " #name "/" #type " exceeds MQTT_SUFFIX_MAX_LEN"); \
@@ -64,6 +57,10 @@ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, b
return StringRef(this->custom_##name##_##type##_topic_.data(), this->custom_##name##_##type##_topic_.size()); \
return this->get_default_topic_for_to_(buf, #name "/" #type, sizeof(#name "/" #type) - 1); \
} \
MQTTTopicSource get_##name##_##type##_topic_source() const { \
using Self = std::remove_cvref_t<decltype(*this)>; \
return this->topic_source_<Self, &Self::get_##name##_##type##_topic_to>(); \
} \
std::string get_##name##_##type##_topic() const { \
if (this->custom_##name##_##type##_topic_.empty()) \
return this->get_default_topic_for_(#name "/" #type); \
@@ -128,6 +125,7 @@ class MQTTComponent : public Component {
template<typename T> void set_custom_state_topic(T &&custom_state_topic) {
this->custom_state_topic_ = std::forward<T>(custom_state_topic);
}
/// Set a custom command topic before setup(); a later change only reaches the broker after a reconnect.
template<typename T> void set_custom_command_topic(T &&custom_command_topic) {
this->custom_command_topic_ = std::forward<T>(custom_command_topic);
}
@@ -255,30 +253,33 @@ class MQTTComponent : public Component {
/** Subscribe to a MQTT topic.
*
* @param topic The topic. Wildcards are currently not supported.
* @param topic The topic: an MQTTTopicSource, or a string that is copied to the heap.
* @param callback The callback that will be called when a message with matching topic is received.
* @param qos The MQTT quality of service. Defaults to 0.
*/
template<typename F> void subscribe(std::string topic, F &&callback, uint8_t qos = 0) {
global_mqtt_client->subscribe(std::move(topic), std::forward<F>(callback), qos);
template<typename T, typename F> void subscribe(T &&topic, F &&callback, uint8_t qos = 0) {
global_mqtt_client->subscribe(std::forward<T>(topic), std::forward<F>(callback), qos);
}
/** Subscribe to a MQTT topic and automatically parse JSON payload.
*
* If an invalid JSON payload is received, the callback will not be called.
*
* @param topic The topic. Wildcards are currently not supported.
* @param topic The topic: an MQTTTopicSource, or a string that is copied to the heap.
* @param callback The callback with a parsed JsonObject that will be called when a message with matching topic is
* received.
* @param qos The MQTT quality of service. Defaults to 0.
*/
template<typename F> void subscribe_json(std::string topic, F &&callback, uint8_t qos = 0) {
global_mqtt_client->subscribe_json(std::move(topic), std::forward<F>(callback), qos);
template<typename T, typename F> void subscribe_json(T &&topic, F &&callback, uint8_t qos = 0) {
global_mqtt_client->subscribe_json(std::forward<T>(topic), std::forward<F>(callback), qos);
}
protected:
static constexpr uint32_t SEND_DEFER_ID = 0;
/// Publish a float formatted to accuracy, or "None" for NaN when publish_nan_as_none is set.
bool publish_float_(StringRef topic, float value, int8_t accuracy);
/// Helper method to get the discovery topic for this component into a buffer.
StringRef get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf,
const MQTTDiscoveryInfo &discovery_info) const;
@@ -322,6 +323,18 @@ class MQTTComponent : public Component {
/// @return StringRef pointing to the topic in the buffer.
StringRef get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
/// The command topic as a subscription source. A lambda topic is read once and copied, as it may change.
MQTTTopicSource command_topic_source_() const;
/// A topic built by one of this component's `get_*_topic_to` getters, rebuilt when needed.
template<typename C, StringRef (C::*Getter)(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN>) const>
MQTTTopicSource topic_source_() const {
return {[](const void *context, std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) {
return (static_cast<const C *>(context)->*Getter)(buf);
},
static_cast<const C *>(this)};
}
/// Get the MQTT topic that new states will be shared to (allocates std::string).
std::string get_state_topic_() const;
+27 -25
View File
@@ -31,38 +31,40 @@ MQTTCoverComponent::MQTTCoverComponent(Cover *cover) : cover_(cover) {}
void MQTTCoverComponent::setup() {
auto traits = this->cover_->get_traits();
this->cover_->add_on_state_callback([this]() { this->publish_state(); });
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
auto call = this->cover_->make_call();
call.set_command(payload.c_str());
call.perform();
});
if (traits.get_supports_position()) {
this->subscribe(this->get_position_command_topic(), [this](const std::string &topic, const std::string &payload) {
auto value = parse_number<float>(payload);
if (!value.has_value()) {
ESP_LOGW(TAG, "Invalid position value: '%s'", payload.c_str());
return;
}
auto call = this->cover_->make_call();
call.set_position(*value / 100.0f);
call.perform();
});
this->subscribe(this->get_position_command_topic_source(),
[this](const std::string &topic, const std::string &payload) {
auto value = parse_number<float>(payload);
if (!value.has_value()) {
ESP_LOGW(TAG, "Invalid position value: '%s'", payload.c_str());
return;
}
auto call = this->cover_->make_call();
call.set_position(*value / 100.0f);
call.perform();
});
}
if (traits.get_supports_tilt()) {
this->subscribe(this->get_tilt_command_topic(), [this](const std::string &topic, const std::string &payload) {
if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("STOP")) == 0) {
this->cover_->make_call().set_command_stop_tilt().perform();
return;
}
auto value = parse_number<float>(payload);
if (!value.has_value()) {
ESP_LOGW(TAG, "Invalid tilt value: '%s'", payload.c_str());
return;
}
auto call = this->cover_->make_call();
call.set_tilt(*value / 100.0f);
call.perform();
});
this->subscribe(this->get_tilt_command_topic_source(),
[this](const std::string &topic, const std::string &payload) {
if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("STOP")) == 0) {
this->cover_->make_call().set_command_stop_tilt().perform();
return;
}
auto value = parse_number<float>(payload);
if (!value.has_value()) {
ESP_LOGW(TAG, "Invalid tilt value: '%s'", payload.c_str());
return;
}
auto call = this->cover_->make_call();
call.set_tilt(*value / 100.0f);
call.perform();
});
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ using namespace esphome::datetime;
MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {}
void MQTTDateComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
this->subscribe_json(this->command_topic_source_(), [this](const std::string &topic, JsonObject root) {
auto call = this->date_->make_call();
if (root[ESPHOME_F("year")].is<uint16_t>()) {
call.set_year(root[ESPHOME_F("year")]);
+1 -1
View File
@@ -17,7 +17,7 @@ using namespace esphome::datetime;
MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetime_(datetime) {}
void MQTTDateTimeComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
this->subscribe_json(this->command_topic_source_(), [this](const std::string &topic, JsonObject root) {
auto call = this->datetime_->make_call();
if (root[ESPHOME_F("year")].is<uint16_t>()) {
call.set_year(root[ESPHOME_F("year")]);
+27 -26
View File
@@ -28,7 +28,7 @@ MQTT_COMPONENT_TYPE(MQTTFanComponent, "fan")
const EntityBase *MQTTFanComponent::get_entity() const { return this->state_; }
void MQTTFanComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
auto val = parse_on_off(payload.c_str());
switch (val) {
case PARSE_ON:
@@ -52,33 +52,34 @@ void MQTTFanComponent::setup() {
});
if (this->state_->get_traits().supports_direction()) {
this->subscribe(this->get_direction_command_topic(), [this](const std::string &topic, const std::string &payload) {
auto val = parse_on_off(payload.c_str(), "forward", "reverse");
switch (val) {
case PARSE_ON:
ESP_LOGD(TAG, "'%s': Setting direction FORWARD", LOG_STR_ARG(this->log_name_()));
this->state_->make_call().set_direction(fan::FanDirection::FORWARD).perform();
break;
case PARSE_OFF:
ESP_LOGD(TAG, "'%s': Setting direction REVERSE", LOG_STR_ARG(this->log_name_()));
this->state_->make_call().set_direction(fan::FanDirection::REVERSE).perform();
break;
case PARSE_TOGGLE:
this->state_->make_call()
.set_direction(this->state_->direction == fan::FanDirection::FORWARD ? fan::FanDirection::REVERSE
: fan::FanDirection::FORWARD)
.perform();
break;
case PARSE_NONE:
ESP_LOGW(TAG, "Unknown direction Payload %s", payload.c_str());
this->status_momentary_warning(5000);
break;
}
});
this->subscribe(
this->get_direction_command_topic_source(), [this](const std::string &topic, const std::string &payload) {
auto val = parse_on_off(payload.c_str(), "forward", "reverse");
switch (val) {
case PARSE_ON:
ESP_LOGD(TAG, "'%s': Setting direction FORWARD", LOG_STR_ARG(this->log_name_()));
this->state_->make_call().set_direction(fan::FanDirection::FORWARD).perform();
break;
case PARSE_OFF:
ESP_LOGD(TAG, "'%s': Setting direction REVERSE", LOG_STR_ARG(this->log_name_()));
this->state_->make_call().set_direction(fan::FanDirection::REVERSE).perform();
break;
case PARSE_TOGGLE:
this->state_->make_call()
.set_direction(this->state_->direction == fan::FanDirection::FORWARD ? fan::FanDirection::REVERSE
: fan::FanDirection::FORWARD)
.perform();
break;
case PARSE_NONE:
ESP_LOGW(TAG, "Unknown direction Payload %s", payload.c_str());
this->status_momentary_warning(5000);
break;
}
});
}
if (this->state_->get_traits().supports_oscillation()) {
this->subscribe(this->get_oscillation_command_topic(),
this->subscribe(this->get_oscillation_command_topic_source(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_on_off(payload.c_str(), "oscillate_on", "oscillate_off");
switch (val) {
@@ -102,7 +103,7 @@ void MQTTFanComponent::setup() {
}
if (this->state_->get_traits().supports_speed()) {
this->subscribe(this->get_speed_level_command_topic(),
this->subscribe(this->get_speed_level_command_topic_source(),
[this](const std::string &topic, const std::string &payload) {
optional<int> speed_level_opt = parse_number<int>(payload);
if (speed_level_opt.has_value()) {
+1 -1
View File
@@ -18,7 +18,7 @@ MQTT_COMPONENT_TYPE(MQTTJSONLightComponent, "light")
const EntityBase *MQTTJSONLightComponent::get_entity() const { return this->state_; }
void MQTTJSONLightComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
this->subscribe_json(this->command_topic_source_(), [this](const std::string &topic, JsonObject root) {
LightCall call = this->state_->make_call();
LightJSONSchema::parse_json(*this->state_, call, root);
call.perform();
+1 -1
View File
@@ -16,7 +16,7 @@ using namespace esphome::lock;
MQTTLockComponent::MQTTLockComponent(lock::Lock *a_lock) : lock_(a_lock) {}
void MQTTLockComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("LOCK")) == 0) {
this->lock_->lock();
} else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("UNLOCK")) == 0) {
+1 -1
View File
@@ -19,7 +19,7 @@ PROGMEM_STRING_TABLE(NumberMqttModeStrings, "", "box", "slider");
MQTTNumberComponent::MQTTNumberComponent(Number *number) : number_(number) {}
void MQTTNumberComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) {
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &state) {
auto val = parse_number<float>(state);
if (!val.has_value()) {
ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str());
+1 -1
View File
@@ -15,7 +15,7 @@ using namespace esphome::select;
MQTTSelectComponent::MQTTSelectComponent(Select *select) : select_(select) {}
void MQTTSelectComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) {
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &state) {
auto call = this->select_->make_call();
call.set_option(state);
call.perform();
+1 -6
View File
@@ -73,12 +73,7 @@ bool MQTTSensorComponent::send_initial_state() {
}
bool MQTTSensorComponent::publish_state(float value) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (mqtt::global_mqtt_client->is_publish_nan_as_none() && std::isnan(value))
return this->publish(this->get_state_topic_to_(topic_buf), "None", 4);
int8_t accuracy = this->sensor_->get_accuracy_decimals();
char buf[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(buf, value, accuracy);
return this->publish(this->get_state_topic_to_(topic_buf), buf, len);
return this->publish_float_(this->get_state_topic_to_(topic_buf), value, this->sensor_->get_accuracy_decimals());
}
} // namespace esphome::mqtt
+1 -1
View File
@@ -15,7 +15,7 @@ using namespace esphome::switch_;
MQTTSwitchComponent::MQTTSwitchComponent(switch_::Switch *a_switch) : switch_(a_switch) {}
void MQTTSwitchComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
switch (parse_on_off(payload.c_str())) {
case PARSE_ON:
this->switch_->turn_on();
+1 -1
View File
@@ -19,7 +19,7 @@ PROGMEM_STRING_TABLE(TextMqttModeStrings, "text", "password");
MQTTTextComponent::MQTTTextComponent(Text *text) : text_(text) {}
void MQTTTextComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) {
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &state) {
auto call = this->text_->make_call();
call.set_value(state);
call.perform();
+1 -1
View File
@@ -17,7 +17,7 @@ using namespace esphome::datetime;
MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {}
void MQTTTimeComponent::setup() {
this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) {
this->subscribe_json(this->command_topic_source_(), [this](const std::string &topic, JsonObject root) {
auto call = this->time_->make_call();
if (root[ESPHOME_F("hour")].is<uint8_t>()) {
call.set_hour(root[ESPHOME_F("hour")]);
+1 -1
View File
@@ -15,7 +15,7 @@ using namespace esphome::update;
MQTTUpdateComponent::MQTTUpdateComponent(UpdateEntity *update) : update_(update) {}
void MQTTUpdateComponent::setup() {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
if (payload == "INSTALL") {
this->update_->perform();
} else {
+12 -11
View File
@@ -31,22 +31,23 @@ MQTTValveComponent::MQTTValveComponent(Valve *valve) : valve_(valve) {}
void MQTTValveComponent::setup() {
auto traits = this->valve_->get_traits();
this->valve_->add_on_state_callback([this]() { this->publish_state(); });
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
auto call = this->valve_->make_call();
call.set_command(payload.c_str());
call.perform();
});
if (traits.get_supports_position()) {
this->subscribe(this->get_position_command_topic(), [this](const std::string &topic, const std::string &payload) {
auto value = parse_number<float>(payload);
if (!value.has_value()) {
ESP_LOGW(TAG, "Invalid position value: '%s'", payload.c_str());
return;
}
auto call = this->valve_->make_call();
call.set_position(*value / 100.0f);
call.perform();
});
this->subscribe(this->get_position_command_topic_source(),
[this](const std::string &topic, const std::string &payload) {
auto value = parse_number<float>(payload);
if (!value.has_value()) {
ESP_LOGW(TAG, "Invalid position value: '%s'", payload.c_str());
return;
}
auto call = this->valve_->make_call();
call.set_position(*value / 100.0f);
call.perform();
});
}
}
@@ -198,7 +198,7 @@ OTAResponseTypes IDFOTABackend::end() {
// signature, and only re-points boot at an already-installed app -- so it is
// intentionally not run through the signature verifier below.
if (this->ota_type_ == ota::OTA_TYPE_UPDATE_PARTITION_TABLE) {
return this->hold_lock_if_ok_(this->update_partition_table());
return this->hold_lock_if_ok_(this->update_partition_table_());
}
if (!this->is_app_or_bootloader_update_()) {
return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE;
+1 -1
View File
@@ -54,7 +54,7 @@ class IDFOTABackend final {
OTAResponseTypes validate_new_partition_table_(uint32_t running_app_offset, size_t running_app_size,
PartitionTablePlan &plan);
OTAResponseTypes update_partition_table();
OTAResponseTypes update_partition_table_();
OTAResponseTypes register_and_validate_partition_table_part_();
// Defined in ota_bootloader_esp_idf.cpp:
OTAResponseTypes register_and_validate_bootloader_part_();
@@ -40,7 +40,7 @@ static const esp_partition_t *find_app_partition_at(uint32_t address, size_t min
}
// Validates the staged partition table and picks the post-update boot slot. All non-destructive
// checks live here; the destructive write is in update_partition_table().
// checks live here; the destructive write is in update_partition_table_().
// Side effect: registers the live partition-table region as partition_table_part_ so the caller
// can write to it; abort() releases it on error.
OTAResponseTypes IDFOTABackend::validate_new_partition_table_(uint32_t running_app_offset, size_t running_app_size,
@@ -165,7 +165,7 @@ OTAResponseTypes IDFOTABackend::validate_new_partition_table_(uint32_t running_a
return OTA_RESPONSE_OK;
}
OTAResponseTypes IDFOTABackend::update_partition_table() {
OTAResponseTypes IDFOTABackend::update_partition_table_() {
if (this->buf_written_ == 0 || this->image_size_ != this->buf_written_) {
ESP_LOGE(TAG, "Not enough data received");
return OTA_RESPONSE_ERROR_PARTITION_TABLE_VERIFY;
@@ -300,9 +300,9 @@ OTAResponseTypes IDFOTABackend::register_and_validate_partition_table_part_() {
// Process-scoped cache. Cannot be a backend member: backends are per-connection but the cache
// must outlive a connection that called esp_partition_unload_all(), after which
// esp_ota_get_running_partition() no longer returns valid data.
static bool s_running_app_initialized = false;
static uint32_t s_running_app_cached_offset = 0;
static size_t s_running_app_cached_size = 0;
static bool s_running_app_initialized = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static uint32_t s_running_app_cached_offset = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static size_t s_running_app_cached_size = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Flag-gated rather than size==0 so a failed first call doesn't poison the cache.
void get_running_app_position(uint32_t &offset, size_t &size) {
+1 -2
View File
@@ -29,10 +29,9 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent),
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
.extend(i2c.i2c_device_schema(0x21))
)
+3 -26
View File
@@ -50,24 +50,7 @@ void PCA6416AComponent::setup() {
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&PCA6416AComponent::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
this->disable_loop();
}
void IRAM_ATTR PCA6416AComponent::gpio_intr(PCA6416AComponent *arg) { arg->enable_loop_soon_any_context(); }
void PCA6416AComponent::loop() {
// Invalidate cache at the start of each loop
this->reset_pin_cache_();
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
// stale state forever; keep looping until the line is released so we self-heal.
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
this->disable_loop();
}
this->setup_interrupt_pin_();
}
void PCA6416AComponent::dump_config() {
@@ -113,9 +96,6 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) {
this->update_register_(pin, true, pull_dir);
this->update_register_(pin, false, pull_en);
}
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) {
this->update_register_(pin, true, io_dir);
if (has_pullup_) {
@@ -124,12 +104,11 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) {
} else {
ESP_LOGW(TAG, "Your PCA6416A does not support pull-up resistors");
}
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags == gpio::FLAG_OUTPUT) {
this->update_register_(pin, false, io_dir);
}
if (flags & gpio::FLAG_INPUT)
this->enable_polling_loop_();
}
bool PCA6416AComponent::read_register_(uint8_t reg, uint8_t *value) {
@@ -182,8 +161,6 @@ void PCA6416AComponent::update_register_(uint8_t pin, bool pin_value, uint8_t re
}
}
float PCA6416AComponent::get_setup_priority() const { return setup_priority::IO; }
} // namespace esphome::pca6416a
ESPHOME_EXPANDER_PIN_SUMMARY(esphome::pca6416a::PCA6416AComponent, "%u via PCA6416A")
+2 -11
View File
@@ -3,31 +3,23 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/gpio_expander/cached_gpio_component.h"
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
namespace esphome::pca6416a {
class PCA6416AComponent final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, 16> {
class PCA6416AComponent final : public gpio_expander::CachedGpioExpanderComponent<uint8_t, 16>, public i2c::I2CDevice {
public:
PCA6416AComponent() = default;
/// Check i2c availability and setup masks
void setup() override;
void loop() override;
/// Helper function to set the pin mode of a pin.
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void gpio_intr(PCA6416AComponent *arg);
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
@@ -46,7 +38,6 @@ class PCA6416AComponent final : public Component,
esphome::i2c::ErrorCode last_error_;
/// Only the PCAL6416A has pull-up resistors
bool has_pullup_{false};
InternalGPIOPin *interrupt_pin_{nullptr};
};
using PCA6416AGPIOPin = gpio_expander::ExpanderGPIOPin<PCA6416AComponent>;
+1 -2
View File
@@ -24,10 +24,9 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(PCA9505Component),
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
.extend(i2c.i2c_device_schema(0x20))
)
+3 -28
View File
@@ -38,28 +38,7 @@ void PCA9505Component::setup() {
}
this->write_all_banks_(MASK_REG, 0xFF);
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&PCA9505Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
// Don't invalidate cache on read - only invalidate when interrupt fires
this->set_invalidate_on_read_(false);
}
// Nothing to poll until pin_mode() registers an input pin.
this->disable_loop();
}
void IRAM_ATTR PCA9505Component::gpio_intr(PCA9505Component *arg) { arg->enable_loop_soon_any_context(); }
void PCA9505Component::loop() {
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
this->reset_pin_cache_();
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
// stale state forever; keep looping until the line is released so we self-heal.
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
this->disable_loop();
}
this->setup_interrupt_pin_();
}
void PCA9505Component::dump_config() {
@@ -84,11 +63,9 @@ void PCA9505Component::pin_mode(uint8_t pin, gpio::Flags flags) {
const uint8_t mask = 1 << (pin % 8);
if (flags & gpio::FLAG_INPUT) {
this->config_[bank] &= ~mask;
// An input may raise INT; the polling loop is only needed when there is no interrupt pin.
// An input may raise INT.
this->mask_[bank] &= ~mask;
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
this->enable_polling_loop_();
} else if (flags & gpio::FLAG_OUTPUT) {
this->config_[bank] |= mask;
// An output must never raise INT.
@@ -130,8 +107,6 @@ bool PCA9505Component::write_all_banks_(uint8_t reg, uint8_t value) {
LOG_STR("Failed to write register"));
}
float PCA9505Component::get_setup_priority() const { return setup_priority::IO; }
} // namespace esphome::pca9505
ESPHOME_EXPANDER_PIN_SUMMARY(esphome::pca9505::PCA9505Component, "%u via PCA9505")
+3 -12
View File
@@ -3,7 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/gpio_expander/cached_gpio_component.h"
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
#include "esphome/components/i2c/i2c.h"
@@ -14,23 +14,15 @@ static constexpr uint8_t PIN_COUNT = 40;
/// The pins are grouped into five ports of eight, each with its own set of registers.
static constexpr uint8_t BANK_COUNT = PIN_COUNT / 8;
class PCA9505Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, PIN_COUNT> {
class PCA9505Component final : public gpio_expander::CachedGpioExpanderComponent<uint8_t, PIN_COUNT>,
public i2c::I2CDevice {
public:
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
void setup() override;
void loop() override;
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
void dump_config() override;
protected:
static void gpio_intr(PCA9505Component *arg);
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
@@ -47,7 +39,6 @@ class PCA9505Component final : public Component,
uint8_t input_[BANK_COUNT]{};
/// Which pins may pull INT low - 1 means masked, matching the chip
uint8_t mask_[BANK_COUNT]{};
InternalGPIOPin *interrupt_pin_{nullptr};
};
using PCA9505GPIOPin = gpio_expander::ExpanderGPIOPin<PCA9505Component>;
+2 -4
View File
@@ -2,9 +2,8 @@ from typing import NamedTuple
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components import gpio_expander, i2c
from esphome.components.const import CONF_DRIVE_STRENGTH
from esphome.components.gpio_expander import validate_interrupt_pin
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
@@ -90,9 +89,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ADDRESS): cv.i2c_address,
cv.Optional(CONF_DEVICE): cv.one_of(*DEVICES, upper=True),
cv.Optional(CONF_PIN_COUNT): cv.one_of(4, 8, 16),
cv.Optional(CONF_INTERRUPT_PIN): validate_interrupt_pin,
}
).extend(cv.COMPONENT_SCHEMA),
).extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA),
_validate_hub,
)
+2 -30
View File
@@ -52,29 +52,7 @@ void PCA9554Component::setup() {
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&PCA9554Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
// Don't invalidate cache on read — only invalidate when interrupt fires
this->set_invalidate_on_read_(false);
}
// Disable loop until an input pin is configured via pin_mode()
// For interrupt-driven mode, loop is re-enabled by the ISR
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
this->disable_loop();
}
void IRAM_ATTR PCA9554Component::gpio_intr(PCA9554Component *arg) { arg->enable_loop_soon_any_context(); }
void PCA9554Component::loop() {
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
this->reset_pin_cache_();
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
// stale state forever; keep looping until the line is released so we self-heal.
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
this->disable_loop();
}
this->setup_interrupt_pin_();
}
void PCA9554Component::dump_config() {
@@ -113,11 +91,7 @@ void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags & gpio::FLAG_INPUT) {
// Clear mode mask bit
this->config_mask_ &= ~(1 << pin);
// Enable polling loop for input pins (not needed for interrupt-driven mode
// where the ISR handles re-enabling loop)
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
this->enable_polling_loop_();
} else if (flags & gpio::FLAG_OUTPUT) {
// Set mode mask bit
this->config_mask_ |= 1 << pin;
@@ -204,8 +178,6 @@ void PCA9554Component::update_pcal_register_(uint8_t reg, uint16_t &shadow, uint
}
#endif
float PCA9554Component::get_setup_priority() const { return setup_priority::IO; }
} // namespace esphome::pca9554
ESPHOME_EXPANDER_PIN_SUMMARY(esphome::pca9554::PCA9554Component, "%u via PCA9554")
+2 -11
View File
@@ -3,28 +3,22 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/gpio_expander/cached_gpio_component.h"
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
namespace esphome::pca9554 {
class PCA9554Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint16_t, 16> {
class PCA9554Component final : public gpio_expander::CachedGpioExpanderComponent<uint16_t, 16>, public i2c::I2CDevice {
public:
explicit PCA9554Component(uint8_t pin_count) : pin_count_(pin_count), reg_width_((pin_count + 7) / 8) {}
/// Check i2c availability and setup masks
void setup() override;
void loop() override;
/// Helper function to set the pin mode of a pin.
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
#ifdef USE_PCA9554_PCAL
/// The PCAL parts add pull resistors, input latches, an interrupt mask and drive strength.
void set_pcal() { this->pcal_ = true; }
@@ -38,8 +32,6 @@ class PCA9554Component final : public Component,
#endif
protected:
static void gpio_intr(PCA9554Component *arg);
bool read_inputs_();
bool write_register_(uint8_t reg, uint16_t value) { return this->write_address_(reg * this->reg_width_, value); }
bool write_address_(uint8_t address, uint16_t value);
@@ -61,7 +53,6 @@ class PCA9554Component final : public Component,
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
InternalGPIOPin *interrupt_pin_{nullptr};
#ifdef USE_PCA9554_PCAL
/// Drive strength shadow, two bits per pin; the low reg_width_ bytes go to OUTPUT_DRIVE_0
uint32_t drive_{0xFFFFFFFF};
+1 -2
View File
@@ -36,10 +36,9 @@ CONFIG_SCHEMA = (
cv.Required(CONF_ID): cv.declare_id(PCF8574Component),
cv.Optional(CONF_PCF8575, default=False): cv.boolean,
cv.Optional(CONF_RESET, default=DEFAULT_RESET): cv.boolean,
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
.extend(i2c.i2c_device_schema(0x21))
)
+2 -27
View File
@@ -21,27 +21,7 @@ void PCF8574Component::setup() {
this->mode_mask_ = ~this->configured_input_pins_;
}
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&PCF8574Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
// Don't invalidate cache on read — only invalidate when interrupt fires
this->set_invalidate_on_read_(false);
}
// Disable loop until an input pin is configured via pin_mode()
// For interrupt-driven mode, loop is re-enabled by the ISR
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
this->disable_loop();
}
void IRAM_ATTR PCF8574Component::gpio_intr(PCF8574Component *arg) { arg->enable_loop_soon_any_context(); }
void PCF8574Component::loop() {
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
this->reset_pin_cache_();
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
// stale state forever; keep looping until the line is released so we self-heal.
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
this->disable_loop();
}
this->setup_interrupt_pin_();
}
void PCF8574Component::dump_config() {
ESP_LOGCONFIG(TAG,
@@ -69,11 +49,7 @@ void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
this->mode_mask_ &= ~(1 << pin);
// Write GPIO to enable input mode
this->write_gpio_();
// Enable polling loop for input pins (not needed for interrupt-driven mode
// where the ISR handles re-enabling loop)
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
this->enable_polling_loop_();
} else if (flags == gpio::FLAG_OUTPUT) {
// Set mode mask bit
this->mode_mask_ |= 1 << pin;
@@ -103,7 +79,6 @@ bool PCF8574Component::write_gpio_() {
data[1] = value >> 8;
return this->status_comm_result(this->write(data, this->pcf8575_ ? 2 : 1) == i2c::ERROR_OK);
}
float PCF8574Component::get_setup_priority() const { return setup_priority::IO; }
} // namespace esphome::pcf8574
+2 -11
View File
@@ -3,16 +3,14 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/gpio_expander/cached_gpio_component.h"
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
namespace esphome::pcf8574 {
// PCF8574(8 pins)/PCF8575(16 pins) always read/write all pins in a single I2C transaction
// so we use uint16_t as bank type to ensure all pins are in one bank and cached together
class PCF8574Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint16_t, 16> {
class PCF8574Component final : public gpio_expander::CachedGpioExpanderComponent<uint16_t, 16>, public i2c::I2CDevice {
public:
PCF8574Component() = default;
@@ -20,21 +18,15 @@ class PCF8574Component final : public Component,
void set_reset(bool reset) { this->reset_ = reset; }
// Called by pin setters before any component's setup or GPIO writes.
void register_input_pin(uint8_t pin) { this->configured_input_pins_ |= 1 << pin; }
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
/// Check i2c availability and setup masks
void setup() override;
void loop() override;
/// Helper function to set the pin mode of a pin.
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
void dump_config() override;
protected:
static void gpio_intr(PCF8574Component *arg);
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
@@ -54,7 +46,6 @@ class PCF8574Component final : public Component,
// Keep in sync with DEFAULT_RESET in __init__.py; codegen omits the setter for the default.
bool reset_{true};
bool pcf8575_; ///< TRUE->16-channel PCF8575, FALSE->8-channel PCF8574
InternalGPIOPin *interrupt_pin_{nullptr};
};
using PCF8574GPIOPin = gpio_expander::ExpanderGPIOPin<PCF8574Component>;
+1 -2
View File
@@ -34,10 +34,9 @@ CONFIG_SCHEMA = (
{
cv.Required(CONF_ID): cv.declare_id(PI4IOE5V6408Component),
cv.Optional(CONF_RESET, default=True): cv.boolean,
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
.extend(i2c.i2c_device_schema(0x43))
)
@@ -34,20 +34,8 @@ void PI4IOE5V6408Component::setup() {
}
}
// No need to clear latched interrupts before attaching the ISR — if INT is
// already low the ISR fires immediately, loop runs, cache invalidates, and
// the read clears the latch. One harmless extra read at most.
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&PI4IOE5V6408Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
// Disable loop until an input pin is configured via pin_mode()
// For interrupt-driven mode, loop is re-enabled by the ISR
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
this->disable_loop();
this->setup_interrupt_pin_();
}
void IRAM_ATTR PI4IOE5V6408Component::gpio_intr(PI4IOE5V6408Component *arg) { arg->enable_loop_soon_any_context(); }
void PI4IOE5V6408Component::dump_config() {
ESP_LOGCONFIG(TAG, "PI4IOE5V6408:");
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
@@ -67,26 +55,12 @@ void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) {
this->pull_up_down_mask_ &= ~(1 << pin);
this->pull_enable_mask_ |= 1 << pin;
}
// Enable polling loop for input pins (not needed for interrupt-driven mode
// where the ISR handles re-enabling loop)
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
this->enable_polling_loop_();
}
// Write GPIO to enable input mode
this->write_gpio_modes_();
}
void PI4IOE5V6408Component::loop() {
this->reset_pin_cache_();
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
// stale state forever; keep looping until the line is released so we self-heal.
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
this->disable_loop();
}
}
bool PI4IOE5V6408Component::read_gpio_outputs_() {
if (this->is_failed())
return false;
@@ -177,8 +151,6 @@ bool PI4IOE5V6408Component::write_gpio_modes_() {
bool PI4IOE5V6408Component::digital_read_cache(uint8_t pin) { return (this->input_mask_ & (1 << pin)); }
float PI4IOE5V6408Component::get_setup_priority() const { return setup_priority::IO; }
} // namespace esphome::pi4ioe5v6408
ESPHOME_EXPANDER_PIN_SUMMARY(esphome::pi4ioe5v6408::PI4IOE5V6408Component, "%u via PI4IOE5V6408")
+3 -10
View File
@@ -1,32 +1,26 @@
#pragma once
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/gpio_expander/cached_gpio_component.h"
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
namespace esphome::pi4ioe5v6408 {
class PI4IOE5V6408Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, 8> {
class PI4IOE5V6408Component final : public gpio_expander::CachedGpioExpanderComponent<uint8_t, 8>,
public i2c::I2CDevice {
public:
PI4IOE5V6408Component() = default;
void setup() override;
void pin_mode(uint8_t pin, gpio::Flags flags);
float get_setup_priority() const override;
void dump_config() override;
void loop() override;
/// Indicate if the component should reset the state during setup
void set_reset(bool reset) { this->reset_ = reset; }
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void gpio_intr(PI4IOE5V6408Component *arg);
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
@@ -43,7 +37,6 @@ class PI4IOE5V6408Component final : public Component,
uint8_t pull_up_down_mask_{0x00};
bool reset_{true};
InternalGPIOPin *interrupt_pin_{nullptr};
bool read_gpio_modes_();
bool write_gpio_modes_();
+53 -18
View File
@@ -24,12 +24,16 @@ enum class BitOrder : uint8_t {
BIT_ORDER_MSB_FIRST,
};
/// A bit is bit_mark followed by one_space or zero_space.
/// A bit is a mark (one_mark or zero_mark, both bit_mark unless set) and a space (one_space or zero_space),
/// mark first unless space_first.
struct BitTimings {
uint32_t bit_mark;
uint32_t bit_mark{0};
uint32_t one_space;
uint32_t zero_space;
BitOrder bit_order{BitOrder::BIT_ORDER_LSB_FIRST};
uint32_t one_mark{bit_mark};
uint32_t zero_mark{bit_mark};
bool space_first{false};
};
class RemoteTransmitData {
@@ -43,10 +47,17 @@ class RemoteTransmitData {
void reserve(uint32_t len) { this->data_.reserve(len); }
/// One item per bit of each byte.
template<BitTimings T> void item_bytes(const uint8_t *bytes, size_t len) {
static_assert(T.one_mark != 0 && T.zero_mark != 0, "set bit_mark, or one_mark and zero_mark");
for (size_t i = 0; i < len; i++) {
for (uint8_t bit = 0; bit < 8; bit++) {
const uint8_t mask = T.bit_order == BitOrder::BIT_ORDER_MSB_FIRST ? (0x80 >> bit) : (1 << bit);
this->item(T.bit_mark, (bytes[i] & mask) ? T.one_space : T.zero_space);
const bool one = (bytes[i] & mask) != 0;
if constexpr (T.space_first) {
this->space(one ? T.one_space : T.zero_space);
this->mark(one ? T.one_mark : T.zero_mark);
} else {
this->item(one ? T.one_mark : T.zero_mark, one ? T.one_space : T.zero_space);
}
}
}
}
@@ -99,21 +110,12 @@ class RemoteReceiveData {
bool expect_space(uint32_t length);
bool expect_item(uint32_t mark, uint32_t space);
bool expect_pulse_with_gap(uint32_t mark, uint32_t space);
/// False at the first item matching neither space, leaving it unread.
template<BitTimings T> bool expect_bytes(uint8_t *out, size_t len) {
for (size_t i = 0; i < len; i++) {
uint8_t byte = 0;
for (uint8_t bit = 0; bit < 8; bit++) {
const uint8_t mask = T.bit_order == BitOrder::BIT_ORDER_MSB_FIRST ? (0x80 >> bit) : (1 << bit);
if (this->expect_item(T.bit_mark, T.one_space)) {
byte |= mask;
} else if (!this->expect_item(T.bit_mark, T.zero_space)) {
return false;
}
}
out[i] = byte;
}
return true;
/// False at the first bit matching neither the one nor the zero timings, leaving it unread.
template<BitTimings T> bool expect_bytes(uint8_t *out, size_t len) { return this->expect_bytes_<T, false>(out, len); }
/// As expect_bytes, but the last bit is only its mark: its space runs into the gap after the frame.
template<BitTimings T> bool expect_bytes_last_mark(uint8_t *out, size_t len) {
static_assert(!T.space_first && T.one_mark != T.zero_mark, "the last bit must be told apart by its mark");
return this->expect_bytes_<T, true>(out, len);
}
void advance(uint32_t amount = 1) { this->index_ += amount; }
void reset() { this->index_ = 0; }
@@ -126,6 +128,39 @@ class RemoteReceiveData {
ToleranceMode get_tolerance_mode() { return this->tolerance_mode_; }
protected:
template<BitTimings T, bool LAST_MARK> bool expect_bytes_(uint8_t *out, size_t len) {
static_assert(T.one_mark != 0 && T.zero_mark != 0, "set bit_mark, or one_mark and zero_mark");
for (size_t i = 0; i < len; i++) {
uint8_t byte = 0;
for (uint8_t bit = 0; bit < 8; bit++) {
const uint8_t mask = T.bit_order == BitOrder::BIT_ORDER_MSB_FIRST ? (0x80 >> bit) : (1 << bit);
if (LAST_MARK && i + 1 == len && bit == 7) {
if (this->expect_mark(T.one_mark)) {
byte |= mask;
} else if (!this->expect_mark(T.zero_mark)) {
return false;
}
} else if (this->expect_bit_<T>(T.one_mark, T.one_space)) {
byte |= mask;
} else if (!this->expect_bit_<T>(T.zero_mark, T.zero_space)) {
return false;
}
}
out[i] = byte;
}
return true;
}
/// One bit in T's order, consumed only when both its parts match.
template<BitTimings T> bool expect_bit_(uint32_t mark, uint32_t space) {
if constexpr (T.space_first) {
if (!this->peek_space(space) || !this->peek_mark(mark, 1))
return false;
this->advance(2);
return true;
} else {
return this->expect_item(mark, space);
}
}
int32_t lower_bound_(uint32_t length) const {
if (this->tolerance_mode_ == TOLERANCE_MODE_TIME) {
return int32_t(length - this->tolerance_);
@@ -141,17 +141,6 @@ void RP2040PIOLEDStripLightOutput::write_state(light::LightState *state) {
dma_channel_transfer_from_buffer_now(this->dma_chan_, this->buf_, this->get_buffer_size_());
}
light::ESPColorView RP2040PIOLEDStripLightOutput::get_view_internal(int32_t index) const {
const light::ChannelColors &colors = this->channel_colors_;
uint8_t *led = this->buf_ + (index * colors.bytes_per_led());
return {led + colors.r,
led + colors.g,
led + colors.b,
colors.has_white() ? led + colors.w : nullptr,
&this->effect_data_[index],
&this->correction_};
}
void RP2040PIOLEDStripLightOutput::dump_config() {
char channel_colors[5];
ESP_LOGCONFIG(TAG,
@@ -37,13 +37,7 @@ class RP2040PIOLEDStripLightOutput final : public light::AddressableLight {
float get_setup_priority() const override;
int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
this->channel_colors_.has_white()
? traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE})
: traits.set_supported_color_modes({light::ColorMode::RGB});
return traits;
}
light::LightTraits get_traits() override { return this->channel_colors_.traits(); }
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_num_leds(uint32_t num_leds) { this->num_leds_ = num_leds; }
void set_channel_colors(light::ChannelColors channel_colors) { this->channel_colors_ = channel_colors; }
@@ -56,15 +50,16 @@ class RP2040PIOLEDStripLightOutput final : public light::AddressableLight {
void set_chipset(Chipset chipset) { this->chipset_ = chipset; };
void clear_effect_data() override {
for (int i = 0; i < this->size(); i++) {
this->effect_data_[i] = 0;
}
if (this->effect_data_ != nullptr)
memset(this->effect_data_, 0, this->num_leds_);
}
void dump_config() override;
protected:
light::ESPColorView get_view_internal(int32_t index) const override;
light::ESPColorView get_view_internal(int32_t index) const override {
return this->channel_colors_.view(this->buf_, this->effect_data_, index, &this->correction_);
}
size_t get_buffer_size_() const { return this->num_leds_ * this->channel_colors_.bytes_per_led(); }
@@ -84,7 +79,6 @@ class RP2040PIOLEDStripLightOutput final : public light::AddressableLight {
light::ChannelColors channel_colors_{0, 1, 2, light::ChannelColors::NO_WHITE};
Chipset chipset_{CHIPSET_CUSTOM};
uint32_t last_refresh_{0};
float max_refresh_rate_;
const pio_program_t *program_;
+5 -9
View File
@@ -64,17 +64,13 @@ void SafeModeComponent::dump_config() {
this->safe_mode_num_attempts_,
this->safe_mode_enable_time_ / 1000); // because milliseconds
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
const char *state_str;
if (this->ota_state_ == ESP_OTA_IMG_NEW) {
#ifdef USE_OTA_PARTITIONS
state_str = "support unknown";
#else
const char *state_str = "support unknown";
if (this->ota_state_ == ESP_OTA_IMG_PENDING_VERIFY) {
state_str = "supported";
#ifndef USE_OTA_PARTITIONS
} else if (this->ota_state_ == ESP_OTA_IMG_NEW) {
state_str = "not supported";
#endif
} else if (this->ota_state_ == ESP_OTA_IMG_PENDING_VERIFY) {
state_str = "supported";
} else {
state_str = "support unknown";
}
ESP_LOGCONFIG(TAG, " Bootloader rollback: %s", state_str);
#endif
+7
View File
@@ -18,6 +18,7 @@ CONF_ON_USSD_RECEIVED = "on_ussd_received"
CONF_ON_INCOMING_CALL = "on_incoming_call"
CONF_ON_CALL_CONNECTED = "on_call_connected"
CONF_ON_CALL_DISCONNECTED = "on_call_disconnected"
CONF_ON_DTMF_RECEIVED = "on_dtmf_received"
CONF_RECIPIENT = "recipient"
CONF_USSD = "ussd"
@@ -30,6 +31,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ON_CALL_CONNECTED): automation.validate_automation({}),
cv.Optional(CONF_ON_CALL_DISCONNECTED): automation.validate_automation({}),
cv.Optional(CONF_ON_USSD_RECEIVED): automation.validate_automation({}),
cv.Optional(CONF_ON_DTMF_RECEIVED): automation.validate_automation({}),
}
)
.extend(cv.polling_component_schema("5s"))
@@ -62,6 +64,11 @@ _CALLBACK_AUTOMATIONS = (
"add_on_ussd_received_callback",
[(cg.std_string, "ussd")],
),
automation.CallbackAutomation(
CONF_ON_DTMF_RECEIVED,
"add_on_dtmf_received_callback",
[(cg.std_string, "key")],
),
)
+30 -1
View File
@@ -14,6 +14,8 @@ void Sim800LComponent::update() {
if (this->watch_dog_++ == 2) {
this->state_ = STATE_INIT;
this->write(26);
// The module may have restarted, enable DTMF detection again
this->dtmf_ready_ = false;
}
if (this->expect_ack_)
@@ -82,6 +84,15 @@ void Sim800LComponent::parse_cmd_(std::string message) {
this->call_state_ = 6;
this->call_disconnected_callback_.call();
}
} else if (message.starts_with("+DTMF:")) {
// Unsolicited DTMF key report: "+DTMF: <key>[,<duration>]"
size_t start = message.find_first_not_of(' ', 6);
if (start != std::string::npos) {
std::string key = message.substr(start, message.find(',', start) - start);
ESP_LOGD(TAG, "DTMF received: %s", key.c_str());
this->dtmf_received_callback_.call(key);
}
return;
}
}
@@ -93,6 +104,14 @@ void Sim800LComponent::parse_cmd_(std::string message) {
// Expected ack but AT echo received
this->state_ = STATE_DISABLE_ECHO;
this->expect_ack_ = true;
} else if (this->state_ == STATE_DDET_SENT) {
if (message != "ERROR" && !message.starts_with("+CME ERROR:")) {
// Unrelated unsolicited line, keep waiting for the DDET response
this->expect_ack_ = true;
return;
}
// Module firmware without DTMF support, carry on with the setup sequence
ESP_LOGW(TAG, "DTMF detection not supported by the module: %s", message.c_str());
} else {
ESP_LOGW(TAG, "Not ack. %d %s", this->state_, message.c_str());
this->state_ = STATE_IDLE; // Let it timeout
@@ -145,7 +164,12 @@ void Sim800LComponent::parse_cmd_(std::string message) {
break;
case STATE_SETUP_CLIP:
send_cmd_("AT+CLIP=1");
this->state_ = STATE_CREG;
this->state_ = this->dtmf_enabled_ && !this->dtmf_ready_ ? STATE_SETUP_DDET : STATE_CREG;
this->expect_ack_ = true;
break;
case STATE_SETUP_DDET:
send_cmd_("AT+DDET=1");
this->state_ = STATE_DDET_SENT;
this->expect_ack_ = true;
break;
case STATE_SETUP_USSD:
@@ -188,6 +212,11 @@ void Sim800LComponent::parse_cmd_(std::string message) {
if (message == "OK")
this->state_ = STATE_INIT;
break;
case STATE_DDET_SENT:
// Tried once per module reset; firmware without DDET only logs a warning, and a passing error is retried
// after the next reset
this->dtmf_ready_ = true;
[[fallthrough]];
case STATE_CREG:
send_cmd_("AT+CREG?");
this->state_ = STATE_CREG_WAIT;
+11 -1
View File
@@ -44,7 +44,9 @@ enum State {
STATE_SEND_USSD1,
STATE_SEND_USSD2,
STATE_CHECK_USSD,
STATE_RECEIVED_USSD
STATE_RECEIVED_USSD,
STATE_SETUP_DDET,
STATE_DDET_SENT
};
class Sim800LComponent final : public uart::UARTDevice, public PollingComponent {
@@ -76,6 +78,11 @@ class Sim800LComponent final : public uart::UARTDevice, public PollingComponent
template<typename F> void add_on_ussd_received_callback(F &&callback) {
this->ussd_received_callback_.add(std::forward<F>(callback));
}
template<typename F> void add_on_dtmf_received_callback(F &&callback) {
// DTMF detection is only enabled on the module when someone listens for it
this->dtmf_enabled_ = true;
this->dtmf_received_callback_.add(std::forward<F>(callback));
}
void send_sms(const std::string &recipient, const std::string &message);
void send_ussd(const std::string &ussd_code);
void dial(const std::string &recipient);
@@ -111,6 +118,8 @@ class Sim800LComponent final : public uart::UARTDevice, public PollingComponent
bool connect_pending_{false};
bool disconnect_pending_{false};
bool send_ussd_pending_{false};
bool dtmf_enabled_{false};
bool dtmf_ready_{false};
uint8_t call_state_{6};
CallbackManager<void(std::string, std::string)> sms_received_callback_;
@@ -118,6 +127,7 @@ class Sim800LComponent final : public uart::UARTDevice, public PollingComponent
CallbackManager<void()> call_connected_callback_;
CallbackManager<void()> call_disconnected_callback_;
CallbackManager<void(std::string)> ussd_received_callback_;
CallbackManager<void(std::string)> dtmf_received_callback_;
};
} // namespace esphome::sim800l
Loaded 100 of 248 files, more files were not shown because too many files have changed in this diff. Show more