Compare commits

..
254 changed files with 1174 additions and 8870 deletions

No files matched your search

+1 -2
View File
@@ -209,8 +209,7 @@ 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`, published on a text sensor with
`publish_state(Table::get_progmem_str(idx, fallback))`, which copies it out of flash on ESP8266.
logged with `get_log_str` and `LOG_STR_ARG`, copied out with `ESPHOME_strncpy_P` before `publish_state`.
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,7 +436,6 @@ 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,5 +84,3 @@ 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,7 +49,6 @@ from esphome.cpp_generator import ( # noqa: F401
static_const_array,
static_function,
templatable,
unique_global_id,
variable,
with_local_variable,
)
@@ -91,19 +91,17 @@ 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 = ALARM_CONTROL_PANEL_ACTION_SCHEMA
ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(AlarmControlPanel),
}
)
_CALLBACK_AUTOMATIONS = (
@@ -196,7 +194,7 @@ for _name, _arm in (
):
automation.register_apply_action(
_name,
ALARM_CONTROL_PANEL_CODE_ACTION_SCHEMA,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
automation.ApplyCall(_arm),
automation.ApplyField(CONF_CODE, "set_code", cg.std_string),
call="make_call",
@@ -293,9 +293,14 @@ 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->throttle_show_(now, this->last_refresh_, this->max_refresh_rate_))
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();
return;
}
this->last_refresh_ = now;
this->mark_shown_();
@@ -364,6 +369,17 @@ 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"
@@ -374,7 +390,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_, this->num_leds_);
this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_.value_or(0), this->num_leds_);
}
float BekenSPILEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -18,7 +18,15 @@ 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 { return this->channel_colors_.traits(); }
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;
}
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; }
@@ -30,16 +38,14 @@ 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 {
if (this->effect_data_ != nullptr)
memset(this->effect_data_, 0, this->num_leds_);
for (int i = 0; i < this->size(); i++)
this->effect_data_[i] = 0;
}
void dump_config() override;
protected:
light::ESPColorView get_view_internal(int32_t index) const override {
return this->channel_colors_.view(this->buf_, this->effect_data_, index, &this->correction_);
}
light::ESPColorView get_view_internal(int32_t index) const override;
size_t get_buffer_size_() const { return this->num_leds_ * this->channel_colors_.bytes_per_led(); }
@@ -56,7 +62,7 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
light::ChannelColors channel_colors_{0, 1, 2, light::ChannelColors::NO_WHITE};
uint32_t last_refresh_{0};
uint32_t max_refresh_rate_{0};
optional<uint32_t> max_refresh_rate_{};
};
} // namespace esphome::beken_spi_led_strip
+2 -3
View File
@@ -24,9 +24,7 @@ climate::ClimateTraits ClimateIR::traits() {
return traits;
}
void ClimateIR::setup() { this->setup_(this->restore_state_()); }
void ClimateIR::setup_(optional<climate::ClimateDeviceRestoreState> restore) {
void ClimateIR::setup() {
if (this->sensor_) {
this->sensor_->add_on_state_callback([this](float state) {
this->current_temperature = state;
@@ -45,6 +43,7 @@ void ClimateIR::setup_(optional<climate::ClimateDeviceRestoreState> restore) {
}
// restore set points
auto restore = this->restore_state_();
if (restore.has_value()) {
restore->apply(this);
} else {
@@ -60,10 +60,6 @@ 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"
#if defined(USE_ESP32) && defined(USE_FAN)
#ifdef USE_ESP32
#include "ecocomfort2_fan.h"
#include "esphome/core/log.h"
@@ -138,4 +138,4 @@ void Ecocomfort2Fan::on_status() {
} // namespace esphome::ecocomfort2
#endif // USE_ESP32 && USE_FAN
#endif // USE_ESP32
@@ -1,7 +1,7 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ESP32) && defined(USE_FAN)
#ifdef USE_ESP32
#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 && USE_FAN
#endif // USE_ESP32
-2
View File
@@ -56,7 +56,6 @@ 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
@@ -3994,7 +3993,6 @@ 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
-1
View File
@@ -667,7 +667,6 @@ 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,9 +157,14 @@ 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();
if (this->throttle_show_(now, this->last_refresh_, this->max_refresh_rate_))
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();
return;
}
this->last_refresh_ = now;
this->mark_shown_();
@@ -212,6 +217,17 @@ 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"
@@ -223,7 +239,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_, this->num_leds_);
this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_.value_or(0), this->num_leds_);
}
float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -29,7 +29,15 @@ 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 { return this->channel_colors_.traits(); }
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;
}
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_inverted(bool inverted) { this->invert_out_ = inverted; }
@@ -47,16 +55,14 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; }
void clear_effect_data() override {
if (this->effect_data_ != nullptr)
memset(this->effect_data_, 0, this->num_leds_);
for (int i = 0; i < this->size(); i++)
this->effect_data_[i] = 0;
}
void dump_config() override;
protected:
light::ESPColorView get_view_internal(int32_t index) const override {
return this->channel_colors_.view(this->buf_, this->effect_data_, index, &this->correction_);
}
light::ESPColorView get_view_internal(int32_t index) const override;
size_t get_buffer_size_() const { return this->num_leds_ * this->channel_colors_.bytes_per_led(); }
@@ -80,7 +86,7 @@ class ESP32RMTLEDStripLightOutput final : public light::AddressableLight {
light::ChannelColors channel_colors_{0, 1, 2, light::ChannelColors::NO_WHITE};
uint32_t last_refresh_{0};
uint32_t max_refresh_rate_{0};
optional<uint32_t> max_refresh_rate_{};
};
} // namespace esphome::esp32_rmt_led_strip
@@ -30,13 +30,6 @@ 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
@@ -283,31 +276,6 @@ 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
@@ -319,7 +287,6 @@ def _consume_ota_sockets(config: ConfigType) -> ConfigType:
CONFIG_SCHEMA = cv.All(
_validate_platform,
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
@@ -1,4 +1,3 @@
from dataclasses import dataclass, field
import logging
from pathlib import Path
from typing import Any
@@ -25,33 +24,6 @@ _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,
@@ -95,15 +67,6 @@ 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
@@ -143,7 +106,6 @@ 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,16 +48,12 @@ 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;
}
@@ -65,7 +61,6 @@ 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_);
@@ -74,7 +69,6 @@ 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()");
}
@@ -198,68 +192,51 @@ 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");
@@ -271,56 +248,41 @@ 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:
@@ -527,33 +489,25 @@ 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,9 +22,3 @@ 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}
)
@@ -1,29 +0,0 @@
#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
@@ -1,50 +0,0 @@
#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,8 +1,6 @@
#include "hitachi_ac344.h"
#include "esphome/core/helpers.h"
#include <cstring>
namespace esphome::hitachi_ac344 {
ESPHOME_LOG_TAG(TAG, "climate.hitachi_ac344");
@@ -103,7 +101,21 @@ 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);
}
@@ -117,59 +129,18 @@ 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; }
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) {
void HitachiClimate::transmit_state() {
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
set_mode_(HITACHI_AC344_MODE_COOL);
@@ -196,9 +167,6 @@ void HitachiClimate::transmit_state_(uint8_t button) {
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;
@@ -214,18 +182,27 @@ void HitachiClimate::transmit_state_(uint8_t button) {
set_fan_(HITACHI_AC344_FAN_AUTO);
}
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);
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;
}
// 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;
// TODO: find change value to set button, now always set to power button
set_button_(HITACHI_AC344_BUTTON_POWER);
invert_byte_pairs(remote_state_ + 3, HITACHI_AC344_STATE_LENGTH - 3);
@@ -298,8 +275,6 @@ 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;
@@ -323,13 +298,11 @@ 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 != HITACHI_AC344_SWINGH_AUTO) {
this->swing_mode = swing_v ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
if ((swing_modeh & 0x3) == 0x3) {
this->swing_mode = climate::CLIMATE_SWING_OFF;
} else {
this->swing_mode = swing_v ? climate::CLIMATE_SWING_BOTH : climate::CLIMATE_SWING_HORIZONTAL;
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
}
return true;
@@ -372,10 +345,9 @@ 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();
this->publish_mildew_proof_();
for (uint8_t i = 0; i < HITACHI_AC344_STATE_LENGTH; i++)
remote_state_[i] = recv_state[i];
return true;
}
@@ -2,9 +2,6 @@
#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 {
@@ -82,18 +79,9 @@ 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_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;
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL}) {}
protected:
uint8_t remote_state_[HITACHI_AC344_STATE_LENGTH]{0x01, 0x10, 0x00, 0x40, 0x00, 0xFF, 0x00, 0xCC, 0x00, 0x00, 0x00,
@@ -101,14 +89,8 @@ 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_();
@@ -116,10 +98,13 @@ 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;
@@ -1,30 +0,0 @@
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)"
)
+1 -2
View File
@@ -322,9 +322,8 @@ 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,12 +149,17 @@ 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) {
// The effect data follows the color data
this->color_data_ = allocator.allocate(this->color_data_bytes_ + this->num_leds_);
this->color_data_ = allocator.allocate(this->color_data_bytes_);
if (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;
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;
}
allocator.deallocate(this->i2s_data_, i2s_data_bytes);
this->i2s_data_ = nullptr;
@@ -162,6 +167,16 @@ 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;
@@ -221,6 +236,19 @@ 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,7 +8,6 @@
#include <atomic>
#include <cstdint>
#include <cstring>
#include "esphome/core/component.h"
#include "esphome/core/log.h"
@@ -28,27 +27,20 @@ class I2SClocklessLedStrip final : public light::AddressableLight {
float get_setup_priority() const override;
void setup() override;
light::LightTraits get_traits() override { return this->channel_colors_.traits(); }
light::LightTraits get_traits() override;
int32_t size() const override { return this->num_leds_; }
void clear_effect_data() override {
if (uint8_t *effect = this->effect_data_())
memset(effect, 0, this->num_leds_);
}
void clear_effect_data() override;
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_();
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_);
}
light::ESPColorView get_view_internal(int32_t index) const override;
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};
+76 -32
View File
@@ -9,7 +9,6 @@
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cinttypes>
@@ -20,6 +19,23 @@ namespace esphome::ld2450 {
ESPHOME_LOG_TAG(TAG, "ld2450");
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8
};
enum ZoneType : uint8_t {
ZONE_DISABLED = 0,
ZONE_DETECTION = 1,
ZONE_FILTER = 2,
};
enum PeriodicData : uint8_t {
TARGET_X = 4,
TARGET_Y = 6,
@@ -38,20 +54,61 @@ enum AckData : uint8_t {
COMMAND_STATUS = 7,
};
// Index + 1 is the BaudRate value; the select options use the same order
PROGMEM_STRING_TABLE(BaudRateStrings, "9600", "19200", "38400", "57600", "115200", "230400", "256000", "460800");
static_assert(BAUD_RATE_9600 == 1 && BAUD_RATE_460800 == BaudRateStrings::COUNT, "BaudRate is index + 1");
// Index is the Direction value
PROGMEM_STRING_TABLE(DirectionStrings, "Approaching", "Moving away", "Stationary", "NA");
static_assert(DIRECTION_APPROACHING == 0 && DIRECTION_MOVING_AWAY == 1 && DIRECTION_STATIONARY == 2 &&
DIRECTION_NA == 3,
"Direction is the table index");
// Index is the ZoneType value; the select options use the same order
PROGMEM_STRING_TABLE(ZoneTypeStrings, "Disabled", "Detection", "Filter");
static_assert(ZONE_DISABLED == 0 && ZONE_DETECTION == 1 && ZONE_FILTER == 2, "ZoneType is the table index");
// Memory-efficient lookup tables
struct StringToUint8 {
const char *str;
const uint8_t value;
};
// Baud rates in the same order as BaudRateStrings for index-based lookup
static constexpr uint32_t BAUD_RATES[] PROGMEM = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
struct Uint8ToString {
const uint8_t value;
const char *str;
};
constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
{"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
{"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
{"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
};
constexpr Uint8ToString DIRECTION_BY_UINT[] = {
{DIRECTION_APPROACHING, "Approaching"},
{DIRECTION_MOVING_AWAY, "Moving away"},
{DIRECTION_STATIONARY, "Stationary"},
{DIRECTION_NA, "NA"},
};
constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = {
{ZONE_DISABLED, "Disabled"},
{ZONE_DETECTION, "Detection"},
{ZONE_FILTER, "Filter"},
};
constexpr StringToUint8 ZONE_TYPE_BY_STR[] = {
{"Disabled", ZONE_DISABLED},
{"Detection", ZONE_DETECTION},
{"Filter", ZONE_FILTER},
};
// Baud rates in the same order as BAUD_RATES_BY_STR for index-based lookup
constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
// Helper functions for lookups
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
for (const auto &entry : arr) {
if (str == entry.str)
return entry.value;
}
return 0xFF; // Not found
}
template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) {
for (const auto &entry : arr) {
if (value == entry.value)
return entry.str;
}
return ""; // Not found
}
// LD2450 UART Serial Commands
static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
@@ -489,7 +546,7 @@ void LD2450Component::handle_periodic_data_() {
if (this->direction_dedup_[index].next(direction)) {
text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
if (tsd != nullptr) {
tsd->publish_state(DirectionStrings::get_progmem_str(direction, DirectionStrings::LAST_INDEX));
tsd->publish_state(find_str(ld2450::DIRECTION_BY_UINT, direction));
}
}
#endif
@@ -807,15 +864,8 @@ void LD2450Component::set_bluetooth(bool enable) {
// Set Baud rate
void LD2450Component::set_baud_rate(const char *state) {
const int16_t index = progmem_string_table_find<BaudRateStrings>(state, strlen(state));
if (index >= 0) {
this->set_baud_rate(static_cast<BaudRate>(index + 1));
}
}
void LD2450Component::set_baud_rate(BaudRate rate) {
this->set_config_mode_(true);
const uint8_t cmd_value[2] = {rate, 0x00};
const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
this->set_timeout(200, [this]() { this->restart_(); });
}
@@ -823,14 +873,8 @@ void LD2450Component::set_baud_rate(BaudRate rate) {
// Set Zone Type - one of: Disabled, Detection, Filter
void LD2450Component::set_zone_type(const char *state) {
ESP_LOGV(TAG, "Set zone type: %s", state);
const int16_t index = progmem_string_table_find<ZoneTypeStrings>(state, strlen(state));
if (index >= 0) {
this->set_zone_type(static_cast<ZoneType>(index));
}
}
void LD2450Component::set_zone_type(ZoneType type) {
this->zone_type_ = type;
uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state);
this->zone_type_ = zone_type;
this->send_set_zone_command_();
}
@@ -838,7 +882,7 @@ void LD2450Component::set_zone_type(ZoneType type) {
void LD2450Component::publish_zone_type() {
#ifdef USE_SELECT
if (this->zone_type_select_ != nullptr) {
this->zone_type_select_->publish_state(static_cast<size_t>(this->zone_type_));
this->zone_type_select_->publish_state(find_str(ZONE_TYPE_BY_UINT, this->zone_type_));
}
#endif
}
-19
View File
@@ -55,23 +55,6 @@ enum Direction : uint8_t {
DIRECTION_UNDEFINED = 4,
};
enum BaudRate : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8
};
enum ZoneType : uint8_t {
ZONE_DISABLED = 0,
ZONE_DETECTION = 1,
ZONE_FILTER = 2,
};
// Target coordinate struct
struct Target {
int16_t x;
@@ -138,9 +121,7 @@ class LD2450Component : public Component, public uart::UARTDevice {
void set_bluetooth(bool enable);
void set_multi_target(bool enable);
void set_baud_rate(const char *state);
void set_baud_rate(BaudRate rate);
void set_zone_type(const char *state);
void set_zone_type(ZoneType type);
void publish_zone_type();
void factory_reset();
#ifdef USE_TEXT_SENSOR
@@ -35,7 +35,6 @@ CONFIG_SCHEMA = {
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_LD2450_ID])
selects = select.sub_selects(config, parent=hub)
# Option lists follow the order of the C++ enums; the hub maps by index
await selects(
CONF_BAUD_RATE,
hub.set_baud_rate_select,
@@ -4,7 +4,7 @@ namespace esphome::ld2450 {
void BaudRateSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_baud_rate(static_cast<BaudRate>(index + 1));
this->parent_->set_baud_rate(this->option_at(index));
}
} // namespace esphome::ld2450
@@ -4,7 +4,7 @@ namespace esphome::ld2450 {
void ZoneTypeSelect::control(size_t index) {
this->publish_state(index);
this->parent_->set_zone_type(static_cast<ZoneType>(index));
this->parent_->set_zone_type(this->option_at(index));
}
} // namespace esphome::ld2450
+3 -11
View File
@@ -1,7 +1,6 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <memory>
@@ -25,17 +24,10 @@
namespace esphome::ld24xx {
// Index of value in a constant table that may live in flash, or none
template<typename T, size_t N> optional<size_t> find_index(const T (&arr)[N], T value) {
static_assert(sizeof(T) == 1 || sizeof(T) == 4, "flash tables are read as bytes or aligned 32 bit words");
// Helper to find index of value in constexpr array
template<size_t N> optional<size_t> find_index(const uint32_t (&arr)[N], uint32_t value) {
for (size_t i = 0; i < N; i++) {
T entry;
if constexpr (sizeof(T) == 1) {
entry = progmem_read_byte(reinterpret_cast<const uint8_t *>(&arr[i]));
} else {
entry = arr[i]; // an aligned 32 bit read from flash is safe on ESP8266
}
if (entry == value)
if (arr[i] == value)
return i;
}
return {};
+6 -8
View File
@@ -1066,20 +1066,19 @@ 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_count_ >= CMD_QUEUE_SIZE) {
if (this->cmd_queue_.size() >= CMD_QUEUE_SIZE) {
ESP_LOGW(TAG, "Command queue full, dropping command 0x%04X", type);
return false;
}
PendingCommand &cmd = this->cmd_queue_[this->cmd_tail_];
PendingCommand cmd;
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;
}
@@ -1146,12 +1145,11 @@ void LD6002BComponent::process_command_queue_() {
return;
}
if (this->cmd_count_ == 0)
if (this->cmd_queue_.empty())
return;
this->active_command_ = this->cmd_queue_[this->cmd_head_];
this->cmd_head_ = (this->cmd_head_ + 1) % CMD_QUEUE_SIZE;
this->cmd_count_--;
this->active_command_ = this->cmd_queue_.front();
this->cmd_queue_.pop();
this->send_generation_++;
this->retries_left_ = CMD_MAX_RETRIES;
+1 -4
View File
@@ -419,10 +419,7 @@ 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;
std::array<PendingCommand, CMD_QUEUE_SIZE> cmd_queue_{};
uint8_t cmd_head_{0};
uint8_t cmd_tail_{0};
uint8_t cmd_count_{0};
StaticRingBuffer<PendingCommand, CMD_QUEUE_SIZE> cmd_queue_;
bool command_active_{false};
bool command_sent_{false};
PendingCommand active_command_{};
+8 -12
View File
@@ -1,7 +1,6 @@
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import libretiny, output
from esphome.components.libretiny.const import FAMILY_LN882H
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_PIN
from esphome.types import ConfigType
@@ -13,16 +12,13 @@ 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 = 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),
)
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)
async def to_code(config: ConfigType) -> None:
@@ -134,13 +134,6 @@ 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,10 +1,8 @@
#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"
@@ -103,28 +101,17 @@ 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;
uint8_t random_slot; ///< index into the effect's random colors, or NO_RANDOM_SLOT
bool random;
size_t num_leds;
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) {}
/// 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_colors(const std::initializer_list<AddressableColorWipeEffectColor> &colors) { this->colors_ = 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 {
@@ -137,11 +124,12 @@ class AddressableColorWipeEffect : public AddressableLightEffect {
} else {
it.shift_right(1);
}
const AddressableColorWipeEffectColor color = this->entry_(this->at_color_);
Color esp_color = this->color_of_(color);
const AddressableColorWipeEffectColor &color = this->colors_[this->at_color_];
Color esp_color = Color(color.r, color.g, color.b, color.w);
if (color.gradient) {
size_t next_color_index = (this->at_color_ + 1) % this->colors_.size();
const Color next_esp_color = this->color_of_(this->entry_(next_color_index));
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);
uint8_t gradient = 255 * ((float) this->leds_added_ / color.num_leds);
esp_color = esp_color.gradient(next_esp_color, gradient);
}
@@ -153,32 +141,19 @@ 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();
const AddressableColorWipeEffectColor new_color = this->entry_(this->at_color_);
if (new_color.random_slot != NO_RANDOM_SLOT) {
AddressableColorWipeEffectColor &new_color = this->colors_[this->at_color_];
if (new_color.random) {
Color c = Color::random_color();
Color &slot = this->random_colors_[new_color.random_slot];
slot.r = c.r;
slot.g = c.g;
slot.b = c.b;
new_color.r = c.r;
new_color.g = c.g;
new_color.b = c.b;
}
}
it.schedule_show();
}
protected:
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};
FixedVector<AddressableColorWipeEffectColor> colors_;
size_t at_color_{0};
uint32_t last_add_{0};
uint32_t add_led_interval_{};
+8 -34
View File
@@ -1,10 +1,8 @@
#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"
@@ -156,25 +154,11 @@ 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 {
float state;
float brightness;
float color_brightness;
float red;
float green;
float blue;
float white;
float color_temperature;
float cold_white;
float warm_white;
LightColorValues color;
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:
@@ -182,22 +166,19 @@ 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->duration_(this->at_color_);
this->last_switch_ = millis() - this->colors_.back().duration;
}
void apply() override {
const uint32_t now = millis();
if (now - this->last_switch_ < this->duration_(this->at_color_))
if (now - this->last_switch_ < this->colors_[this->at_color_].duration)
return;
// Switch to next color
this->at_color_ = (this->at_color_ + 1) % this->colors_.size();
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 color = this->colors_[this->at_color_].color;
auto call = this->state_->turn_on();
call.from_light_color_values(color);
call.from_light_color_values(this->colors_[this->at_color_].color);
if (!color.is_on()) {
// Don't turn the light off, otherwise the light effect will be stopped
@@ -206,22 +187,15 @@ class StrobeLightEffect : public LightEffect {
}
call.set_publish(false);
call.set_save(false);
call.set_transition_length_if_supported(step.transition_length);
call.set_transition_length_if_supported(this->colors_[this->at_color_].transition_length);
call.perform();
this->last_switch_ = now;
}
/// 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}; }
void set_colors(const std::initializer_list<StrobeLightEffectColor> &colors) { this->colors_ = colors; }
protected:
uint32_t duration_(size_t index) const {
uint32_t duration;
progmem_memcpy(&duration, &this->colors_[index].duration, sizeof(duration));
return duration;
}
ConstVector<StrobeLightEffectColor> colors_;
FixedVector<StrobeLightEffectColor> colors_;
uint32_t last_switch_{0};
size_t at_color_{0};
};
-27
View File
@@ -2,9 +2,6 @@
#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.
@@ -39,30 +36,6 @@ 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
+26 -64
View File
@@ -29,7 +29,6 @@ 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
@@ -53,6 +52,7 @@ from .types import (
ColorMode,
FlickerLightEffect,
LambdaLightEffect,
LightColorValues,
LightStateRef,
PulseLightEffect,
RandomLightEffect,
@@ -351,26 +351,28 @@ async def strobe_effect_to_code(config, effect_id):
colors = [
cg.StructInitializer(
StrobeLightEffectColor,
("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]),
(
"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],
),
),
("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, [])
]
table = cg.shared_progmem_array(
"light_strobe_colors", StrobeLightEffectColor, cg.ArrayInitializer(*colors)
)
cg.add(var.set_colors(table, len(colors)))
cg.add(var.set_colors(colors))
return var
@@ -429,16 +431,6 @@ 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,
@@ -467,11 +459,7 @@ ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA = _with_color_option(
{
cv.Optional(
CONF_COLORS, default=[{CONF_NUM_LEDS: 1, CONF_RANDOM: True}]
): cv.All(
cv.ensure_list(ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA),
cv.Length(min=1),
_validate_random_slots,
),
): cv.ensure_list(ADDRESSABLE_COLOR_WIPE_COLOR_SCHEMA),
cv.Optional(
CONF_ADD_LED_INTERVAL, default="0.1s"
): cv.positive_time_period_milliseconds,
@@ -483,47 +471,21 @@ 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", rgbw[0]),
("g", rgbw[1]),
("b", rgbw[2]),
("w", rgbw[3]),
("random_slot", random_slot),
("gradient", color[CONF_GRADIENT]),
("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]),
("gradient", color[CONF_GRADIENT]),
)
)
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))
cg.add(var.set_colors(colors))
return var
@@ -141,13 +141,32 @@ 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->throttle_show_(now, this->last_refresh_, this->max_refresh_rate_))
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();
return;
}
if (spi_dma_busy()) {
// Busy sending the last frame, try again later.
@@ -172,6 +191,20 @@ 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,21 +23,16 @@ class LN882xSPILEDStripLightOutput final : public light::AddressableLight {
void dump_config() override;
float get_setup_priority() const override;
light::LightTraits get_traits() override { return this->channel_colors_.traits(); }
light::LightTraits get_traits() override;
int32_t size() const override { return this->num_leds_; }
void clear_effect_data() override {
if (this->effect_data_ != nullptr)
memset(this->effect_data_, 0, this->num_leds_);
}
void clear_effect_data() override;
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 {
return this->channel_colors_.view(this->color_data_, this->effect_data_, index, &this->correction_);
}
light::ESPColorView get_view_internal(int32_t index) const override;
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))
+1 -6
View File
@@ -210,15 +210,10 @@ class WidgetType:
async def update_to_code(self, w: "Widget", config: dict) -> None:
"""
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.
Update a widget. Defaults to calling to_code, but 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):
+2 -1
View File
@@ -20,9 +20,10 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(MCP23016),
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
}
)
.extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x20))
)
+21 -2
View File
@@ -25,9 +25,25 @@ void MCP23016::setup() {
// all pins input
this->write_reg_(MCP23016_IODIR1, 0xFFFF);
this->setup_interrupt_pin_();
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();
}
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); }
@@ -35,11 +51,14 @@ 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);
this->enable_polling_loop_();
if (this->interrupt_pin_ == nullptr) {
this->enable_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;
+9 -2
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_component.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
namespace esphome::mcp23016 {
@@ -25,14 +25,20 @@ enum MCP23016GPIORegisters {
MCP23016_IOCON1 = 0x0B,
};
class MCP23016 final : public gpio_expander::CachedGpioExpanderComponent<uint16_t, 16>, public i2c::I2CDevice {
class MCP23016 final : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander<uint16_t, 16> {
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;
@@ -48,6 +54,7 @@ class MCP23016 final : public gpio_expander::CachedGpioExpanderComponent<uint16_
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,11 +32,15 @@ void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) {
} else if (flags == gpio::FLAG_OUTPUT) {
this->update_reg(pin, false, iodir);
}
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_();
// 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();
}
}
@@ -43,11 +43,15 @@ void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) {
} else if (flags == gpio::FLAG_OUTPUT) {
this->update_reg(pin, false, iodir);
}
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_();
// 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();
}
}
+2 -1
View File
@@ -32,8 +32,9 @@ 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(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
).extend(cv.COMPONENT_SCHEMA)
@coroutine
@@ -4,6 +4,8 @@
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_component.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
@@ -8,15 +8,47 @@ namespace esphome::mcp23xxx_base {
enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING };
template<uint8_t N> class MCP23XXXBase : public gpio_expander::CachedGpioExpanderComponent<uint8_t, N> {
// 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> {
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
@@ -25,6 +57,7 @@ template<uint8_t N> class MCP23XXXBase : public gpio_expander::CachedGpioExpande
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 {
+3 -2
View File
@@ -173,8 +173,9 @@ void AirConditioner::do_follow_me(float temperature, bool use_fahrenheit, bool b
return;
}
// 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)));
// 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)));
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
@@ -1,232 +0,0 @@
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
@@ -1,282 +0,0 @@
#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
@@ -1,27 +0,0 @@
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)
+27 -233
View File
@@ -1,5 +1,3 @@
from collections.abc import Iterator
from esphome import automation
import esphome.codegen as cg
from esphome.components import logger, network, socket
@@ -12,7 +10,6 @@ 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,
@@ -29,8 +26,6 @@ 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,
@@ -38,17 +33,11 @@ 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,
@@ -57,7 +46,6 @@ from esphome.const import (
CONF_PAYLOAD,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_PLATFORM,
CONF_PORT,
CONF_PUBLISH_NAN_AS_NONE,
CONF_QOS,
@@ -67,9 +55,6 @@ 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,
@@ -84,9 +69,6 @@ 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"]
@@ -112,12 +94,10 @@ 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
# 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.
# 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.
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):
@@ -147,91 +127,34 @@ 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)
# 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"
MQTTAlarmControlPanelComponent = mqtt_ns.class_(
"MQTTAlarmControlPanelComponent", 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")
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)
MQTTDiscoveryUniqueIdGenerator = mqtt_ns.enum("MQTTDiscoveryUniqueIdGenerator")
MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS = {
@@ -475,137 +398,6 @@ 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:
@@ -774,7 +566,9 @@ async def to_code(config):
((CONF_PAYLOAD, cg.std_string, automation.string_ref_literal),),
),
)
cg.add(var.subscribe(_topic_source(conf[CONF_TOPIC]), callback, conf[CONF_QOS]))
cg.add(
var.subscribe(cg.progmem_string(conf[CONF_TOPIC]), callback, conf[CONF_QOS])
)
for conf in config.get(CONF_ON_JSON_MESSAGE, []):
callback = await automation.build_trigger_callback(
@@ -785,7 +579,7 @@ async def to_code(config):
)
cg.add(
var.subscribe_json(
_topic_source(conf[CONF_TOPIC]), callback, conf[CONF_QOS]
cg.progmem_string(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->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->get_command_topic_(), [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->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
if (payload == "PRESS") {
this->button_->press();
} else {
+12 -67
View File
@@ -98,16 +98,12 @@ void MQTTClientComponent::setup() {
if (this->is_discovery_ip_enabled()) {
this->subscribe(
static_topic("esphome/discover"),
[this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
"esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); },
2);
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};
// Format topic on stack - subscribe() copies it
char ping_topic[ESPHOME_TOPIC_BUFFER_SIZE];
format_esphome_topic(ping_topic, "ping");
this->subscribe(
ping_topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2);
}
@@ -483,12 +479,6 @@ 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;
@@ -497,7 +487,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_topic_(sub->topic, sub->qos);
sub->subscribed = this->subscribe_(sub->topic.c_str(), sub->qos);
sub->resubscribe_timeout = now;
}
}
@@ -520,57 +510,20 @@ void MQTTClientComponent::handle_crash_log_request_(const std::string &topic, co
}
#endif
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,
void MQTTClientComponent::add_subscription_(std::string &&topic, mqtt_subscription_callback_t callback, bool boxed,
uint8_t qos) {
auto &subscription = this->subscriptions_.emplace_back(MQTTSubscription{
.topic = topic,
.callback = callback,
.resubscribe_timeout = 0,
.topic = std::move(topic),
.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();
@@ -583,7 +536,7 @@ void MQTTClientComponent::unsubscribe(const std::string &topic) {
}
for (auto &subscription : this->subscriptions_) {
if (topic_equals(subscription.topic, topic))
if (subscription.topic == topic)
subscription.removed = true;
}
// A callback may be running; the outermost dispatch erases them once it returns.
@@ -600,8 +553,6 @@ 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;
@@ -678,12 +629,6 @@ 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.
@@ -717,7 +662,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_matches(topic.c_str(), subscription.topic))
if (!subscription.removed && topic_match(topic.c_str(), subscription.topic.c_str()))
subscription.callback.call(topic, payload);
}
this->dispatching_ = was_dispatching;
+13 -66
View File
@@ -8,11 +8,8 @@
#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
@@ -25,7 +22,6 @@
#endif
#include "lwip/ip_addr.h"
#include <span>
#include <vector>
// The crash log replay needs the logger, which mqtt does not depend on
@@ -35,18 +31,6 @@
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>;
@@ -62,33 +46,15 @@ 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 {
MQTTTopicSource topic;
mqtt_subscription_callback_t callback;
uint32_t resubscribe_timeout;
std::string topic;
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.
@@ -217,44 +183,32 @@ 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(const std::string &topic, F &&callback, uint8_t qos = 0) {
this->subscribe(copy_topic(StringRef(topic)), std::forward<F>(callback), qos);
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);
}
/** 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 An MQTTTopicSource, or a string that is copied to the heap.
* @param topic The topic. Wildcards are currently not supported.
* @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 T, typename F> void subscribe_json(T &&topic, F &&callback, uint8_t qos = 0) {
template<typename F> void subscribe_json(std::string 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::forward<T>(topic),
std::move(topic),
[cb = std::forward<F>(callback)](const std::string &topic, const std::string &payload) {
call_json(cb, topic, payload);
},
@@ -347,12 +301,6 @@ 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.
@@ -369,7 +317,6 @@ 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);
@@ -381,7 +328,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_(MQTTTopicSource topic, mqtt_subscription_callback_t callback, bool boxed, uint8_t qos);
void add_subscription_(std::string &&topic, mqtt_subscription_callback_t callback, bool boxed, uint8_t qos);
void resubscribe_subscriptions_();
MQTTCredentials credentials_;
+29 -31
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_source(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->get_mode_command_topic(), [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_source(),
this->subscribe(this->get_target_temperature_low_command_topic(),
[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_source(),
this->subscribe(this->get_target_temperature_high_command_topic(),
[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_source(),
this->subscribe(this->get_target_temperature_command_topic(),
[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_source(),
this->subscribe(this->get_target_humidity_command_topic(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_number<float>(payload);
if (!val.has_value()) {
@@ -268,30 +268,27 @@ void MQTTClimateComponent::setup() {
}
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
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();
});
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();
});
}
if (traits.get_supports_fan_modes()) {
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();
});
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();
});
}
if (traits.get_supports_swing_modes()) {
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->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->device_->add_on_state_callback([this](Climate & /*unused*/) { this->publish_state_(); });
@@ -321,15 +318,15 @@ bool MQTTClimateComponent::publish_state_() {
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
if (!this->publish_float_(this->get_target_temperature_low_state_topic_to(topic_buf),
this->device_->target_temperature_low, target_accuracy))
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))
success = false;
if (!this->publish_float_(this->get_target_temperature_high_state_topic_to(topic_buf),
this->device_->target_temperature_high, target_accuracy))
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))
success = false;
} else {
if (!this->publish_float_(this->get_target_temperature_state_topic_to(topic_buf), this->device_->target_temperature,
target_accuracy))
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))
success = false;
}
@@ -340,8 +337,9 @@ bool MQTTClimateComponent::publish_state_() {
success = false;
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) &&
(!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))
!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))
success = false;
}
@@ -2,8 +2,6 @@
#ifdef USE_MQTT
#include <cmath>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -126,14 +124,6 @@ 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);
@@ -180,14 +170,6 @@ 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);
}
+20 -33
View File
@@ -21,14 +21,23 @@ struct SendDiscoveryConfig {
bool command_topic{true}; ///< If the command topic should be included. Default to true.
};
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;
// 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)
// 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)
@@ -40,9 +49,7 @@ 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.
// Set the topic before setup(): subscriptions rebuild it when needed, so a later change only reaches the broker
// after a reconnect.
// Macro to define custom topic getter/setter with compile-time suffix length verification
#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"); \
@@ -57,10 +64,6 @@ 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); \
@@ -125,7 +128,6 @@ 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);
}
@@ -253,33 +255,30 @@ class MQTTComponent : public Component {
/** Subscribe to a MQTT topic.
*
* @param topic The topic: an MQTTTopicSource, or a string that is copied to the heap.
* @param topic The topic. Wildcards are currently not supported.
* @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 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);
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);
}
/** 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: an MQTTTopicSource, or a string that is copied to the heap.
* @param topic The topic. Wildcards are currently not supported.
* @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 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);
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);
}
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;
@@ -323,18 +322,6 @@ 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;
+25 -27
View File
@@ -31,40 +31,38 @@ 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->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->get_command_topic_(), [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_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();
});
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();
});
}
if (traits.get_supports_tilt()) {
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();
});
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();
});
}
}
+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->command_topic_source_(), [this](const std::string &topic, JsonObject root) {
this->subscribe_json(this->get_command_topic_(), [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->command_topic_source_(), [this](const std::string &topic, JsonObject root) {
this->subscribe_json(this->get_command_topic_(), [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")]);
+26 -27
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->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
auto val = parse_on_off(payload.c_str());
switch (val) {
case PARSE_ON:
@@ -52,34 +52,33 @@ void MQTTFanComponent::setup() {
});
if (this->state_->get_traits().supports_direction()) {
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;
}
});
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;
}
});
}
if (this->state_->get_traits().supports_oscillation()) {
this->subscribe(this->get_oscillation_command_topic_source(),
this->subscribe(this->get_oscillation_command_topic(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_on_off(payload.c_str(), "oscillate_on", "oscillate_off");
switch (val) {
@@ -103,7 +102,7 @@ void MQTTFanComponent::setup() {
}
if (this->state_->get_traits().supports_speed()) {
this->subscribe(this->get_speed_level_command_topic_source(),
this->subscribe(this->get_speed_level_command_topic(),
[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->command_topic_source_(), [this](const std::string &topic, JsonObject root) {
this->subscribe_json(this->get_command_topic_(), [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->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->get_command_topic_(), [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->command_topic_source_(), [this](const std::string &topic, const std::string &state) {
this->subscribe(this->get_command_topic_(), [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->command_topic_source_(), [this](const std::string &topic, const std::string &state) {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) {
auto call = this->select_->make_call();
call.set_option(state);
call.perform();
+6 -1
View File
@@ -73,7 +73,12 @@ bool MQTTSensorComponent::send_initial_state() {
}
bool MQTTSensorComponent::publish_state(float value) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_float_(this->get_state_topic_to_(topic_buf), value, this->sensor_->get_accuracy_decimals());
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);
}
} // 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->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->get_command_topic_(), [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->command_topic_source_(), [this](const std::string &topic, const std::string &state) {
this->subscribe(this->get_command_topic_(), [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->command_topic_source_(), [this](const std::string &topic, JsonObject root) {
this->subscribe_json(this->get_command_topic_(), [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->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
if (payload == "INSTALL") {
this->update_->perform();
} else {
+11 -12
View File
@@ -31,23 +31,22 @@ 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->command_topic_source_(), [this](const std::string &topic, const std::string &payload) {
this->subscribe(this->get_command_topic_(), [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_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();
});
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();
});
}
}
@@ -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; // 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)
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;
// 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) {
+2 -1
View File
@@ -29,9 +29,10 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent),
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
}
)
.extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x21))
)
+26 -3
View File
@@ -50,7 +50,24 @@ void PCA6416AComponent::setup() {
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
this->setup_interrupt_pin_();
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();
}
}
void PCA6416AComponent::dump_config() {
@@ -96,6 +113,9 @@ 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_) {
@@ -104,11 +124,12 @@ 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) {
@@ -161,6 +182,8 @@ 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")
+11 -2
View File
@@ -3,23 +3,31 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/gpio_expander/cached_gpio_component.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
namespace esphome::pca6416a {
class PCA6416AComponent final : public gpio_expander::CachedGpioExpanderComponent<uint8_t, 16>, public i2c::I2CDevice {
class PCA6416AComponent final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, 16> {
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;
@@ -38,6 +46,7 @@ class PCA6416AComponent final : public gpio_expander::CachedGpioExpanderComponen
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>;
+2 -1
View File
@@ -24,9 +24,10 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(PCA9505Component),
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
}
)
.extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x20))
)
+28 -3
View File
@@ -38,7 +38,28 @@ void PCA9505Component::setup() {
}
this->write_all_banks_(MASK_REG, 0xFF);
this->setup_interrupt_pin_();
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();
}
}
void PCA9505Component::dump_config() {
@@ -63,9 +84,11 @@ 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.
// An input may raise INT; the polling loop is only needed when there is no interrupt pin.
this->mask_[bank] &= ~mask;
this->enable_polling_loop_();
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags & gpio::FLAG_OUTPUT) {
this->config_[bank] |= mask;
// An output must never raise INT.
@@ -107,6 +130,8 @@ 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")
+12 -3
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_component.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
#include "esphome/components/i2c/i2c.h"
@@ -14,15 +14,23 @@ 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 gpio_expander::CachedGpioExpanderComponent<uint8_t, PIN_COUNT>,
public i2c::I2CDevice {
class PCA9505Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, PIN_COUNT> {
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;
@@ -39,6 +47,7 @@ class PCA9505Component final : public gpio_expander::CachedGpioExpanderComponent
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>;
+4 -2
View File
@@ -2,8 +2,9 @@ from typing import NamedTuple
from esphome import pins
import esphome.codegen as cg
from esphome.components import gpio_expander, i2c
from esphome.components import 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,
@@ -89,8 +90,9 @@ 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(gpio_expander.INTERRUPT_EXPANDER_SCHEMA),
).extend(cv.COMPONENT_SCHEMA),
_validate_hub,
)
+30 -2
View File
@@ -52,7 +52,29 @@ void PCA9554Component::setup() {
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
this->setup_interrupt_pin_();
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();
}
}
void PCA9554Component::dump_config() {
@@ -91,7 +113,11 @@ void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags & gpio::FLAG_INPUT) {
// Clear mode mask bit
this->config_mask_ &= ~(1 << pin);
this->enable_polling_loop_();
// 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();
}
} else if (flags & gpio::FLAG_OUTPUT) {
// Set mode mask bit
this->config_mask_ |= 1 << pin;
@@ -178,6 +204,8 @@ 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")
+11 -2
View File
@@ -3,22 +3,28 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/gpio_expander/cached_gpio_component.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
namespace esphome::pca9554 {
class PCA9554Component final : public gpio_expander::CachedGpioExpanderComponent<uint16_t, 16>, public i2c::I2CDevice {
class PCA9554Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint16_t, 16> {
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; }
@@ -32,6 +38,8 @@ class PCA9554Component final : public gpio_expander::CachedGpioExpanderComponent
#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);
@@ -53,6 +61,7 @@ class PCA9554Component final : public gpio_expander::CachedGpioExpanderComponent
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};
+2 -1
View File
@@ -36,9 +36,10 @@ 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(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x21))
)
+27 -2
View File
@@ -21,7 +21,27 @@ void PCF8574Component::setup() {
this->mode_mask_ = ~this->configured_input_pins_;
}
this->setup_interrupt_pin_();
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();
}
}
void PCF8574Component::dump_config() {
ESP_LOGCONFIG(TAG,
@@ -49,7 +69,11 @@ void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
this->mode_mask_ &= ~(1 << pin);
// Write GPIO to enable input mode
this->write_gpio_();
this->enable_polling_loop_();
// 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();
}
} else if (flags == gpio::FLAG_OUTPUT) {
// Set mode mask bit
this->mode_mask_ |= 1 << pin;
@@ -79,6 +103,7 @@ 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
+11 -2
View File
@@ -3,14 +3,16 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/gpio_expander/cached_gpio_component.h"
#include "esphome/components/gpio_expander/cached_gpio.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 gpio_expander::CachedGpioExpanderComponent<uint16_t, 16>, public i2c::I2CDevice {
class PCF8574Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint16_t, 16> {
public:
PCF8574Component() = default;
@@ -18,15 +20,21 @@ class PCF8574Component final : public gpio_expander::CachedGpioExpanderComponent
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;
@@ -46,6 +54,7 @@ class PCF8574Component final : public gpio_expander::CachedGpioExpanderComponent
// 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>;
+2 -1
View File
@@ -34,9 +34,10 @@ 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(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x43))
)
@@ -34,8 +34,20 @@ void PI4IOE5V6408Component::setup() {
}
}
this->setup_interrupt_pin_();
// 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();
}
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_);
@@ -55,12 +67,26 @@ void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) {
this->pull_up_down_mask_ &= ~(1 << pin);
this->pull_enable_mask_ |= 1 << pin;
}
this->enable_polling_loop_();
// 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();
}
}
// 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;
@@ -151,6 +177,8 @@ 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")
+10 -3
View File
@@ -1,26 +1,32 @@
#pragma once
#include "esphome/components/gpio_expander/cached_gpio_component.h"
#include "esphome/components/gpio_expander/cached_gpio.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 gpio_expander::CachedGpioExpanderComponent<uint8_t, 8>,
public i2c::I2CDevice {
class PI4IOE5V6408Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, 8> {
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;
@@ -37,6 +43,7 @@ class PI4IOE5V6408Component final : public gpio_expander::CachedGpioExpanderComp
uint8_t pull_up_down_mask_{0x00};
bool reset_{true};
InternalGPIOPin *interrupt_pin_{nullptr};
bool read_gpio_modes_();
bool write_gpio_modes_();
+18 -53
View File
@@ -24,16 +24,12 @@ enum class BitOrder : uint8_t {
BIT_ORDER_MSB_FIRST,
};
/// 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.
/// A bit is bit_mark followed by one_space or zero_space.
struct BitTimings {
uint32_t bit_mark{0};
uint32_t bit_mark;
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 {
@@ -47,17 +43,10 @@ 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);
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);
}
this->item(T.bit_mark, (bytes[i] & mask) ? T.one_space : T.zero_space);
}
}
}
@@ -110,12 +99,21 @@ 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 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);
/// 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;
}
void advance(uint32_t amount = 1) { this->index_ += amount; }
void reset() { this->index_ = 0; }
@@ -128,39 +126,6 @@ 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,6 +141,17 @@ 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,
Loaded 100 of 254 files, more files were not shown because too many files have changed in this diff. Show more